diff --git a/.clang-format b/.clang-format index 4897bcf18..ee0c924b3 100644 --- a/.clang-format +++ b/.clang-format @@ -18,7 +18,6 @@ AlwaysBreakAfterDefinitionReturnType: None AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: true AlwaysBreakTemplateDeclarations: true -AllowShortCaseLabelsOnASingleLine: true BinPackArguments: true BinPackParameters: true BraceWrapping: diff --git a/.gitignore b/.gitignore index 25ba3af2e..256c24fc6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /build/ /Submodules/ +/.vscode/ # C++ objects and libs *.slo diff --git a/CMakeLists.txt b/CMakeLists.txt index 3be56113f..2a4295745 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,25 +1,85 @@ +################################################################################### +## ## +## Copyright (C) 2024 Saif Kandil (k0T0z) ## +## ## +## This file is a part of the ENIGMA Development Environment. ## +## ## +## ## +## ENIGMA is free software: you can redistribute it and/or modify it under the ## +## terms of the GNU General Public License as published by the Free Software ## +## Foundation, version 3 of the license or any later version. ## +## ## +## This application and its source code is distributed AS-IS, WITHOUT ANY ## +## WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS ## +## FOR A PARTICULAR PURPOSE. See the GNU General Public License for more ## +## details. ## +## ## +## You should have recieved a copy of the GNU General Public License along ## +## with this code. If not, see ## +## ## +## ENIGMA is an environment designed to create games and other programs with a ## +## high-level, fully compilable language. Developers of ENIGMA or anything ## +## associated with ENIGMA are in no way responsible for its users or ## +## applications created by its users, or damages caused by the environment ## +## or programs made in the environment. ## +## ## +################################################################################### + cmake_minimum_required(VERSION 3.14) project(RadialGM) +# Uncomment to give priority to the local CMake modules +# set(CMAKE_PREFIX_PATH "/usr/local/lib") + include(CMakeDependentOption) option(RGM_BUILD_EMAKE "Build Emake and the compiler." ON) +option(RGM_BUILD_TESTS "Build tests." OFF) # FIXME: MSVC dynamic linking requires US TO DLLEXPORT our funcs # since we currently don't, I'm force disabling the option on MSVC cmake_dependent_option(RGM_BUILD_STATIC "Build static libs." ON "MSVC" OFF) +# Check https://stackoverflow.com/q/33062728/14629018 for more information. +# if(MSVC) +# set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS TRUE) +# endif() + if (RGM_BUILD_STATIC) - set(LIB_TYPE STATIC) + set(LIB_TYPE STATIC CACHE STRING "Static Library type") +else() + set(LIB_TYPE SHARED CACHE STRING "Shared Library type") +endif() + +# Set default build type +if(NOT CMAKE_BUILD_TYPE) + message(STATUS "Build type not set - defaulting to Debug") + set( + CMAKE_BUILD_TYPE "Debug" + CACHE + STRING + "Choose the type of build from: Debug Release MinSizeRel RelWithDebInfo." + FORCE) else() - set(LIB_TYPE SHARED) + if (NOT CMAKE_BUILD_TYPE MATCHES "Debug" AND NOT CMAKE_BUILD_TYPE MATCHES "Release" AND NOT CMAKE_BUILD_TYPE MATCHES "MinSizeRel" AND NOT CMAKE_BUILD_TYPE MATCHES "RelWithDebInfo") + message(FATAL_ERROR "Invalid build type: ${CMAKE_BUILD_TYPE}") + endif() endif() +set(CMAKE_DEBUG_POSTFIX "d") +set(CMAKE_RELEASE_POSTFIX "") +set(CMAKE_MINSIZEREL_POSTFIX "s") +set(CMAKE_RELWITHDEBINFO_POSTFIX "rd") + if (CMAKE_BUILD_TYPE MATCHES "Debug") - set(EXE "RadialGM-Debug") + set(EXE "RadialGM-Debug" CACHE STRING "RGM Executable name") add_definitions(-DRGM_DEBUG) -else() - set(EXE "RadialGM") +elseif(CMAKE_BUILD_TYPE MATCHES "MinSizeRel") + set(EXE "RadialGM-MinSizeRel" CACHE STRING "RGM Executable name") +elseif(CMAKE_BUILD_TYPE MATCHES "RelWithDebInfo") + set(EXE "RadialGM-RelWithDebInfo" CACHE STRING "RGM Executable name") +else() # Release + set(EXE "RadialGM" CACHE STRING "RGM Executable name") endif() set(EXE_DESCRIPTION "ENIGMA IDE") @@ -40,10 +100,30 @@ set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_AUTOUIC_SEARCH_PATHS "${CMAKE_CURRENT_SOURCE_DIR}/Dialogs") +# TODO: Do we need to disable exceptions in RGM like we do in ENIGMA? +# if(MSVC) +# string(APPEND CMAKE_CXX_FLAGS " /EHsc /wd26812") +# string(APPEND CMAKE_C_FLAGS " /EHsc /wd26812") +# endif() + +# # Disable C++ exceptions. +# if(MSVC) +# string(REGEX REPLACE "/EHsc" "/EHs-c-" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") +# add_definitions(-D_HAS_EXCEPTIONS=0) +# else() +# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions -fno-unwind-tables -fno-asynchronous-unwind-tables") +# endif() +set(CMAKE_AUTOUIC_SEARCH_PATHS "${CMAKE_CURRENT_SOURCE_DIR}/" "${CMAKE_CURRENT_SOURCE_DIR}/Dialogs" "${CMAKE_CURRENT_SOURCE_DIR}/Editors") + +# Uncomment to be able to use local grpc_cpp_plugin +# set(GRPC_EXE "/usr/local/bin/grpc_cpp_plugin") + +set(RGM_ROOTDIR "${CMAKE_CURRENT_SOURCE_DIR}") + +# The ENIGMA_DIR is sent to the C++ code as a define # Include ENIGMA things -set(ENIGMA_DIR ${CMAKE_CURRENT_SOURCE_DIR}/Submodules/enigma-dev) +set(ENIGMA_DIR ${RGM_ROOTDIR}/Submodules/enigma-dev CACHE PATH "ENIGMA directory") include_directories("${CMAKE_BINARY_DIR}/Submodules/enigma-dev/shared/protos/" "${ENIGMA_DIR}/CommandLine/libEGM" "${ENIGMA_DIR}/shared") # Populate a CMake variable with the sources @@ -81,12 +161,14 @@ set(RGM_SOURCES Editors/ScriptEditor.cpp Editors/CodeEditor.cpp Editors/TimelineEditor.cpp + Editors/VisualShaderEditor.cpp main.cpp Plugins/RGMPlugin.cpp - Plugins/ServerPlugin.cpp Dialogs/EventArgumentsDialog.cpp Dialogs/TimelineChangeMoment.cpp Dialogs/PreferencesDialog.cpp + Dialogs/PreferencesKeys.cpp + Dialogs/KeyBindingPreferences.cpp Utils/ProtoManip.cpp Utils/FieldPath.cpp MainWindow.cpp @@ -117,7 +199,6 @@ set(RGM_HEADERS Models/ModelMapper.h Models/RepeatedSortFilterProxyModel.h Models/TreeSortFilterProxyModel.h - main.h Components/RecentFiles.h Components/QMenuView_p.h Components/Utility.h @@ -139,13 +220,14 @@ set(RGM_HEADERS Editors/FontEditor.h Editors/SpriteEditor.h Editors/BackgroundEditor.h - Plugins/ServerPlugin.h + Editors/VisualShaderEditor.h Plugins/RGMPlugin.h MainWindow.h Dialogs/EventArgumentsDialog.h Dialogs/PreferencesDialog.h Dialogs/PreferencesKeys.h Dialogs/TimelineChangeMoment.h + Dialogs/KeyBindingPreferences.h Utils/SafeCasts.h Utils/ProtoManip.h Utils/FieldPath.h @@ -196,14 +278,14 @@ else() set(EDITOR_SOURCES Widgets/CodeWidgetScintilla.cpp) endif() - set(RGM_SOURCES ${RGM_SOURCES} Plugins/ServerPlugin.cpp) - set(RGM_HEADERS ${RGM_HEADERS} Plugins/ServerPlugin.h) +set(RGM_SOURCES ${RGM_SOURCES} Plugins/ServerPlugin.cpp) +set(RGM_HEADERS ${RGM_HEADERS} Plugins/ServerPlugin.h) # Tell CMake to create the RadialGM executable add_executable(${EXE} WIN32 ${RGM_UI} ${RGM_HEADERS} ${RGM_SOURCES} ${EDITOR_SOURCES} ${RGM_RC}) # we do this even in release mode for "Editor Diagnostics" -target_compile_definitions(${EXE} PUBLIC QT_MESSAGELOGCONTEXT) +target_compile_definitions(${EXE} PUBLIC QT_MESSAGELOGCONTEXT ENIGMA_DIR="${ENIGMA_DIR}") if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND NOT WIN32) set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address") @@ -244,14 +326,13 @@ include_directories(${EXE} PRIVATE ${RAPIDJSON_INCLUDE_DIRS}) find_package(yaml-cpp CONFIG REQUIRED) target_link_libraries(${EXE} PRIVATE yaml-cpp) - #Find gRPC - find_package(gRPC CONFIG REQUIRED) - target_link_libraries(${EXE} PRIVATE gRPC::gpr gRPC::grpc gRPC::grpc++) +# Find gRPC +find_package(gRPC CONFIG REQUIRED) +target_link_libraries(${EXE} PRIVATE gRPC::gpr gRPC::grpc gRPC::grpc++) # Find Protobuf -include(FindProtobuf) -include_directories(${Protobuf_INCLUDE_DIRS}) -target_link_libraries(${EXE} PRIVATE ${Protobuf_LIBRARIES}) +find_package(Protobuf CONFIG REQUIRED) +target_link_libraries(${EXE} PRIVATE protobuf::libprotobuf) # Find OpenSSL find_package(OpenSSL REQUIRED) @@ -262,11 +343,13 @@ find_package(Qt5 COMPONENTS Core Widgets Gui PrintSupport Multimedia REQUIRED) target_link_libraries(${EXE} PRIVATE Qt5::Core Qt5::Widgets Qt5::Gui Qt5::PrintSupport Qt5::Multimedia) # LibProto -add_subdirectory(Submodules/enigma-dev/shared) -add_subdirectory(Submodules/enigma-dev/shared/protos) -add_subdirectory(Submodules/enigma-dev/CommandLine/libEGM) -add_dependencies(${EXE} "EGM") -target_link_libraries(${EXE} PRIVATE "EGM" "Protocols" "ENIGMAShared") +# Arrangement of these is important: shared depends on proto and emake depends on all of them +# We need to cache the library names first so we can build on top of them +add_subdirectory(${ENIGMA_DIR}/shared/protos) +add_subdirectory(${ENIGMA_DIR}/shared) +add_subdirectory(${ENIGMA_DIR}/CommandLine/libEGM) +add_dependencies(${EXE} ${LIB_EGM}) +target_link_libraries(${EXE} PRIVATE ${LIB_EGM} ${LIB_PROTO} ${SHARED_LIB}) # Find FreeType find_package(Freetype REQUIRED) @@ -289,6 +372,10 @@ target_link_libraries(${EXE} PRIVATE ${LIB_PCRE2}) find_library(LIB_DOUBLE_CONVERSION NAMES double-conversion) target_link_libraries(${EXE} PRIVATE ${LIB_DOUBLE_CONVERSION}) +if(RGM_BUILD_TESTS) + add_subdirectory(Tests) +endif() + if(WIN32) # Windows is a turd target_link_libraries(${EXE} PRIVATE Ws2_32 Wtsapi32 Wldap32 Crypt32 Winmm Userenv Netapi32 version Dwmapi Imm32) @@ -300,16 +387,24 @@ if(MSVC) endif() if (RGM_BUILD_EMAKE) - add_subdirectory(Submodules/enigma-dev/CompilerSource) - add_subdirectory(Submodules/enigma-dev/CommandLine/emake) - if (CMAKE_BUILD_TYPE MATCHES "Debug") - set(CLI_TARGET "emake-debug") - else() - set(CLI_TARGET "emake") - endif() + add_subdirectory(${ENIGMA_DIR}/CompilerSource) + add_subdirectory(${ENIGMA_DIR}/CommandLine/emake) add_dependencies(${EXE} ${CLI_TARGET}) endif() +add_custom_command( + TARGET ${EXE} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_BINARY_DIR}/Submodules/enigma-dev/CommandLine/emake/${CLI_TARGET}${CMAKE_EXECUTABLE_SUFFIX} + ${CMAKE_BINARY_DIR}/Submodules/enigma-dev/CommandLine/libEGM/${CMAKE_SHARED_LIBRARY_PREFIX}${LIB_EGM}$,${CMAKE_DEBUG_POSTFIX},$,${CMAKE_MINSIZEREL_POSTFIX},$,${CMAKE_RELWITHDEBINFO_POSTFIX},>>>${CMAKE_SHARED_LIBRARY_SUFFIX} + ${CMAKE_BINARY_DIR}/Submodules/enigma-dev/CompilerSource/${CMAKE_SHARED_LIBRARY_PREFIX}${COMPILER_LIB}$,${CMAKE_DEBUG_POSTFIX},$,${CMAKE_MINSIZEREL_POSTFIX},$,${CMAKE_RELWITHDEBINFO_POSTFIX},>>>${CMAKE_SHARED_LIBRARY_SUFFIX} + ${CMAKE_BINARY_DIR}/Submodules/enigma-dev/shared/protos/${CMAKE_SHARED_LIBRARY_PREFIX}${LIB_PROTO}$,${CMAKE_DEBUG_POSTFIX},$,${CMAKE_MINSIZEREL_POSTFIX},$,${CMAKE_RELWITHDEBINFO_POSTFIX},>>>${CMAKE_SHARED_LIBRARY_SUFFIX} + ${CMAKE_BINARY_DIR}/Submodules/enigma-dev/shared/${CMAKE_SHARED_LIBRARY_PREFIX}${SHARED_LIB}$,${CMAKE_DEBUG_POSTFIX},$,${CMAKE_MINSIZEREL_POSTFIX},$,${CMAKE_RELWITHDEBINFO_POSTFIX},>>>${CMAKE_SHARED_LIBRARY_SUFFIX} + ${ENIGMA_DIR} + COMMENT "Copying exes to ${ENIGMA_DIR}" +) + install(TARGETS ${EXE} RUNTIME DESTINATION .) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${EXE}.dir/Debug/${EXE}.pdb" DESTINATION . OPTIONAL) @@ -324,9 +419,9 @@ file(TO_CMAKE_PATH ${VCPKG_ROOT} VCPKG_ROOT) set(SEARCH_PATHS "${VCPKG_ROOT}/installed/x64-windows/bin/") endif() else() - set(LIBS "${CMAKE_INSTALL_PREFIX}/${CMAKE_SHARED_LIBRARY_PREFIX}EGM${CMAKE_SHARED_LIBRARY_SUFFIX}" - "${CMAKE_INSTALL_PREFIX}/${CMAKE_SHARED_LIBRARY_PREFIX}Protocols${CMAKE_SHARED_LIBRARY_SUFFIX}" - "${CMAKE_INSTALL_PREFIX}/${CMAKE_SHARED_LIBRARY_PREFIX}ENIGMAShared${CMAKE_SHARED_LIBRARY_SUFFIX}") + set(LIBS "${CMAKE_INSTALL_PREFIX}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIB_EGM}${CMAKE_SHARED_LIBRARY_SUFFIX}" + "${CMAKE_INSTALL_PREFIX}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIB_PROTO}${CMAKE_SHARED_LIBRARY_SUFFIX}" + "${CMAKE_INSTALL_PREFIX}/${CMAKE_SHARED_LIBRARY_PREFIX}${SHARED_LIB}${CMAKE_SHARED_LIBRARY_SUFFIX}") endif() if (WIN32) diff --git a/Dialogs/keybindingpreferences.cpp b/Dialogs/KeyBindingPreferences.cpp similarity index 94% rename from Dialogs/keybindingpreferences.cpp rename to Dialogs/KeyBindingPreferences.cpp index 61b4e313d..957eac93d 100644 --- a/Dialogs/keybindingpreferences.cpp +++ b/Dialogs/KeyBindingPreferences.cpp @@ -1,4 +1,4 @@ -#include "KeybindingPreferences.h" +#include "KeyBindingPreferences.h" #include "ui_MainWindow.h" #include "ui_SpriteEditor.h" diff --git a/Dialogs/keybindingpreferences.h b/Dialogs/KeyBindingPreferences.h similarity index 100% rename from Dialogs/keybindingpreferences.h rename to Dialogs/KeyBindingPreferences.h diff --git a/Dialogs/PreferencesDialog.cpp b/Dialogs/PreferencesDialog.cpp index 489c8f8c2..a25cacec0 100644 --- a/Dialogs/PreferencesDialog.cpp +++ b/Dialogs/PreferencesDialog.cpp @@ -2,8 +2,7 @@ #include "ui_PreferencesDialog.h" #include "PreferencesKeys.h" -#include "KeybindingPreferences.h" -#include "main.h" +#include "KeyBindingPreferences.h" #include "Components/Logger.h" #include diff --git a/Dialogs/PreferencesKeys.cpp b/Dialogs/PreferencesKeys.cpp new file mode 100644 index 000000000..f023935d4 --- /dev/null +++ b/Dialogs/PreferencesKeys.cpp @@ -0,0 +1,30 @@ +/*********************************************************************************/ +/* */ +/* Copyright (C) 2024 Saif Kandil (k0T0z) */ +/* */ +/* This file is a part of the ENIGMA Development Environment. */ +/* */ +/* */ +/* ENIGMA is free software: you can redistribute it and/or modify it under the */ +/* terms of the GNU General Public License as published by the Free Software */ +/* Foundation, version 3 of the license or any later version. */ +/* */ +/* This application and its source code is distributed AS-IS, WITHOUT ANY */ +/* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS */ +/* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more */ +/* details. */ +/* */ +/* You should have recieved a copy of the GNU General Public License along */ +/* with this code. If not, see */ +/* */ +/* ENIGMA is an environment designed to create games and other programs with a */ +/* high-level, fully compilable language. Developers of ENIGMA or anything */ +/* associated with ENIGMA are in no way responsible for its users or */ +/* applications created by its users, or damages caused by the environment */ +/* or programs made in the environment. */ +/* */ +/*********************************************************************************/ + +#include "Dialogs/PreferencesKeys.h" + +QString defaultStyle = ""; diff --git a/Dialogs/PreferencesKeys.h b/Dialogs/PreferencesKeys.h index 46e574c97..4ab196514 100644 --- a/Dialogs/PreferencesKeys.h +++ b/Dialogs/PreferencesKeys.h @@ -3,6 +3,11 @@ #include +// Qt doesn't have a way of getting the default style +// so we store it for later when the user restores +// default settings so we can apply it again +extern QString defaultStyle; + // these are settings key helpers to prevent typos and promote // consistency, or at least turn such mistakes into compile-time // errors and not difficult-to-diagnose issues at runtime diff --git a/Editors/BaseEditor.cpp b/Editors/BaseEditor.cpp index a411d3666..1ac76e056 100644 --- a/Editors/BaseEditor.cpp +++ b/Editors/BaseEditor.cpp @@ -5,6 +5,8 @@ #include #include +BaseEditor::BaseEditor(QWidget* parent) : QWidget(parent) {} + BaseEditor::BaseEditor(MessageModel* resource_model, QWidget* parent) : QWidget(parent), _model(resource_model->GetParentModel()), _nodeMapper(new ModelMapper(_model, this)) { _resMapper = new ModelMapper(resource_model, this); diff --git a/Editors/BaseEditor.h b/Editors/BaseEditor.h index 0cf6de9db..7c4165b2d 100644 --- a/Editors/BaseEditor.h +++ b/Editors/BaseEditor.h @@ -13,12 +13,14 @@ static const QHash ResTypeFields = { {TypeCase::kPath, TreeNode::kPathFieldNumber}, {TypeCase::kFont, TreeNode::kFontFieldNumber}, {TypeCase::kScript, TreeNode::kScriptFieldNumber}, {TypeCase::kTimeline, TreeNode::kTimelineFieldNumber}, {TypeCase::kObject, TreeNode::kObjectFieldNumber}, {TypeCase::kRoom, TreeNode::kRoomFieldNumber}, - {TypeCase::kSettings, TreeNode::kSettingsFieldNumber}, {TypeCase::kShader, TreeNode::kShaderFieldNumber}}; + {TypeCase::kSettings, TreeNode::kSettingsFieldNumber}, {TypeCase::kShader, TreeNode::kShaderFieldNumber}, + {TypeCase::kVisualShader, TreeNode::kVisualShaderFieldNumber}}; class BaseEditor : public QWidget { Q_OBJECT public: + explicit BaseEditor(QWidget *parent = nullptr); explicit BaseEditor(MessageModel *treeNodeModel, QWidget *parent); ~BaseEditor(); void ReplaceBuffer(google::protobuf::Message *buffer); diff --git a/Editors/RoomEditor.cpp b/Editors/RoomEditor.cpp index cd0eda3d8..e941a6502 100644 --- a/Editors/RoomEditor.cpp +++ b/Editors/RoomEditor.cpp @@ -90,15 +90,15 @@ RoomEditor::RoomEditor(MessageModel* model, QWidget* parent) : BaseEditor(model, _ui->objectSelectButton->setMenu(objMenu); _ui->objectSelectButton->setPopupMode(QToolButton::MenuButtonPopup); - auto objects = treeProxy - ->match(treeProxy->index(0, 0), TreeModel::UserRoles::TypeCaseRole, - TypeCase::kObject, 1, Qt::MatchRecursive); - if (!objects.empty()) { - QModelIndex firstObjIdx = objects.first(); - QString firstObj = firstObjIdx.data(Qt::DisplayRole).toString(); - _ui->objectSelectButton->setIcon(firstObjIdx.data(Qt::DecorationRole).value()); - _ui->currentObject->setText(firstObj); - } + // auto objects = treeProxy + // ->match(treeProxy->index(0, 0), TreeModel::UserRoles::TypeCaseRole, + // TypeCase::kObject, 1, Qt::MatchRecursive); + // if (!objects.empty()) { + // QModelIndex firstObjIdx = objects.first(); + // QString firstObj = firstObjIdx.data(Qt::DisplayRole).toString(); + // _ui->objectSelectButton->setIcon(firstObjIdx.data(Qt::DecorationRole).value()); + // _ui->currentObject->setText(firstObj); + // } connect(objMenu, &QMenuView::triggered, [=](const QModelIndex &index) { _ui->currentObject->setText(treeProxy->data(index, Qt::DisplayRole).toString()); @@ -116,9 +116,10 @@ RoomEditor::RoomEditor(MessageModel* model, QWidget* parent) : BaseEditor(model, // This updates all the model views in the event of a sprite is changed connect(MainWindow::resourceMap, &ResourceModelMap::DataChanged, this, [this]() { - _ui->instancesListView->reset(); - _ui->tilesListView->reset(); - _ui->layersPropertiesView->reset(); + // _ui->entitiesListView->reset(); + _ui->elementsListView->reset(); + _ui->layersListView->reset(); + _ui->propertiesView->reset(); }); RoomEditor::RebindSubModels(); @@ -133,35 +134,35 @@ void RoomEditor::RebindSubModels() { RepeatedMessageModel* im = _roomModel->GetSubModel(Room::kInstancesFieldNumber); RepeatedSortFilterProxyModel* imp = new RepeatedSortFilterProxyModel(this); imp->SetSourceModel(im); - _ui->instancesListView->setModel(imp); + // _ui->instancesListView->setModel(imp); - for (int c = 0; c < im->columnCount(); ++c) { - if (c != im->FieldToColumn(Room::Instance::kNameFieldNumber) && - c != im->FieldToColumn(Room::Instance::kObjectTypeFieldNumber) && - c != im->FieldToColumn(Room::Instance::kIdFieldNumber)) - _ui->instancesListView->hideColumn(c); - else - _ui->instancesListView->resizeColumnToContents(c); - } + // for (int c = 0; c < im->columnCount(); ++c) { + // if (c != im->FieldToColumn(Room::Instance::kNameFieldNumber) && + // c != im->FieldToColumn(Room::Instance::kObjectTypeFieldNumber) && + // c != im->FieldToColumn(Room::Instance::kIdFieldNumber)) + // _ui->instancesListView->hideColumn(c); + // else + // _ui->instancesListView->resizeColumnToContents(c); + // } - _ui->instancesListView->header()->swapSections(im->FieldToColumn(Room::Instance::kNameFieldNumber), - im->FieldToColumn(Room::Instance::kObjectTypeFieldNumber)); + // _ui->instancesListView->header()->swapSections(im->FieldToColumn(Room::Instance::kNameFieldNumber), + // im->FieldToColumn(Room::Instance::kObjectTypeFieldNumber)); RepeatedMessageModel* tm = _roomModel->GetSubModel(Room::kTilesFieldNumber); RepeatedSortFilterProxyModel* tmp = new RepeatedSortFilterProxyModel(this); tmp->SetSourceModel(tm); - _ui->tilesListView->setModel(tmp); + _ui->layersListView->setModel(tmp); for (int c = 0; c < tm->columnCount(); ++c) { if (c != tm->FieldToColumn(Room::Tile::kBackgroundNameFieldNumber) && c != tm->FieldToColumn(Room::Tile::kIdFieldNumber) && c != tm->FieldToColumn(Room::Tile::kDepthFieldNumber) && c != tm->FieldToColumn(Room::Tile::kNameFieldNumber)) - _ui->tilesListView->hideColumn(c); + _ui->layersListView->hideColumn(c); else - _ui->tilesListView->resizeColumnToContents(c); + _ui->layersListView->resizeColumnToContents(c); } - _ui->tilesListView->header()->swapSections(tm->FieldToColumn(Room::Tile::kNameFieldNumber), + _ui->layersListView->header()->swapSections(tm->FieldToColumn(Room::Tile::kNameFieldNumber), tm->FieldToColumn(Room::Tile::kBackgroundNameFieldNumber)); RepeatedMessageModel* vm = _roomModel->GetSubModel(Room::kViewsFieldNumber); @@ -170,19 +171,19 @@ void RoomEditor::RebindSubModels() { connect(_ui->elementsListView->selectionModel(), &QItemSelectionModel::selectionChanged, [=](const QItemSelection& selected, const QItemSelection& /*deselected*/) { if (selected.empty()) return; - _ui->tilesListView->clearSelection(); + _ui->layersListView->clearSelection(); auto selectedIndex = selected.indexes().first(); auto currentInstanceModel = imp->GetSubModel(selectedIndex.row()); - _ui->layersPropertiesView->setModel(currentInstanceModel); + _ui->propertiesView->setModel(currentInstanceModel); }); - connect(_ui->tilesListView->selectionModel(), &QItemSelectionModel::selectionChanged, + connect(_ui->layersListView->selectionModel(), &QItemSelectionModel::selectionChanged, [=](const QItemSelection& selected, const QItemSelection& /*deselected*/) { if (selected.empty()) return; - _ui->instancesListView->clearSelection(); + // _ui->instancesListView->clearSelection(); auto selectedIndex = selected.indexes().first(); auto currentInstanceModel = tmp->GetSubModel(selectedIndex.row()); - _ui->layersPropertiesView->setModel(currentInstanceModel); + _ui->propertiesView->setModel(currentInstanceModel); }); BaseEditor::RebindSubModels(); @@ -201,7 +202,7 @@ void RoomEditor::MousePressed(Qt::MouseButton button) { if (button == Qt::MouseButton::LeftButton) { auto index = layerElements->rowCount(); layerElements->insertRow(index); - layerElements->SetData(_ui->currentObject->text(), index, Room::Instance::kObjectTypeFieldNumber); + // layerElements->SetData(_ui->currentObject->text(), index, Room::Instance::kObjectTypeFieldNumber); } } @@ -218,3 +219,7 @@ void RoomEditor::on_actionZoom_triggered() { _ui->roomPreviewBackground->ResetZo void RoomEditor::on_actionShowHideGrid_triggered() { _ui->roomPreviewBackground->SetGridVisible(_ui->actionShowHideGrid->isChecked()); } + +void RoomEditor::updateCursorPositionLabel(const QPoint& pos) { + +} diff --git a/Editors/RoomEditor.h b/Editors/RoomEditor.h index 7847f3ac4..8f399f775 100644 --- a/Editors/RoomEditor.h +++ b/Editors/RoomEditor.h @@ -21,6 +21,12 @@ class RoomEditor : public BaseEditor { void setZoom(qreal zoom); + void MouseMoved(int x, int y); + + void MousePressed(Qt::MouseButton button); + + void MouseReleased(Qt::MouseButton button); + public slots: void RebindSubModels() override; diff --git a/Editors/VisualShaderEditor.cpp b/Editors/VisualShaderEditor.cpp new file mode 100644 index 000000000..39120004d --- /dev/null +++ b/Editors/VisualShaderEditor.cpp @@ -0,0 +1,3957 @@ +/*********************************************************************************/ +/* */ +/* Copyright (C) 2024 Saif Kandil (k0T0z) */ +/* */ +/* This file is a part of the ENIGMA Development Environment. */ +/* */ +/* */ +/* ENIGMA is free software: you can redistribute it and/or modify it under the */ +/* terms of the GNU General Public License as published by the Free Software */ +/* Foundation, version 3 of the license or any later version. */ +/* */ +/* This application and its source code is distributed AS-IS, WITHOUT ANY */ +/* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS */ +/* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more */ +/* details. */ +/* */ +/* You should have recieved a copy of the GNU General Public License along */ +/* with this code. If not, see */ +/* */ +/* ENIGMA is an environment designed to create games and other programs with a */ +/* high-level, fully compilable language. Developers of ENIGMA or anything */ +/* associated with ENIGMA are in no way responsible for its users or */ +/* applications created by its users, or damages caused by the environment */ +/* or programs made in the environment. */ +/* */ +/*********************************************************************************/ + +#include "Editors/VisualShaderEditor.h" + +#include + +#include "EVisualShader.pb.h" + +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ +/***** *****/ +/***** VisualShaderEditor *****/ +/***** *****/ +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ + +VisualShaderEditor::VisualShaderEditor(QWidget* parent) + : BaseEditor(parent), + visual_shader(nullptr), + layout(nullptr), + side_widget(nullptr), + side_outer_layout(nullptr), + side_layout(nullptr), + name_edit(nullptr), + save_button(nullptr), + scene_layer_layout(nullptr), + scene_layer(nullptr), + scene(nullptr), + view(nullptr), + top_layer(nullptr), + menu_bar(nullptr), + menu_button(nullptr), + create_node_button(nullptr), + preview_shader_button(nullptr), + create_node_action(nullptr), + zoom_in_button(nullptr), + reset_zoom_button(nullptr), + zoom_out_button(nullptr), + load_image_button(nullptr), + match_image_button(nullptr), + create_node_dialog(nullptr), + code_previewer_dialog(nullptr), + code_previewer_layout(nullptr), + code_previewer(nullptr) { + VisualShaderEditor::init(); +} + +VisualShaderEditor::VisualShaderEditor(MessageModel* model, QWidget* parent) + : BaseEditor(model, parent), + visual_shader(nullptr), + layout(nullptr), + side_widget(nullptr), + side_outer_layout(nullptr), + side_layout(nullptr), + name_edit(nullptr), + save_button(nullptr), + scene_layer_layout(nullptr), + scene_layer(nullptr), + scene(nullptr), + view(nullptr), + top_layer(nullptr), + menu_bar(nullptr), + menu_button(nullptr), + create_node_button(nullptr), + preview_shader_button(nullptr), + create_node_action(nullptr), + zoom_in_button(nullptr), + reset_zoom_button(nullptr), + zoom_out_button(nullptr), + load_image_button(nullptr), + match_image_button(nullptr), + create_node_dialog(nullptr), + code_previewer_dialog(nullptr), + code_previewer_layout(nullptr), + code_previewer(nullptr) { + VisualShaderEditor::init(); + + _nodeMapper->addMapping(name_edit, TreeNode::kNameFieldNumber); + QObject::connect(save_button, &QAbstractButton::pressed, this, &BaseEditor::OnSave); + RebindSubModels(); + visual_shader_model = _model->GetSubModel(TreeNode::kVisualShaderFieldNumber); +} + +VisualShaderEditor::~VisualShaderEditor() { + if (visual_shader) delete visual_shader; +} + +void VisualShaderEditor::init() { + visual_shader = new VisualShader(); + + // Create the main layout. + layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + layout->setSizeConstraint(QLayout::SetNoConstraint); + layout->setSpacing(5); + layout->setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); + + //////////////// End of Header //////////////// + + // Create the side widget + side_widget = new QWidget(); + side_widget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + side_widget->setContentsMargins(10, 10, 10, 10); // Left, top, right, bottom + side_widget->setVisible(false); + + // Create the side outer layout + side_outer_layout = new QVBoxLayout(side_widget); + side_outer_layout->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + side_outer_layout->setSpacing(5); + side_outer_layout->setAlignment(Qt::AlignTop | Qt::AlignLeft); + side_outer_layout->setSizeConstraint(QLayout::SetNoConstraint); + + // Add the side inner layout + side_layout = new QVBoxLayout(); + side_layout->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + side_layout->setSpacing(5); + side_layout->setAlignment(Qt::AlignTop | Qt::AlignLeft); + side_layout->setSizeConstraint(QLayout::SetNoConstraint); + + // Fill in the left layout + QHBoxLayout* name_layout = new QHBoxLayout(); + name_layout->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + name_layout->setSpacing(5); + name_layout->setAlignment(Qt::AlignTop | Qt::AlignLeft); + name_layout->setSizeConstraint(QLayout::SetNoConstraint); + + QLabel* name_label = new QLabel("Name"); + name_layout->addWidget(name_label, 1); + + name_edit = new QLineEdit(); + name_layout->addWidget(name_edit, 4); + + side_layout->addLayout(name_layout); + + side_outer_layout->addLayout(side_layout); + + save_button = new QPushButton("Save"); + save_button->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + save_button->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + save_button->setToolTip("Save editor changes including the graph"); + save_button->setIcon(QIcon(":/actions/accept.png")); + side_outer_layout->addWidget(save_button); + + side_widget->setLayout(side_outer_layout); + + // Create the scene layer. + scene_layer = new QWidget(); + scene_layer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + scene_layer->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + // Create the scene layer layout. + scene_layer_layout = new QHBoxLayout(scene_layer); + scene_layer_layout->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + scene_layer_layout->setSpacing(0); + scene_layer_layout->setSizeConstraint(QLayout::SetNoConstraint); + scene_layer_layout->setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); + + scene = new VisualShaderGraphicsScene(visual_shader); + scene->set_editor(this); + + view = new VisualShaderGraphicsView(scene, scene_layer); + view->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + VisualShaderEditor::init_graph(); // Must be called after a scene and a view are created. + + scene_layer_layout->addWidget(view); + + // Set the scene layer layout. + scene_layer->setLayout(scene_layer_layout); + + // Create the menu bar layer on top of the scene layer. + top_layer = new QWidget(view); + top_layer->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + top_layer->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + + // Create the menu bar layout. + menu_bar = new QHBoxLayout(top_layer); + menu_bar->setContentsMargins(10, 10, 10, 10); // Left, top, right, bottom + menu_bar->setSpacing(5); // Adjust spacing as needed + menu_bar->setAlignment(Qt::AlignTop | Qt::AlignLeft); + menu_bar->setSizeConstraint(QLayout::SetMinimumSize); + + // Create the menu button + menu_button = new QPushButton("Show Menu", top_layer); + menu_button->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + menu_button->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + menu_button->setToolTip("Toggle Menu"); + menu_bar->addWidget(menu_button); + QObject::connect(menu_button, &QPushButton::pressed, this, &VisualShaderEditor::on_menu_button_pressed); + + // Create the create node button. + create_node_button = new QPushButton("Create Node", top_layer); + create_node_button->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + create_node_button->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + create_node_button->setToolTip("Create a new node"); + menu_bar->addWidget(create_node_button); + QObject::connect(create_node_button, &QPushButton::pressed, this, &VisualShaderEditor::on_create_node_button_pressed); + + this->connect(this, &VisualShaderEditor::create_node_dialog_requested, this, + &VisualShaderEditor::show_create_node_dialog); + + // Create the preview shader button. + preview_shader_button = new QPushButton("Preview Shader", top_layer); + preview_shader_button->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + preview_shader_button->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + preview_shader_button->setToolTip("Preview the expected generated shader code"); + menu_bar->addWidget(preview_shader_button); + QObject::connect(preview_shader_button, &QPushButton::pressed, this, + &VisualShaderEditor::on_preview_shader_button_pressed); + + zoom_in_button = new QPushButton("Zoom In", scene_layer); + zoom_in_button->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + zoom_in_button->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + zoom_in_button->setToolTip("Zoom In"); + menu_bar->addWidget(zoom_in_button); + QObject::connect(zoom_in_button, &QPushButton::pressed, view, &VisualShaderGraphicsView::zoom_in); + + reset_zoom_button = new QPushButton("Reset Zoom", scene_layer); + reset_zoom_button->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + reset_zoom_button->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + reset_zoom_button->setToolTip("Reset Zoom"); + menu_bar->addWidget(reset_zoom_button); + QObject::connect(reset_zoom_button, &QPushButton::pressed, view, &VisualShaderGraphicsView::reset_zoom); + + zoom_out_button = new QPushButton("Zoom Out", scene_layer); + zoom_out_button->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + zoom_out_button->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + zoom_out_button->setToolTip("Zoom Out"); + menu_bar->addWidget(zoom_out_button); + QObject::connect(zoom_out_button, &QPushButton::pressed, view, &VisualShaderGraphicsView::zoom_out); + + load_image_button = new QPushButton("Load Image", scene_layer); + load_image_button->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + load_image_button->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + load_image_button->setToolTip("Load an image to match"); + menu_bar->addWidget(load_image_button); + QObject::connect(load_image_button, &QPushButton::pressed, this, &VisualShaderEditor::on_load_image_button_pressed); + + match_image_button = new QPushButton("Match Image", scene_layer); + match_image_button->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + match_image_button->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + match_image_button->setToolTip("Match the shader to the loaded image"); + menu_bar->addWidget(match_image_button); + QObject::connect(match_image_button, &QPushButton::pressed, this, &VisualShaderEditor::on_match_image_button_pressed); + + // Set the top layer layout. + top_layer->setLayout(menu_bar); + + // Add the left layout + layout->addWidget(side_widget, 1); + + // Add the scene layer to the main layout. + layout->addWidget(scene_layer, 4); + + //////////////////////////////////// + // Code Previewer + //////////////////////////////////// + + code_previewer_dialog = new QDialog(this); + code_previewer_dialog->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + code_previewer_dialog->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + + code_previewer_layout = new QVBoxLayout(code_previewer_dialog); + code_previewer_layout->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + code_previewer_layout->setSpacing(0); // Adjust spacing as needed + code_previewer_layout->setAlignment(Qt::AlignTop | Qt::AlignLeft); + code_previewer_layout->setSizeConstraint(QLayout::SetMinimumSize); + + code_previewer = new QPlainTextEdit(code_previewer_dialog); + code_previewer->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + code_previewer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + code_previewer->setReadOnly(true); + code_previewer->setLineWrapMode(QPlainTextEdit::NoWrap); + code_previewer->setWordWrapMode(QTextOption::NoWrap); + code_previewer->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + code_previewer->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + code_previewer->setTabChangesFocus(true); + code_previewer->setMinimumSize(800, 600); + + code_previewer_layout->addWidget(code_previewer); + + code_previewer_dialog->setLayout(code_previewer_layout); + + //////////////////////////////////// + // CreateNodeDialog Nodes Tree + //////////////////////////////////// + + // Create the create node dialog under the main layout. + create_node_dialog = new CreateNodeDialog(this); + create_node_dialog->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + create_node_dialog->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + + ////////////////////////////////////////// + // CreateNodeDialog Nodes Tree Children + ////////////////////////////////////////// + + const VisualShaderEditor::CreateNodeDialogNodesTreeItem* items{ + VisualShaderEditor::create_node_dialog_nodes_tree_items}; + + // Map to store category items + std::unordered_map category_path_map; + + int i{0}; + + while (!items[i].type.empty()) { + const CreateNodeDialogNodesTreeItem& item{items[i]}; + + // Parse the category string into a vector of strings + std::vector categories{parse_node_category_path(item.category_path)}; + QTreeWidgetItem* parent{nullptr}; // Start from the root + + std::string current_category_path; + // Create/find each level of categories + for (const std::string& category : categories) { + if (!current_category_path.empty()) { + current_category_path += "/"; + } + + current_category_path += category; + + parent = find_or_create_category_item(parent, category, current_category_path, + create_node_dialog->get_nodes_tree(), category_path_map); + } + + // Now add the item to its corresponding parent category + QTreeWidgetItem* node_item = new QTreeWidgetItem(parent); + node_item->setText(0, QString::fromStdString(item.name)); + node_item->setData(0, Qt::UserRole, QString::fromStdString(item.type)); + node_item->setData(0, Qt::UserRole + 1, QString::fromStdString(item.description)); + + i++; + } + + //////////////// Start of Footer //////////////// + + this->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + // this->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + + // Set the window title and icon. + this->setWindowTitle("Visual Shader Editor"); + // this->setWindowIcon(QIcon(":/resources/visual_shader.png")); + this->setLayout(layout); +} + +void VisualShaderEditor::init_graph() { + // Load the nodes and connections from the VisualShader + std::vector ns{visual_shader->get_nodes()}; + for (const int& n_id : ns) { + const std::shared_ptr n{visual_shader->get_node(n_id)}; + + if (!n) { + continue; + } + + TVector2 c{visual_shader->get_node_coordinate(n_id)}; + + scene->add_node(n_id, n, {c.x, c.y}); + } + + std::vector cs{visual_shader->get_connections()}; + for (const VisualShader::Connection& c : cs) { + scene->add_connection(c.from_node, c.from_port, c.to_node, c.to_port); + } +} + +const VisualShaderEditor::CreateNodeDialogNodesTreeItem VisualShaderEditor::create_node_dialog_nodes_tree_items[] = { + + // Input + + {"Input", "Input/Basic", "VisualShaderNodeInput", "Input parameter."}, + + {"ColorConstant", "Input/Basic", "VisualShaderNodeColorConstant", "Color constant."}, + {"BooleanConstant", "Input/Basic", "VisualShaderNodeBooleanConstant", "Boolean constant."}, + {"FloatConstant", "Input/Basic", "VisualShaderNodeFloatConstant", "Scalar floating-point constant."}, + {"IntConstant", "Input/Basic", "VisualShaderNodeIntConstant", "Scalar integer constant."}, + {"UIntConstant", "Input/Basic", "VisualShaderNodeUIntConstant", "Scalar unsigned integer constant."}, + {"Vector2Constant", "Input/Basic", "VisualShaderNodeVec2Constant", "2D vector constant."}, + {"Vector3Constant", "Input/Basic", "VisualShaderNodeVec3Constant", "3D vector constant."}, + {"Vector4Constant", "Input/Basic", "VisualShaderNodeVec4Constant", "4D vector constant."}, + + // Functions + + {"FloatFunc", "Functions/Scalar", "VisualShaderNodeFloatFunc", "Float function."}, + {"IntFunc", "Functions/Scalar", "VisualShaderNodeIntFunc", "Integer function."}, + {"UIntFunc", "Functions/Scalar", "VisualShaderNodeUIntFunc", "Unsigned integer function."}, + {"VectorFunc", "Functions/Vector", "VisualShaderNodeVectorFunc", "Vector function."}, + {"DerivativeFunc", "Functions/Others", "VisualShaderNodeDerivativeFunc", "Derivative function."}, + {"Step", "Functions/Others", "VisualShaderNodeStep", + "Step function( scalar(edge), scalar(x) ).\n\nReturns 0.0 if 'x' is smaller than 'edge' and otherwise 1.0."}, + {"SmoothStep", "Functions/Others", "VisualShaderNodeSmoothStep", + "SmoothStep function( scalar(edge0), scalar(edge1), scalar(x) ).\n\nReturns 0.0 if 'x' is smaller than 'edge0' " + "and 1.0 if x is larger than 'edge1'. Otherwise the return value is interpolated between 0.0 and 1.0 using " + "Hermite polynomials."}, + {"Dot", "Functions/Others", "VisualShaderNodeDotProduct", "Calculates the dot product of two vectors."}, + + // Operators + + {"FloatOp", "Operators/Scalar", "VisualShaderNodeFloatOp", "Float operator."}, + {"IntOp", "Operators/Scalar", "VisualShaderNodeIntOp", "Integer operator."}, + {"UIntOp", "Operators/Scalar", "VisualShaderNodeUIntOp", "Unsigned integer operator."}, + {"VectorOp", "Operators/Vector", "VisualShaderNodeVectorOp", "Vector operator."}, + {"VectorCompose", "Operators/Vector", "VisualShaderNodeVectorCompose", "Composes vector from scalars."}, + {"VectorDecompose", "Operators/Vector", "VisualShaderNodeVectorDecompose", "Decomposes vector to scalars."}, + + // Procedural + + {"ValueNoise", "Procedural/Noise", "VisualShaderNodeValueNoise", + "Generates a simple, or Value, noise based on input 'UV'. The scale of the generated noise is controlled by input " + "'Scale'."}, + {"PerlinNoise", "Procedural/Noise", "VisualShaderNodePerlinNoise", + "Generates a gradient, or Perlin, noise based on input 'UV'. The scale of the generated noise is controlled by " + "input 'Scale'."}, + {"VoronoiNoise", "Procedural/Noise", "VisualShaderNodeVoronoiNoise", + "Generates a Voronoi, or Worley, noise based on input 'UV'. Voronoi noise is generated by calculating distances " + "between a pixel and a lattice of points. By offsetting these points by a pseudo-random number, controlled by " + "input 'Angle Offset', a cluster of cells can be generated. The scale of these cells, and the resulting noise, is " + "controlled by input 'Cell Density'. The output 'Cells' contains the raw cell data."}, + + // Utility + + {"Compare", "Utility/Logic", "VisualShaderNodeCompare", + "Returns the boolean result of the comparison between two parameters."}, + {"If", "Utility/Logic", "VisualShaderNodeIf", + "Returns the value of the 'True' or 'False' input based on the value of the 'Condition' input."}, + {"Switch", "Utility/Logic", "VisualShaderNodeSwitch", + "Returns an associated scalar if the provided boolean value is true or false."}, + {"Is", "Utility/Logic", "VisualShaderNodeIs", + "Returns the boolean result of the comparison between INF (or NaN) and a scalar parameter."}, + + {"", "", "", ""}, +}; + +void VisualShaderEditor::create_node(const QPointF& coordinate) { + QTreeWidgetItem* selected_item{create_node_dialog->get_selected_item()}; + + if (!selected_item) { + return; + } + + VisualShaderEditor::add_node(selected_item, coordinate); +} + +void VisualShaderEditor::add_node(QTreeWidgetItem* selected_item, const QPointF& coordinate) { + std::string type{selected_item->data(0, Qt::UserRole).toString().toStdString()}; + + if (type.empty()) { + return; + } + + scene->add_node(type, coordinate); +} + +void VisualShaderEditor::show_create_node_dialog(const QPointF& coordinate) { + int status{create_node_dialog->exec()}; + switch (status) { + case QDialog::Accepted: + std::cout << "Create node dialog accepted" << std::endl; + VisualShaderEditor::create_node(coordinate); + break; + case QDialog::Rejected: + std::cout << "Create node dialog rejected" << std::endl; + break; + default: + std::cout << "Create node dialog unknown status" << std::endl; + break; + } +} + +void VisualShaderEditor::on_create_node_button_pressed() { + // Send the center of the current view port + QPointF coordinate{view->mapToScene(view->viewport()->rect().center())}; + + Q_EMIT create_node_dialog_requested(coordinate); +} + +void VisualShaderEditor::on_preview_shader_button_pressed() { + bool result{visual_shader->generate_shader()}; + if (!result) { + std::cout << "Failed to generate shader" << std::endl; + return; + } + code_previewer->setPlainText(QString::fromStdString(visual_shader->get_code())); + code_previewer_dialog->exec(); +} + +void VisualShaderEditor::on_menu_button_pressed() { + bool is_visible{side_widget->isVisible()}; + side_widget->setVisible(!is_visible); + menu_button->setText(!is_visible ? "Hide Menu" : "Show Menu"); +} + +void VisualShaderEditor::on_load_image_button_pressed() { + // TODO: Decide on how to load an image + // For example, use QFileDialog to open an image file or + // load an existing sprite or background from the project. + // Then, send the image to OriginalMatchingImageWidget widget to display it. + // R0bert — 27/09/2024 at 22:10 + // i would use resource picker and let user pick a sprite or background that exists in the project + // Josh — 27/09/2024 at 22:13 + // sprites have multiple frames, which is a headache for this project because it's a lot more behavior we need to define +} + +void VisualShaderEditor::on_match_image_button_pressed() {} + +std::vector VisualShaderEditor::parse_node_category_path(const std::string& node_category_path) { + std::vector tokens; + std::stringstream ss(node_category_path); + std::string token; + while (std::getline(ss, token, '/')) { + tokens.push_back(token); + } + return tokens; +} + +QTreeWidgetItem* VisualShaderEditor::find_or_create_category_item( + QTreeWidgetItem* parent, const std::string& category, const std::string& category_path, + QTreeWidget* create_node_dialog_nodes_tree, std::unordered_map& category_path_map) { + // Check if category already exists under parent + if (category_path_map.find(category_path) != category_path_map.end()) { + return category_path_map[category_path]; + } + + // Create a new QTreeWidgetItem + QTreeWidgetItem* new_item; + + if (parent) { + new_item = new QTreeWidgetItem(parent); + } else { + new_item = new QTreeWidgetItem(create_node_dialog_nodes_tree); + } + + new_item->setText(0, QString::fromStdString(category)); + + // Add the new category to the map + category_path_map[category_path] = new_item; + + return new_item; +} + +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ +/***** *****/ +/***** CreateNodeDialog *****/ +/***** *****/ +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ + +CreateNodeDialog::CreateNodeDialog(QWidget* parent) + : QDialog(parent), + layout(nullptr), + create_node_dialog_nodes_tree_layout(nullptr), + create_node_dialog_nodes_tree(nullptr), + create_node_dialog_nodes_description(nullptr), + buttons_layout(nullptr), + create_button(nullptr), + cancel_button(nullptr), + selected_item(nullptr) { + layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + layout->setSizeConstraint(QLayout::SetNoConstraint); + layout->setSpacing(0); + layout->setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); + + //////////////// End of Header //////////////// + + // Create the nodes tree layout. + create_node_dialog_nodes_tree_layout = new QVBoxLayout(); + create_node_dialog_nodes_tree_layout->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + create_node_dialog_nodes_tree_layout->setSpacing(0); // Adjust spacing as needed + create_node_dialog_nodes_tree_layout->setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); + create_node_dialog_nodes_tree_layout->setSizeConstraint(QLayout::SetMinimumSize); + + // Add the nodes tree layout to the main layout. + layout->addLayout(create_node_dialog_nodes_tree_layout); + + // Create the nodes tree. + create_node_dialog_nodes_tree = new QTreeWidget(); + create_node_dialog_nodes_tree->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + create_node_dialog_nodes_tree->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + create_node_dialog_nodes_tree->setColumnCount(1); + create_node_dialog_nodes_tree->setHeaderHidden(true); + this->connect(create_node_dialog_nodes_tree, &QTreeWidget::itemSelectionChanged, this, + &CreateNodeDialog::update_selected_item); + + // Add the nodes tree to the nodes tree layout. + create_node_dialog_nodes_tree_layout->addWidget(create_node_dialog_nodes_tree, + 2); // 2x the size of the nodes description. + + // Create the nodes description. + create_node_dialog_nodes_description = new QTextEdit(); + create_node_dialog_nodes_description->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + create_node_dialog_nodes_description->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + create_node_dialog_nodes_description->setReadOnly(true); + create_node_dialog_nodes_description->setAlignment(Qt::AlignTop | Qt::AlignLeft); + + // Add the nodes description to the nodes tree layout. + create_node_dialog_nodes_tree_layout->addWidget(create_node_dialog_nodes_description, 1); + + // Create the buttons layout. + buttons_layout = new QHBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + layout->setSizeConstraint(QLayout::SetNoConstraint); + layout->setSpacing(0); + layout->setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); + + create_button = new QPushButton("Create"); + QObject::connect(create_button, &QPushButton::pressed, this, &CreateNodeDialog::on_create_node_button_pressed); + create_button->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + create_button->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + create_button->setToolTip("Create the selected node."); + + cancel_button = new QPushButton("Cancel"); + QObject::connect(cancel_button, &QPushButton::pressed, this, + &CreateNodeDialog::on_cancel_node_creation_button_pressed); + cancel_button->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + cancel_button->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + cancel_button->setToolTip("Cancel the node creation."); + + // Add the buttons to the buttons layout. + buttons_layout->addWidget(create_button); + buttons_layout->addWidget(cancel_button); + + // Add the buttons layout to the main layout. + layout->addLayout(buttons_layout); + + //////////////// Start of Footer //////////////// + + this->setWindowTitle("Create Shader Node"); + + this->setLayout(layout); +} + +CreateNodeDialog::~CreateNodeDialog() {} + +void CreateNodeDialog::on_create_node_button_pressed() { this->accept(); } + +void CreateNodeDialog::on_cancel_node_creation_button_pressed() { this->reject(); } + +void CreateNodeDialog::update_selected_item() { + QTreeWidgetItem* item{create_node_dialog_nodes_tree->currentItem()}; + if (item) { + selected_item = item; + create_node_dialog_nodes_description->setText(item->data(0, Qt::UserRole + 1).toString()); + } else { + selected_item = nullptr; + create_node_dialog_nodes_description->setText(""); + } +} + +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ +/***** *****/ +/***** ShaderPreviewerWidget *****/ +/***** *****/ +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ + +ShaderPreviewerWidget::ShaderPreviewerWidget(QWidget* parent) + : QOpenGLWidget(parent), shader_program(nullptr), VAO(0), VBO(0) {} + +ShaderPreviewerWidget::~ShaderPreviewerWidget() {} + +void ShaderPreviewerWidget::set_code(const std::string& new_code) { + if (new_code == code) return; + + code = new_code; + shader_needs_update = true; + if (isVisible()) { + update_shader_program(); + timer.restart(); + } +} + +void ShaderPreviewerWidget::initializeGL() { + QOpenGLFunctions_4_3_Core* f{QOpenGLContext::currentContext()->versionFunctions()}; + + if (!f) { + qWarning() << "Failed to get OpenGL 4.3 functions"; + return; + } + + if (!f->initializeOpenGLFunctions()) { + qWarning() << "Failed to initialize OpenGL functions"; + return; + } + + f->glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // Black background + init_buffers(); + init_shaders(); + + timer.start(); + + connect(context(), &QOpenGLContext::aboutToBeDestroyed, this, &ShaderPreviewerWidget::cleanup); +} + +void ShaderPreviewerWidget::resizeGL(int w, int h) { + QOpenGLFunctions_4_3_Core* f{QOpenGLContext::currentContext()->versionFunctions()}; + + if (!f) { + qWarning() << "Failed to get OpenGL 4.3 functions"; + return; + } + + f->glViewport(0, 0, w, h); +} + +void ShaderPreviewerWidget::paintGL() { + // Check https://doc.qt.io/qt-5/qopenglwidget.html#isValid + // At start, the widget is hidden so this call returns false which results + // in returning (no painting happens). + if (!isValid()) return; + + QOpenGLFunctions_4_3_Core* f{QOpenGLContext::currentContext()->versionFunctions()}; + + if (!f) { + qWarning() << "Failed to get OpenGL 4.3 functions"; + return; + } + + if (shader_needs_update) { + update_shader_program(); + } + + if (!shader_program || !shader_program->isLinked()) { + qWarning() << "Shader program is not linked."; + return; + } + + float time_value{timer.elapsed() * 0.001f}; + + f->glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + f->glClear(GL_COLOR_BUFFER_BIT); + + shader_program->bind(); + shader_program->setUniformValue("uTime", time_value); + + f->glBindVertexArray(VAO); + f->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + f->glBindVertexArray(0); + + shader_program->release(); + + update(); // Request a repaint + Q_EMIT scene_update_requested(); +} + +void ShaderPreviewerWidget::cleanup() { + makeCurrent(); + + QOpenGLFunctions_4_3_Core* f{QOpenGLContext::currentContext()->versionFunctions()}; + + if (!f) { + qWarning() << "Failed to get OpenGL 4.3 functions"; + return; + } + + f->glDeleteVertexArrays(1, &VAO); + f->glDeleteBuffers(1, &VBO); +} + +void ShaderPreviewerWidget::init_buffers() { + QOpenGLFunctions_4_3_Core* f{QOpenGLContext::currentContext()->versionFunctions()}; + + if (!f) { + qWarning() << "Failed to get OpenGL 4.3 functions"; + return; + } + + float vertices[] = { + // coordinates // frag coords + -1.0f, 1.0f, 0.0f, 1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 0.0f}; + + f->glGenVertexArrays(1, &VAO); + f->glGenBuffers(1, &VBO); + + f->glBindVertexArray(VAO); + + f->glBindBuffer(GL_ARRAY_BUFFER, VBO); + f->glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + f->glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0); + f->glEnableVertexAttribArray(0); + + f->glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float))); + f->glEnableVertexAttribArray(1); + + f->glBindVertexArray(0); +} + +void ShaderPreviewerWidget::update_shader_program() { + shader_program.reset(new QOpenGLShaderProgram()); + + const char* vertex_shader_source = R"( + #version 330 core + layout(location = 0) in vec2 aPos; + layout(location = 1) in vec2 aFragCoord; + + out vec2 FragCoord; + + void main() { + gl_Position = vec4(aPos, 0.0, 1.0); + FragCoord = aFragCoord; + } + )"; + + std::string fragment_shader_source{code.empty() ? R"( + #version 330 core + out vec4 FragColor; + in vec2 FragCoord; + + uniform float uTime; + + void main() { + FragColor = vec4(0.0, 0.0, 0.0, 1.0); + } + )" + : "#version 330 core\n\n" + code}; + + if (!shader_program->addShaderFromSourceCode(QOpenGLShader::Vertex, vertex_shader_source)) { + qWarning() << "Vertex shader compilation failed:" << shader_program->log(); + } + + if (!shader_program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragment_shader_source.c_str())) { + qWarning() << "Fragment shader compilation failed:" << shader_program->log(); + } + + if (!shader_program->link()) { + qWarning() << "Shader program linking failed:" << shader_program->log(); + } + + shader_needs_update = false; +} + +void ShaderPreviewerWidget::init_shaders() { update_shader_program(); } + +void ShaderPreviewerWidget::showEvent(QShowEvent* event) { + QOpenGLWidget::showEvent(event); + if (!timer.isValid()) { + // See https://doc.qt.io/qt-5/qelapsedtimer.html#start. + timer.start(); // Start the timer on first show + } +} + +void ShaderPreviewerWidget::hideEvent(QHideEvent* event) { + QOpenGLWidget::hideEvent(event); + // See https://doc.qt.io/qt-5/qelapsedtimer.html#invalidate. + timer.invalidate(); +} + +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ +/***** *****/ +/***** VisualShaderGraphicsScene *****/ +/***** *****/ +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ + +////////////////////////////// +// Public functions +////////////////////////////// + +VisualShaderGraphicsScene::VisualShaderGraphicsScene(VisualShader* vs, QObject* parent) + : QGraphicsScene(parent), vs(vs), temporary_connection_graphics_object(nullptr) { + setItemIndexMethod(QGraphicsScene::NoIndex); // https://doc.qt.io/qt-6/qgraphicsscene.html#ItemIndexMethod-enum +} + +VisualShaderGraphicsScene::~VisualShaderGraphicsScene() {} + +bool VisualShaderGraphicsScene::add_node(const std::string& type, const QPointF& coordinate) { + // Instantiate the node based on the type + std::shared_ptr n; + + if (type == "VisualShaderNodeInput") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeColorConstant") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeBooleanConstant") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeFloatConstant") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeIntConstant") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeUIntConstant") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeVec2Constant") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeVec3Constant") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeVec4Constant") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeFloatFunc") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeIntFunc") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeUIntFunc") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeDerivativeFunc") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeFloatOp") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeIntOp") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeUIntOp") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeValueNoise") { + n = std::make_shared(); + } else if (type == "VisualShaderNodePerlinNoise") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeVoronoiNoise") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeVectorFunc") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeVectorOp") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeVectorCompose") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeVectorDecompose") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeCompare") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeIf") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeIs") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeSwitch") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeStep") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeSmoothStep") { + n = std::make_shared(); + } else if (type == "VisualShaderNodeDotProduct") { + n = std::make_shared(); + } else { + std::cout << "Unknown node type: " << type << std::endl; + } + + if (!n) { + std::cout << "Failed to create node of type: " << type << std::endl; + return false; + } + + int n_id{vs->get_valid_node_id()}; + + if (n_id == (int)VisualShader::NODE_ID_INVALID) { + return false; + } + + return VisualShaderGraphicsScene::add_node(n_id, n, coordinate); +} + +bool VisualShaderGraphicsScene::add_node(const int& n_id, const std::shared_ptr& n, + const QPointF& coordinate) { + // Make sure the node doesn't already exist, we don't want to overwrite a node. + if (node_graphics_objects.find(n_id) != node_graphics_objects.end()) { + return false; + } + + QList views{this->views()}; + if (views.isEmpty()) { + std::cout << "No views available" << std::endl; + return false; + } + + // The output node cannot be removed or added by the user + if (n_id >= (int)VisualShader::NODE_ID_OUTPUT + 1) { + bool result{vs->add_node(n, {(float)coordinate.x(), (float)coordinate.y()}, n_id)}; + + if (!result) { + return false; + } + } + + VisualShaderGraphicsView* view{dynamic_cast(views.first())}; + + if (vs->get_node_coordinate(n_id).x < view->get_x() || + vs->get_node_coordinate(n_id).x > view->get_x() + view->get_width() || + vs->get_node_coordinate(n_id).y < view->get_y() || + vs->get_node_coordinate(n_id).y > view->get_y() + view->get_height()) { + std::cout << "Node is out of view bounds" << std::endl; + } + + VisualShaderNodeGraphicsObject* n_o{new VisualShaderNodeGraphicsObject(n_id, coordinate, n)}; + + QObject::connect(n_o, &VisualShaderNodeGraphicsObject::node_moved, this, &VisualShaderGraphicsScene::on_node_moved); + QObject::connect(n_o, &VisualShaderNodeGraphicsObject::in_port_pressed, this, + &VisualShaderGraphicsScene::on_port_pressed); + QObject::connect(n_o, &VisualShaderNodeGraphicsObject::in_port_dragged, this, + &VisualShaderGraphicsScene::on_port_dragged); + QObject::connect(n_o, &VisualShaderNodeGraphicsObject::in_port_dropped, this, + &VisualShaderGraphicsScene::on_port_dropped); + QObject::connect(n_o, &VisualShaderNodeGraphicsObject::out_port_pressed, this, + &VisualShaderGraphicsScene::on_port_pressed); + QObject::connect(n_o, &VisualShaderNodeGraphicsObject::out_port_dragged, this, + &VisualShaderGraphicsScene::on_port_dragged); + QObject::connect(n_o, &VisualShaderNodeGraphicsObject::out_port_dropped, this, + &VisualShaderGraphicsScene::on_port_dropped); + + QObject::connect(n_o, &VisualShaderNodeGraphicsObject::scene_update_requested, this, + &VisualShaderGraphicsScene::on_scene_update_requested); + QObject::connect(n_o, &VisualShaderNodeGraphicsObject::in_port_remove_requested, this, + &VisualShaderGraphicsScene::on_in_port_remove_requested); + QObject::connect(n_o, &VisualShaderNodeGraphicsObject::out_port_remove_requested, this, + &VisualShaderGraphicsScene::on_out_port_remove_requested); + + if (n_id != (int)VisualShader::NODE_ID_OUTPUT) { + VisualShaderNodeEmbedWidget* embed_widget{new VisualShaderNodeEmbedWidget(n)}; + QGraphicsProxyWidget* embed_widget_proxy{new QGraphicsProxyWidget(n_o)}; + embed_widget_proxy->setWidget(embed_widget); + n_o->set_embed_widget(embed_widget); + QObject::connect(embed_widget, &VisualShaderNodeEmbedWidget::shader_preview_update_requested, this, + &VisualShaderGraphicsScene::on_update_shader_previewer_widgets_requested); + + QObject::connect(embed_widget, &VisualShaderNodeEmbedWidget::node_update_requested, n_o, + &VisualShaderNodeGraphicsObject::on_node_update_requested); + + // Send the shader previewer widget + embed_widget->set_shader_previewer_widget(n_o->get_shader_previewer_widget()); + } + + if (ShaderPreviewerWidget * spw{n_o->get_shader_previewer_widget()}) { + QObject::connect(spw, &ShaderPreviewerWidget::scene_update_requested, this, + &VisualShaderGraphicsScene::on_scene_update_requested); + } + + QObject::connect(n_o, &VisualShaderNodeGraphicsObject::node_deleted, this, + &VisualShaderGraphicsScene::on_node_deleted); + + node_graphics_objects[n_id] = n_o; + + addItem(n_o); + + return true; +} + +bool VisualShaderGraphicsScene::delete_node(const int& n_id) { + const std::shared_ptr n{vs->get_node(n_id)}; + + if (!n) { + return false; + } + + VisualShaderNodeGraphicsObject* n_o{this->get_node_graphics_object(n_id)}; + + if (!n_o) { + return false; + } + + // Remove all connections to the node + for (int i{0}; i < n->get_input_port_count(); i++) { + VisualShaderInputPortGraphicsObject* i_port{n_o->get_input_port_graphics_object(i)}; + + if (!i_port || !i_port->is_connected()) { + continue; + } + + // Get the output port of the connection + VisualShaderConnectionGraphicsObject* c_o{i_port->get_connection_graphics_object()}; + + if (!c_o) { + continue; + } + + bool result{this->delete_connection(c_o->get_from_node_id(), c_o->get_from_port_index(), n_id, i)}; + + if (!result) { + std::cout << "Failed to delete connection" << std::endl; + continue; + } + } + + for (int i{0}; i < n->get_output_port_count(); i++) { + VisualShaderOutputPortGraphicsObject* o_port{n_o->get_output_port_graphics_object(i)}; + + if (!o_port || !o_port->is_connected()) { + continue; + } + + std::vector c_os{o_port->get_connection_graphics_objects()}; + + for (VisualShaderConnectionGraphicsObject* c_o : c_os) { + if (!c_o) { + continue; + } + + bool result{this->delete_connection(n_id, i, c_o->get_to_node_id(), c_o->get_to_port_index())}; + + if (!result) { + std::cout << "Failed to delete connection" << std::endl; + continue; + } + } + } + + bool result{vs->remove_node(n_id)}; + + if (!result) { + return false; + } + + // Remove the node from the scene + // TODO: Why if we exchange the order of these two lines, the program crashes? + this->node_graphics_objects.erase(n_id); + remove_item(n_o); + + return true; +} + +void VisualShaderGraphicsScene::on_update_shader_previewer_widgets_requested() { + for (auto& [n_id, n_o] : node_graphics_objects) { + if (n_id == (int)VisualShader::NODE_ID_OUTPUT) { + continue; + } + + ShaderPreviewerWidget* spw{n_o->get_shader_previewer_widget()}; + if (!spw) { + continue; + } + + spw->set_code(vs->generate_preview_shader(n_id, 0)); // 0 is the output port index + } + + on_scene_update_requested(); +} + +void VisualShaderGraphicsScene::on_scene_update_requested() { update(); } + +void VisualShaderGraphicsScene::on_in_port_remove_requested(VisualShaderInputPortGraphicsObject* in_port) { + if (in_port->is_connected()) { + VisualShaderConnectionGraphicsObject* c_o{in_port->get_connection_graphics_object()}; + delete_connection(c_o->get_from_node_id(), c_o->get_from_port_index(), c_o->get_to_node_id(), + c_o->get_to_port_index()); + } + + remove_item(in_port); +} + +void VisualShaderGraphicsScene::on_out_port_remove_requested(VisualShaderOutputPortGraphicsObject* out_port) { + if (out_port->is_connected()) { + std::vector c_os{out_port->get_connection_graphics_objects()}; + for (VisualShaderConnectionGraphicsObject* c_o : c_os) { + delete_connection(c_o->get_from_node_id(), c_o->get_from_port_index(), c_o->get_to_node_id(), + c_o->get_to_port_index()); + } + } + + remove_item(out_port); +} + +bool VisualShaderGraphicsScene::add_connection(const int& from_node_id, const int& from_port_index, + const int& to_node_id, const int& to_port_index) { + QList views{this->views()}; + if (views.isEmpty()) { + std::cout << "No views available" << std::endl; + return false; + } + + // Create the connection and set its start + VisualShaderNodeGraphicsObject* from_n_o{this->get_node_graphics_object(from_node_id)}; + + if (!from_n_o) { + return false; + } + + VisualShaderOutputPortGraphicsObject* from_o_port{from_n_o->get_output_port_graphics_object(from_port_index)}; + + if (!from_o_port) { + return false; + } + + VisualShaderGraphicsView* view{dynamic_cast(views.first())}; + + if (from_o_port->get_global_coordinate().x() < view->get_x() || + from_o_port->get_global_coordinate().x() > view->get_x() + view->get_width()) { + std::cout << "Start of connection is out of view bounds" << std::endl; + } + + if (!this->temporary_connection_graphics_object) { + this->temporary_connection_graphics_object = + new VisualShaderConnectionGraphicsObject(from_node_id, from_port_index, from_o_port->get_global_coordinate()); + from_o_port->connect(this->temporary_connection_graphics_object); + addItem(this->temporary_connection_graphics_object); + return true; + } + + if (to_node_id != (int)VisualShader::NODE_ID_INVALID && to_port_index != (int)VisualShader::PORT_INDEX_INVALID) { + // Set the end of the connection + VisualShaderNodeGraphicsObject* to_n_o{this->get_node_graphics_object(to_node_id)}; + + if (!to_n_o) { + return false; + } + + VisualShaderInputPortGraphicsObject* to_i_port{to_n_o->get_input_port_graphics_object(to_port_index)}; + + if (!to_i_port) { + return false; + } + + if (to_i_port->get_global_coordinate().y() < view->get_y() || + to_i_port->get_global_coordinate().y() > view->get_y() + view->get_height()) { + std::cout << "End of connection is out of view bounds" << std::endl; + } + + // Connect the nodes in the VisualShader + bool result{vs->can_connect_nodes(from_node_id, from_port_index, to_node_id, to_port_index)}; + if (!result) { + std::cout << "Can't connect nodes" << std::endl; + return false; + } + + result = vs->connect_nodes(from_node_id, from_port_index, to_node_id, to_port_index); + if (!result) { + std::cout << "Failed to connect nodes" << std::endl; + return false; + } + + this->temporary_connection_graphics_object->set_end_coordinate(to_i_port->get_global_coordinate()); + to_i_port->connect(this->temporary_connection_graphics_object); + this->temporary_connection_graphics_object->set_to_node_id(to_node_id); + this->temporary_connection_graphics_object->set_to_port_index(to_port_index); + this->temporary_connection_graphics_object = nullptr; // Make sure to reset the temporary connection object + + on_update_shader_previewer_widgets_requested(); + + return true; + } + + return false; +} + +bool VisualShaderGraphicsScene::delete_connection(const int& from_node_id, const int& from_port_index, + const int& to_node_id, const int& to_port_index) { + VisualShaderNodeGraphicsObject* from_n_o{this->get_node_graphics_object(from_node_id)}; + + if (!from_n_o) { + return false; + } + + VisualShaderOutputPortGraphicsObject* from_o_port{from_n_o->get_output_port_graphics_object(from_port_index)}; + + if (!from_o_port) { + return false; + } + + if (this->temporary_connection_graphics_object) { + from_o_port->detach_connection(this->temporary_connection_graphics_object); + remove_item(this->temporary_connection_graphics_object); + this->temporary_connection_graphics_object = nullptr; + return true; + } + + // If we have a complete connection, then we can disconnect the nodes + if (to_node_id != (int)VisualShader::NODE_ID_INVALID && to_port_index != (int)VisualShader::PORT_INDEX_INVALID) { + VisualShaderConnectionGraphicsObject* c_o{from_o_port->get_connection_graphics_object(to_node_id, to_port_index)}; + + if (!c_o) { + return false; + } + + VisualShaderNodeGraphicsObject* to_n_o{this->get_node_graphics_object(to_node_id)}; + + if (!to_n_o) { + return false; + } + + VisualShaderInputPortGraphicsObject* to_i_port{to_n_o->get_input_port_graphics_object(to_port_index)}; + + if (!to_i_port) { + return false; + } + + bool result{vs->disconnect_nodes(from_node_id, from_port_index, to_node_id, to_port_index)}; + + if (!result) { + return false; + } + + to_i_port->detach_connection(); + from_o_port->detach_connection(c_o); + remove_item(c_o); + + on_update_shader_previewer_widgets_requested(); + + return true; + } + + return false; +} + +VisualShaderNodeGraphicsObject* VisualShaderGraphicsScene::get_node_graphics_object(const int& n_id) const { + VisualShaderNodeGraphicsObject* n_o{nullptr}; + + auto it{node_graphics_objects.find(n_id)}; + if (it != node_graphics_objects.end()) { + n_o = it->second; + } + + return n_o; +} + +void VisualShaderGraphicsScene::on_node_moved(const int& n_id, const QPointF& new_coordinate) { + const std::shared_ptr n{vs->get_node(n_id)}; + + if (!n) { + return; + } + + // Update the node's coordinate in the VisualShader + vs->set_node_coordinate(n_id, {(float)new_coordinate.x(), (float)new_coordinate.y()}); + + // Update coordinates of all connected connections + VisualShaderNodeGraphicsObject* n_o{this->get_node_graphics_object(n_id)}; + + for (int i{0}; i < n->get_input_port_count(); i++) { + VisualShaderInputPortGraphicsObject* i_port{n_o->get_input_port_graphics_object(i)}; + + if (!i_port || !i_port->is_connected()) { + continue; + } + + VisualShaderConnectionGraphicsObject* c_o{i_port->get_connection_graphics_object()}; + + if (!c_o) { + continue; + } + + c_o->set_end_coordinate(i_port->get_global_coordinate()); + } + + for (int i{0}; i < n->get_output_port_count(); i++) { + VisualShaderOutputPortGraphicsObject* o_port{n_o->get_output_port_graphics_object(i)}; + + if (!o_port || !o_port->is_connected()) { + continue; + } + + std::vector c_os{o_port->get_connection_graphics_objects()}; + + for (VisualShaderConnectionGraphicsObject* c_o : c_os) { + if (!c_o) { + continue; + } + + c_o->set_start_coordinate(o_port->get_global_coordinate()); + } + } +} + +void VisualShaderGraphicsScene::on_node_deleted(const int& n_id) { + if (n_id == (int)VisualShader::NODE_ID_OUTPUT) { + return; + } + + bool result{this->delete_node(n_id)}; + + if (!result) { + std::cout << "Failed to delete node" << std::endl; + } +} + +void VisualShaderGraphicsScene::on_port_pressed(QGraphicsObject* port, const QPointF& coordinate) { + this->temporary_connection_graphics_object = nullptr; // Reset the temporary connection object +} + +void VisualShaderGraphicsScene::on_port_dragged(QGraphicsObject* port, const QPointF& coordinate) { + VisualShaderConnectionGraphicsObject* c_o{nullptr}; + + VisualShaderOutputPortGraphicsObject* o_port{dynamic_cast(port)}; + + if (!o_port) { + VisualShaderInputPortGraphicsObject* i_port{dynamic_cast(port)}; + + if (!i_port) { + return; + } + + if (i_port->is_connected() && !temporary_connection_graphics_object) { + c_o = i_port->get_connection_graphics_object(); + temporary_connection_graphics_object = c_o; // Store the connection object for access in the next drag call + bool result{vs->disconnect_nodes(c_o->get_from_node_id(), c_o->get_from_port_index(), c_o->get_to_node_id(), + c_o->get_to_port_index())}; + if (!result) { + std::cout << "Failed to disconnect nodes" << std::endl; + } + i_port->detach_connection(); + c_o->detach_end(); + + on_update_shader_previewer_widgets_requested(); + } else if (!i_port->is_connected() && temporary_connection_graphics_object) { + c_o = temporary_connection_graphics_object; + on_scene_update_requested(); + } else { + return; + } + + c_o->set_end_coordinate(coordinate); + + return; + } + + if (o_port->is_connected() && temporary_connection_graphics_object) { + c_o = temporary_connection_graphics_object; + } else if (!temporary_connection_graphics_object) { + bool result{this->add_connection(o_port->get_node_id(), o_port->get_port_index())}; + if (!result) { + std::cout << "Failed to add connection" << std::endl; + return; + } + c_o = temporary_connection_graphics_object; + } else { + return; + } + + c_o->set_end_coordinate(coordinate); +} + +void VisualShaderGraphicsScene::on_port_dropped(QGraphicsObject* port, const QPointF& coordinate) { + if (!temporary_connection_graphics_object) { + return; + } + + // Find all items under the coordinate + QList items_at_coordinate{this->items(coordinate)}; + + // Iterate through the items and check if an input port is under the mouse + VisualShaderInputPortGraphicsObject* in_p_o{nullptr}; + for (QGraphicsItem* item : items_at_coordinate) { + // Check if the item is an input port + in_p_o = dynamic_cast(item); + + if (in_p_o) { + break; + } + } + + VisualShaderOutputPortGraphicsObject* o_port{dynamic_cast(port)}; + + if (!o_port) { + VisualShaderInputPortGraphicsObject* i_port{dynamic_cast(port)}; + + if (!i_port) { + return; + } + + if (!in_p_o) { + bool result{this->delete_connection(temporary_connection_graphics_object->get_from_node_id(), + temporary_connection_graphics_object->get_from_port_index())}; + + if (!result) { + std::cout << "Failed to delete connection" << std::endl; + } + + return; // Return because we dragging an input port and dropped on nothing + } + + bool result{add_connection(temporary_connection_graphics_object->get_from_node_id(), + temporary_connection_graphics_object->get_from_port_index(), in_p_o->get_node_id(), + in_p_o->get_port_index())}; + + if (!result) { + bool result{this->delete_connection(temporary_connection_graphics_object->get_from_node_id(), + temporary_connection_graphics_object->get_from_port_index())}; + + if (!result) { + std::cout << "Failed to delete connection" << std::endl; + } + + return; // Return because we dragging an input port and dropped on nothing + } + + return; + } + + if (!in_p_o) { + bool result{this->delete_connection(temporary_connection_graphics_object->get_from_node_id(), + temporary_connection_graphics_object->get_from_port_index())}; + + if (!result) { + std::cout << "Failed to delete connection" << std::endl; + } + + return; // Return because we dragging an output port and dropped on nothing + } + + bool result{ + add_connection(o_port->get_node_id(), o_port->get_port_index(), in_p_o->get_node_id(), in_p_o->get_port_index())}; + + if (!result) { + bool result{this->delete_connection(temporary_connection_graphics_object->get_from_node_id(), + temporary_connection_graphics_object->get_from_port_index())}; + + if (!result) { + std::cout << "Failed to delete connection" << std::endl; + } + + return; + } +} + +void VisualShaderGraphicsScene::remove_item(QGraphicsItem* item) { + removeItem(item); + delete item; +} + +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ +/***** *****/ +/***** VisualShaderGraphicsView *****/ +/***** *****/ +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ + +////////////////////////////// +// Public functions +////////////////////////////// + +VisualShaderGraphicsView::VisualShaderGraphicsView(VisualShaderGraphicsScene* scene, QWidget* parent) + : QGraphicsView(scene, parent), + scene(scene), + context_menu(nullptr), + create_node_action(nullptr), + zoom_in_action(nullptr), + reset_zoom_action(nullptr), + zoom_out_action(nullptr) { + setDragMode(QGraphicsView::ScrollHandDrag); + setRenderHint(QPainter::Antialiasing); + + setBackgroundBrush(this->background_color); + + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + setTransformationAnchor(QGraphicsView::AnchorUnderMouse); + + setCacheMode(QGraphicsView::CacheBackground); + setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate); + + // Allow dezooming 8 times from the default zoom level. + zoom_min = (1.0f / std::pow(zoom_step, 8.0f)); + // Allow zooming 4 times from the default zoom level. + zoom_max = (1.0f * std::pow(zoom_step, 4.0f)); + + setSceneRect(rect_x, rect_y, rect_width, rect_height); + + reset_zoom(); + + // Set the context menu + context_menu = new QMenu(this); + create_node_action = new QAction(QStringLiteral("Create Node"), context_menu); + QObject::connect(create_node_action, &QAction::triggered, this, + &VisualShaderGraphicsView::on_create_node_action_triggered); + context_menu->addAction(create_node_action); + + zoom_in_action = new QAction(QStringLiteral("Zoom In"), context_menu); + zoom_in_action->setShortcutContext(Qt::ShortcutContext::WidgetShortcut); + zoom_in_action->setShortcut(QKeySequence(QKeySequence::ZoomIn)); + QObject::connect(zoom_in_action, &QAction::triggered, this, &VisualShaderGraphicsView::zoom_in); + context_menu->addAction(zoom_in_action); + + reset_zoom_action = new QAction(QStringLiteral("Reset Zoom"), context_menu); + QObject::connect(reset_zoom_action, &QAction::triggered, this, &VisualShaderGraphicsView::reset_zoom); + context_menu->addAction(reset_zoom_action); + + zoom_out_action = new QAction(QStringLiteral("Zoom Out"), context_menu); + zoom_out_action->setShortcutContext(Qt::ShortcutContext::WidgetShortcut); + zoom_out_action->setShortcut(QKeySequence(QKeySequence::ZoomOut)); + QObject::connect(zoom_out_action, &QAction::triggered, this, &VisualShaderGraphicsView::zoom_out); + context_menu->addAction(zoom_out_action); +} + +VisualShaderGraphicsView::~VisualShaderGraphicsView() {} + +////////////////////////////// +// Private slots +////////////////////////////// + +void VisualShaderGraphicsView::on_create_node_action_triggered() { + VisualShaderEditor* editor{scene->get_editor()}; + + Q_EMIT editor->create_node_dialog_requested(this->last_context_menu_coordinate); +} + +void VisualShaderGraphicsView::zoom_in() { + const float factor{std::pow(zoom_step, zoom)}; + + QTransform t{transform()}; + t.scale(factor, factor); + if (t.m11() >= zoom_max) { + return; + } + + scale(factor, factor); + Q_EMIT zoom_changed(transform().m11()); +} + +void VisualShaderGraphicsView::reset_zoom() { + if ((float)transform().m11() == zoom) { + return; + } + + resetTransform(); // Reset the zoom level to 1.0f + Q_EMIT zoom_changed(transform().m11()); +} + +void VisualShaderGraphicsView::zoom_out() { + const float factor{std::pow(zoom_step, -1.0f * zoom)}; + + QTransform t{transform()}; + t.scale(factor, factor); + if (t.m11() <= zoom_min) { + return; + } + + scale(factor, factor); + Q_EMIT zoom_changed(transform().m11()); +} + +////////////////////////////// +// Private functions +////////////////////////////// + +void VisualShaderGraphicsView::drawBackground(QPainter* painter, const QRectF& r) { + QGraphicsView::drawBackground(painter, r); + + std::function draw_grid = [&](float grid_step) { + QRect window_rect{this->rect()}; + + QPointF tl{mapToScene(window_rect.topLeft())}; + QPointF br{mapToScene(window_rect.bottomRight())}; + + float left{std::floor((float)tl.x() / grid_step)}; + float right{std::ceil((float)br.x() / grid_step)}; + float bottom{std::floor((float)tl.y() / grid_step)}; + float top{std::ceil((float)br.y() / grid_step)}; + + // Vertical lines + for (int xi{(int)left}; xi <= (int)right; ++xi) { + QLineF line(xi * grid_step, bottom * grid_step, xi * grid_step, top * grid_step); + painter->drawLine(line); + } + + // Horizontal lines + for (int yi{(int)bottom}; yi <= (int)top; ++yi) { + QLineF line(left * grid_step, yi * grid_step, right * grid_step, yi * grid_step); + painter->drawLine(line); + } + }; + + QPen fine_pen(this->fine_grid_color, 1.0f); + painter->setPen(fine_pen); + draw_grid(15.0f); + + QPen coarse_pen(this->coarse_grid_color, 1.0f); + painter->setPen(coarse_pen); + draw_grid(150.0f); +} + +void VisualShaderGraphicsView::contextMenuEvent(QContextMenuEvent* event) { + QGraphicsItem* item{itemAt(event->pos())}; + + // If there is an item and this item is a node object, pass the event to the + // children + if (item && dynamic_cast(item)) { + QGraphicsView::contextMenuEvent(event); + return; + } else if (item) { + return; + } + + this->last_context_menu_coordinate = mapToScene(event->pos()); + + context_menu->exec(event->globalPos()); +} + +void VisualShaderGraphicsView::wheelEvent(QWheelEvent* event) { + float t_zoom{(float)transform().m11()}; + + const QPoint delta{event->angleDelta()}; + + if (delta.y() == 0) { + event->ignore(); + return; + } + + if (delta.y() > 0 && (std::abs(t_zoom - zoom_max) > std::numeric_limits::epsilon())) { + zoom_in(); + } else if (delta.y() < 0 && (std::abs(t_zoom - zoom_min) > std::numeric_limits::epsilon())) { + zoom_out(); + } else { + event->ignore(); + } +} + +void VisualShaderGraphicsView::mousePressEvent(QMouseEvent* event) { + QGraphicsView::mousePressEvent(event); + + switch (event->button()) { + case Qt::LeftButton: + last_click_coordinate = mapToScene(event->pos()); + break; + default: + break; + } +} + +void VisualShaderGraphicsView::mouseMoveEvent(QMouseEvent* event) { + QGraphicsView::mouseMoveEvent(event); + + switch (event->buttons()) { + case Qt::LeftButton: { + QPointF current_coordinate{mapToScene(event->pos())}; + QPointF difference{last_click_coordinate - current_coordinate}; + setSceneRect(sceneRect().translated(difference.x(), difference.y())); + last_click_coordinate = current_coordinate; + } break; + default: + break; + } +} + +void VisualShaderGraphicsView::mouseReleaseEvent(QMouseEvent* event) { QGraphicsView::mouseReleaseEvent(event); } + +void VisualShaderGraphicsView::showEvent(QShowEvent* event) { + QGraphicsView::showEvent(event); + + move_view_to_fit_items(); +} + +void VisualShaderGraphicsView::move_view_to_fit_items() { + if (!scene) { + return; + } + + if (scene->items().isEmpty()) { + return; + } + + std::cout << "Changing view port to fit items..." << std::endl; + + QRectF items_bounding_rect{scene->itemsBoundingRect()}; + items_bounding_rect.adjust(-fit_in_view_margin, -fit_in_view_margin, fit_in_view_margin, fit_in_view_margin); + + QPointF scene_tl{scene->sceneRect().topLeft()}; + QPointF scene_br{scene->sceneRect().bottomRight()}; + QPointF items_tl{items_bounding_rect.topLeft()}; + QPointF items_br{items_bounding_rect.bottomRight()}; + + // Make sure the items bounding rect is inside the scene rect + if (items_tl.x() < scene_tl.x()) { + items_bounding_rect.setLeft(scene_tl.x()); + } + + if (items_tl.y() > scene_tl.y()) { + items_bounding_rect.setTop(scene_tl.y()); + } + + if (items_br.x() > scene_br.x()) { + items_bounding_rect.setRight(scene_br.x()); + } + + if (items_br.y() < scene_br.y()) { + items_bounding_rect.setBottom(scene_br.y()); + } + + fitInView(items_bounding_rect, Qt::KeepAspectRatio); + + centerOn(items_bounding_rect.center()); + + if ((float)transform().m11() > zoom_max) { + std::cout << "Current zoom level is greater than the maximum zoom level." << std::endl; + std::cout << "Maybe due to having a very large distance between the nodes." << std::endl; + } + + if ((float)transform().m11() < zoom_min) { + std::cout << "Current zoom level is less than the minimum zoom level." << std::endl; + std::cout << "Maybe due to having all the nodes outside the scene bounds." << std::endl; + } +} + +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ +/***** *****/ +/***** VisualShaderNodeGraphicsObject *****/ +/***** *****/ +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ + +VisualShaderNodeGraphicsObject::VisualShaderNodeGraphicsObject(const int& n_id, const QPointF& coordinate, + const std::shared_ptr& node, + QGraphicsItem* parent) + : QGraphicsObject(parent), + n_id(n_id), + coordinate(coordinate), + node(node), + context_menu(nullptr), + delete_node_action(nullptr), + rect_width(0.0f), + caption_rect_height(0.0f), + rect_height(0.0f), + rect_margin(0.0f), + rect_padding(0.0f), + embed_widget(nullptr), + matching_image_widget(nullptr), + shader_previewer_widget(nullptr) { + setFlag(QGraphicsItem::ItemDoesntPropagateOpacityToChildren, true); + setFlag(QGraphicsItem::ItemIsFocusable, true); + setFlag(QGraphicsItem::ItemIsMovable, true); + setFlag(QGraphicsItem::ItemIsSelectable, true); + setFlag(QGraphicsItem::ItemSendsScenePositionChanges, true); + + setCacheMode(QGraphicsItem::DeviceCoordinateCache); + + setVisible(true); + setOpacity(this->opacity); + + setZValue(0); + + setPos(coordinate.x(), coordinate.y()); + + // Output node should have a matching image widget + if (n_id == (int)VisualShader::NODE_ID_OUTPUT) { + QGraphicsProxyWidget* matching_image_widget_proxy{new QGraphicsProxyWidget(this)}; + matching_image_widget = new OriginalMatchingImageWidget(); + matching_image_widget_proxy->setWidget(matching_image_widget); + } else { + // Create the shader previewer widget + QGraphicsProxyWidget* shader_previewer_widget_proxy{new QGraphicsProxyWidget(this)}; + shader_previewer_widget = new ShaderPreviewerWidget(); + shader_previewer_widget->setVisible(false); + shader_previewer_widget_proxy->setWidget(shader_previewer_widget); + } + + // Set the context menu + context_menu = new QMenu(); + delete_node_action = new QAction(QStringLiteral("Delete Node"), context_menu); + delete_node_action->setShortcutContext(Qt::ShortcutContext::WidgetShortcut); + delete_node_action->setShortcut(QKeySequence(QKeySequence::Delete)); + QObject::connect(delete_node_action, &QAction::triggered, this, + &VisualShaderNodeGraphicsObject::on_delete_node_action_triggered); + context_menu->addAction(delete_node_action); +} + +VisualShaderNodeGraphicsObject::~VisualShaderNodeGraphicsObject() { + if (context_menu) delete context_menu; +} + +void VisualShaderNodeGraphicsObject::on_delete_node_action_triggered() { Q_EMIT node_deleted(n_id); } + +VisualShaderInputPortGraphicsObject* VisualShaderNodeGraphicsObject::get_input_port_graphics_object( + const int& p_index) const { + if (in_port_graphics_objects.find(p_index) != in_port_graphics_objects.end()) { + return in_port_graphics_objects.at(p_index); + } + + return nullptr; +} + +VisualShaderOutputPortGraphicsObject* VisualShaderNodeGraphicsObject::get_output_port_graphics_object( + const int& p_index) const { + if (out_port_graphics_objects.find(p_index) != out_port_graphics_objects.end()) { + return out_port_graphics_objects.at(p_index); + } + + return nullptr; +} + +void VisualShaderNodeGraphicsObject::on_node_update_requested() { + // Here if the number of ports changed, for example, changing the operation type in a decompose node, + // we need to remove any extra ports that are not needed anymore. Don't forget to remove the connections + // as well. + if (in_port_graphics_objects.size() > node->get_input_port_count()) { + int p_index{node->get_input_port_count()}; + int size{(int)in_port_graphics_objects.size() - p_index}; + for (int i{0}; i < size; ++i) { + Q_EMIT in_port_remove_requested(in_port_graphics_objects.at(p_index)); + in_port_graphics_objects.erase(p_index); + p_index++; + } + } + + if (out_port_graphics_objects.size() > node->get_output_port_count()) { + int p_index{node->get_output_port_count()}; + int size{(int)out_port_graphics_objects.size() - p_index}; + for (int i{0}; i < size; ++i) { + Q_EMIT out_port_remove_requested(out_port_graphics_objects.at(p_index)); + out_port_graphics_objects.erase(p_index); + p_index++; + } + } + + update(); + Q_EMIT scene_update_requested(); +} + +QRectF VisualShaderNodeGraphicsObject::boundingRect() const { + QFont f("Arial", caption_font_size); + f.setBold(true); + QFontMetrics fm(f); + + QString caption{QString::fromStdString(node->get_caption())}; + + rect_width = (float)(fm.horizontalAdvance(caption, caption.length()) + caption_h_padding * 2.0f); + + caption_rect_height = (float)((fm.height()) + caption_v_padding * 2.0f); + + int max_num_ports{qMax(node->get_input_port_count(), node->get_output_port_count())}; + + // Calculate the height of the node + float t_rect_h{caption_rect_height}; + + t_rect_h += body_rect_header_height; // Header + if (max_num_ports >= 0) { + t_rect_h += (float)(max_num_ports - 1) * body_rect_port_step; // Ports + } + t_rect_h += body_rect_footer_height; // Footer + + rect_height = t_rect_h; + + // Correct node rect if it has an embed widget (if needed). + if (embed_widget) { + float embed_widget_width{(float)embed_widget->width()}; + + // Find biggest horizontal length of input port names + int in_p_count{node->get_input_port_count()}; + float max_in_p_width{0.0f}; + for (unsigned i{0}; i < in_p_count; ++i) { + QString p_n{QString::fromStdString(node->get_input_port_name(i))}; + + QFont f("Arial", port_caption_font_size); + QFontMetrics fm(f); + + float w{0.0f}; + + if (!p_n.isEmpty()) { + w = (float)fm.horizontalAdvance(p_n); + } else { + // If the port name is empty, use a default name + // This is because the horizontal advance of an empty string is 0 and + // this will wrong the calculation of the rect width + w = (float)fm.horizontalAdvance("Input"); + } + + if ((w + port_caption_spacing) > max_in_p_width) { + max_in_p_width = w + port_caption_spacing; + } + } + + // Find biggest horizontal length of output port names + int out_p_count{node->get_output_port_count()}; + float max_out_p_width{0.0f}; + for (unsigned i{0}; i < out_p_count; ++i) { + QString p_n{QString::fromStdString(node->get_output_port_name(i))}; + + QFont f("Arial", port_caption_font_size); + QFontMetrics fm(f); + + float w{0.0f}; + + if (!p_n.isEmpty()) { + w = (float)fm.horizontalAdvance(p_n); + } else { + // If the port name is empty, use a default name + // This is because the horizontal advance of an empty string is 0 and + // this will wrong the calculation of the rect width + w = (float)fm.horizontalAdvance("Output"); + } + + if ((w + port_caption_spacing) > max_out_p_width) { + max_out_p_width = w + port_caption_spacing; + } + } + + float calculated_rect{max_in_p_width + embed_widget_width + max_out_p_width + embed_widget_h_padding * 2.0f}; + + if (calculated_rect > rect_width) { + rect_width = calculated_rect; + } + + // Check the height + float calculated_height{caption_rect_height + body_rect_header_height + embed_widget->height() + + body_rect_footer_height + embed_widget_v_padding * 2.0f}; + + if (calculated_height > rect_height) { + rect_height = calculated_height; + } + } + + QRectF r({0.0f, 0.0f}, QSizeF(rect_width, rect_height)); + + // Calculate the margin + this->rect_margin = port_diameter * 0.5f; + + // Calculate the rect padding + // We add a safe area around the rect to prevent the ports from being cut off + this->rect_padding = port_diameter * 0.5f; + + r.adjust(-rect_margin - rect_padding, -rect_margin - rect_padding, rect_margin + rect_padding, + rect_margin + rect_padding); + + return r; +} + +void VisualShaderNodeGraphicsObject::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) { + painter->setClipRect(option->exposedRect); + + // Get the rect without the padding + QRectF r{this->boundingRect()}; + + // { + // // Draw Node Rect + // painter->setPen(Qt::red); + // painter->setBrush(Qt::NoBrush); + // painter->drawRect(r); + // } + + // Add the padding to the rect + r.adjust(rect_padding, rect_padding, -rect_padding, -rect_padding); + + { + // Draw Node Rect + QColor rect_color; + if (isSelected()) { + rect_color = this->normal_boundary_color; + } else { + rect_color = this->selected_boundary_color; + } + + QPen p(rect_color, this->pen_width); + painter->setPen(p); + + painter->setBrush(this->fill_color); + + painter->drawRoundedRect(r, this->corner_radius, this->corner_radius); + } + + // Draw Matching Image Widget + if (n_id == (int)VisualShader::NODE_ID_OUTPUT) { + float matching_image_widget_x{(float)r.x() + (float)r.width() + spacing_between_output_node_and_matching_image}; + float matching_image_widget_y{(float)r.y()}; + + matching_image_widget->setGeometry(matching_image_widget_x, matching_image_widget_y, r.height(), r.height()); + } else { + // Draw Shader Previewer Widget + float shader_previewer_widget_x{(float)r.x()}; + float shader_previewer_widget_y{(float)r.y() + (float)r.height() + + spacing_between_current_node_and_shader_previewer}; + shader_previewer_widget->setGeometry(shader_previewer_widget_x, shader_previewer_widget_y, r.width(), r.width()); + } + + // Add the margin to the rect + r.adjust(rect_margin, rect_margin, -rect_margin, -rect_margin); + + // { + // // Draw Node Rect + // painter->setPen(Qt::red); + // painter->setBrush(Qt::NoBrush); + // painter->drawRect(r); + // } + + float rect_x{(float)r.topLeft().x()}; + float rect_y{(float)r.topLeft().y()}; + + float rect_w{(float)r.width()}; + float rect_h{(float)r.height()}; + + QRectF caption_rect(rect_x, rect_y, rect_w, caption_rect_height); + + { + // Draw Caption + QString caption{QString::fromStdString(node->get_caption())}; + + QFont t_f{painter->font()}; + + QFont f("Arial", caption_font_size); + f.setBold(true); + QFontMetrics fm(f); + painter->setFont(f); + + // Calculate the coordinates of the caption + float x{(float)(caption_rect.center().x() - (float)fm.horizontalAdvance(caption) * 0.5f)}; + + // Instead of subtracting, add the ascent to properly align text within the rect + float y{(float)(caption_rect.center().y() + (float)((fm.ascent() + fm.descent()) * 0.5f - fm.descent()))}; + + // { + // painter->setPen(Qt::red); + // painter->setBrush(Qt::NoBrush); + // painter->drawRect(QRectF(x,y, (float)fm.horizontalAdvance(caption), (float)(fm.ascent() + fm.descent()))); + // } + + QPointF coordinate{x, y}; + + painter->setPen(this->font_color); + painter->drawText(coordinate, caption); + + painter->setFont(t_f); // Reset the font + } + + QPointF caption_rect_bl{caption_rect.bottomLeft()}; + QPointF first_in_port_coordinate{caption_rect_bl.x(), caption_rect_bl.y() + body_rect_header_height}; + + // Correct X coordinate: Remove the margin + first_in_port_coordinate.setX((float)first_in_port_coordinate.x() - this->rect_margin); + + { + // Draw Input Ports + int in_port_count{node->get_input_port_count()}; + + for (unsigned i{0}; i < in_port_count; ++i) { + QPointF port_coordinate{first_in_port_coordinate.x(), first_in_port_coordinate.y() + body_rect_port_step * i}; + + QRectF port_rect(port_coordinate.x(), port_coordinate.y(), port_diameter, port_diameter); + + // Adjust the port rect to be centered + port_rect.adjust(-port_rect.width() * 0.5f, -port_rect.height() * 0.5f, -port_rect.width() * 0.5f, + -port_rect.height() * 0.5f); + + // Draw caption + QString p_n{QString::fromStdString(node->get_input_port_name(i))}; + + if (!p_n.isEmpty()) { + QFont t_f{painter->font()}; + + QFont f("Arial", port_caption_font_size); + QFontMetrics fm(f); + painter->setFont(f); + + float x{rect_x + port_caption_spacing}; + + float y{(float)(port_rect.center().y()) + (float)((fm.ascent() + fm.descent()) * 0.5f - fm.descent())}; + + // { + // painter->setPen(Qt::red); + // painter->setBrush(Qt::NoBrush); + // painter->drawRect(QRectF(x,y, (float)fm.horizontalAdvance(p_n), (float)(fm.ascent() + fm.descent()))); + // } + + QPointF coordinate{x, y}; + + painter->setPen(this->font_color); + painter->drawText(coordinate, p_n); + + painter->setFont(t_f); // Reset the font + } + + if (in_port_graphics_objects.find(i) != in_port_graphics_objects.end()) continue; + + // Draw the port + VisualShaderInputPortGraphicsObject* p_o{new VisualShaderInputPortGraphicsObject(port_rect, n_id, i, this)}; + in_port_graphics_objects[i] = p_o; + + // Connect the signals + QObject::connect(p_o, &VisualShaderInputPortGraphicsObject::port_pressed, this, + &VisualShaderNodeGraphicsObject::on_in_port_pressed); + QObject::connect(p_o, &VisualShaderInputPortGraphicsObject::port_dragged, this, + &VisualShaderNodeGraphicsObject::on_in_port_dragged); + QObject::connect(p_o, &VisualShaderInputPortGraphicsObject::port_dropped, this, + &VisualShaderNodeGraphicsObject::on_in_port_dropped); + } + } + + QPointF caption_rect_br{caption_rect.bottomRight()}; + QPointF first_out_port_coordinate{caption_rect_br.x(), caption_rect_br.y() + body_rect_header_height}; + + // Correct X coordinate: Remove the margin + first_out_port_coordinate.setX((float)first_out_port_coordinate.x() + this->rect_margin); + + { + // Draw Output Ports + int out_port_count{node->get_output_port_count()}; + + for (unsigned i{0}; i < out_port_count; ++i) { + QPointF port_coordinate{first_out_port_coordinate.x(), first_out_port_coordinate.y() + body_rect_port_step * i}; + + QRectF port_rect(port_coordinate.x(), port_coordinate.y(), port_diameter, port_diameter); + + // Adjust the port rect to be centered + port_rect.adjust(-port_rect.width() * 0.5f, -port_rect.height() * 0.5f, -port_rect.width() * 0.5f, + -port_rect.height() * 0.5f); + + // Draw caption + QString p_n{QString::fromStdString(node->get_output_port_name(i))}; + + if (!p_n.isEmpty()) { + QFont t_f{painter->font()}; + + QFont f("Arial", port_caption_font_size); + QFontMetrics fm(f); + painter->setFont(f); + + float x{rect_x + rect_w - (float)fm.horizontalAdvance(p_n) - port_caption_spacing}; + + float y{(float)(port_rect.center().y()) + (float)((fm.ascent() + fm.descent()) * 0.5f - fm.descent())}; + + // { + // painter->setPen(Qt::red); + // painter->setBrush(Qt::NoBrush); + // painter->drawRect(QRectF(x,y, (float)fm.horizontalAdvance(p_n), (float)(fm.ascent() + fm.descent()))); + // } + + QPointF coordinate{x, y}; + + painter->setPen(this->font_color); + painter->drawText(coordinate, p_n); + + painter->setFont(t_f); // Reset the font + } + + if (out_port_graphics_objects.find(i) != out_port_graphics_objects.end()) continue; + + // Draw the port + VisualShaderOutputPortGraphicsObject* p_o{new VisualShaderOutputPortGraphicsObject(port_rect, n_id, i, this)}; + out_port_graphics_objects[i] = p_o; + + // Connect the signals + QObject::connect(p_o, &VisualShaderOutputPortGraphicsObject::port_pressed, this, + &VisualShaderNodeGraphicsObject::on_out_port_pressed); + QObject::connect(p_o, &VisualShaderOutputPortGraphicsObject::port_dragged, this, + &VisualShaderNodeGraphicsObject::on_out_port_dragged); + QObject::connect(p_o, &VisualShaderOutputPortGraphicsObject::port_dropped, this, + &VisualShaderNodeGraphicsObject::on_out_port_dropped); + } + } + + { + // Correct the coordinate of the embed widget + if (embed_widget) { + float embed_widget_x{rect_x + rect_w * 0.5f - embed_widget->width() * 0.5f}; + float embed_widget_y{rect_y + caption_rect_height + body_rect_header_height + embed_widget_v_padding}; + + embed_widget->setGeometry(embed_widget_x, embed_widget_y, embed_widget->width(), embed_widget->height()); + + // { + // // Draw Embed Widget + // painter->setPen(Qt::red); + // painter->setBrush(Qt::NoBrush); + // painter->drawRect(embed_widget_x, embed_widget_y, embed_widget->width(), embed_widget->height()); + // } + } + } +} + +QVariant VisualShaderNodeGraphicsObject::itemChange(GraphicsItemChange change, const QVariant& value) { + if (change == ItemScenePositionHasChanged) { + Q_EMIT node_moved(n_id, scenePos()); + } + + return QGraphicsObject::itemChange(change, value); +} + +void VisualShaderNodeGraphicsObject::contextMenuEvent(QGraphicsSceneContextMenuEvent* event) { + context_menu->exec(event->screenPos()); +} + +VisualShaderInputPortGraphicsObject::VisualShaderInputPortGraphicsObject(const QRectF& rect, const int& n_id, + const int& p_index, QGraphicsItem* parent) + : QGraphicsObject(parent), rect(rect), n_id(n_id), p_index(p_index), connection_graphics_object(nullptr) { + setFlag(QGraphicsItem::ItemDoesntPropagateOpacityToChildren, true); + setFlag(QGraphicsItem::ItemIsFocusable, true); + setFlag(QGraphicsItem::ItemIsSelectable, true); + + setCursor(Qt::PointingHandCursor); + + setCacheMode(QGraphicsItem::DeviceCoordinateCache); + + setVisible(true); + setOpacity(this->opacity); + + setZValue(0); +} + +VisualShaderInputPortGraphicsObject::~VisualShaderInputPortGraphicsObject() {} + +QRectF VisualShaderInputPortGraphicsObject::boundingRect() const { + QRectF t_rect{rect}; + t_rect.adjust(-padding, -padding, padding, padding); + return t_rect; +} + +void VisualShaderInputPortGraphicsObject::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, + QWidget* widget) { + painter->setClipRect(option->exposedRect); + + painter->setBrush(this->connection_point_color); + + painter->drawEllipse(rect); +} + +void VisualShaderInputPortGraphicsObject::mousePressEvent(QGraphicsSceneMouseEvent* event) { + Q_EMIT port_pressed(this, event->scenePos()); + QGraphicsObject::mousePressEvent(event); +} + +void VisualShaderInputPortGraphicsObject::mouseMoveEvent(QGraphicsSceneMouseEvent* event) { + Q_EMIT port_dragged(this, event->scenePos()); + QGraphicsObject::mouseMoveEvent(event); +} + +void VisualShaderInputPortGraphicsObject::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) { + Q_EMIT port_dropped(this, event->scenePos()); + QGraphicsObject::mouseReleaseEvent(event); +} + +VisualShaderOutputPortGraphicsObject::VisualShaderOutputPortGraphicsObject(const QRectF& rect, const int& n_id, + const int& p_index, QGraphicsItem* parent) + : QGraphicsObject(parent), rect(rect), n_id(n_id), p_index(p_index) { + setFlag(QGraphicsItem::ItemDoesntPropagateOpacityToChildren, true); + setFlag(QGraphicsItem::ItemIsFocusable, true); + setFlag(QGraphicsItem::ItemIsSelectable, true); + + setCursor(Qt::PointingHandCursor); + + setCacheMode(QGraphicsItem::DeviceCoordinateCache); + + setVisible(true); + setOpacity(this->opacity); + + setZValue(0); +} + +VisualShaderOutputPortGraphicsObject::~VisualShaderOutputPortGraphicsObject() {} + +VisualShaderConnectionGraphicsObject* VisualShaderOutputPortGraphicsObject::get_connection_graphics_object( + const int& to_node_id, const int& to_port_index) const { + for (auto c_g_o : connection_graphics_objects) { + if (c_g_o->get_to_node_id() == to_node_id && c_g_o->get_to_port_index() == to_port_index) { + return c_g_o; + } + } + return nullptr; +} + +QRectF VisualShaderOutputPortGraphicsObject::boundingRect() const { + QRectF t_rect{rect}; + t_rect.adjust(-padding, -padding, padding, padding); + return t_rect; +} + +void VisualShaderOutputPortGraphicsObject::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, + QWidget* widget) { + painter->setClipRect(option->exposedRect); + + painter->setBrush(this->connection_point_color); + + painter->drawEllipse(rect); +} + +void VisualShaderOutputPortGraphicsObject::mousePressEvent(QGraphicsSceneMouseEvent* event) { + Q_EMIT port_pressed(this, event->scenePos()); + QGraphicsObject::mousePressEvent(event); +} + +void VisualShaderOutputPortGraphicsObject::mouseMoveEvent(QGraphicsSceneMouseEvent* event) { + Q_EMIT port_dragged(this, event->scenePos()); + QGraphicsObject::mouseMoveEvent(event); +} + +void VisualShaderOutputPortGraphicsObject::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) { + Q_EMIT port_dropped(this, event->scenePos()); + QGraphicsObject::mouseReleaseEvent(event); +} + +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ +/***** *****/ +/***** VisualShaderConnectionGraphicsObject *****/ +/***** *****/ +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ + +VisualShaderConnectionGraphicsObject::VisualShaderConnectionGraphicsObject(const int& from_n_id, + const int& from_p_index, + const QPointF& start_coordinate, + QGraphicsItem* parent) + : QGraphicsObject(parent), + from_n_id(from_n_id), + from_p_index(from_p_index), + to_n_id((int)VisualShader::NODE_ID_INVALID), + to_p_index((int)VisualShader::PORT_INDEX_INVALID), + start_coordinate(start_coordinate), + end_coordinate(start_coordinate), + rect_padding(0.0f) { + setFlag(QGraphicsItem::ItemIsFocusable, true); + setFlag(QGraphicsItem::ItemIsSelectable, true); + setAcceptedMouseButtons(Qt::NoButton); + + setZValue(-1.0f); +} + +VisualShaderConnectionGraphicsObject::~VisualShaderConnectionGraphicsObject() {} + +QRectF VisualShaderConnectionGraphicsObject::boundingRect() const { + QRectF r{calculate_bounding_rect_from_coordinates(start_coordinate, end_coordinate)}; + + // Calculate the rect padding + // We add a safe area around the rect to prevent the ports from being cut off + // Due to inaccuracy in the calculation of the bounding rect we use the point diameter not the radius + this->rect_padding = this->point_diameter; + + r.adjust(-rect_padding, -rect_padding, rect_padding, rect_padding); + + return r; +} + +void VisualShaderConnectionGraphicsObject::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, + QWidget* widget) { + painter->setClipRect(option->exposedRect); + + { + // Draw the connection + QPen p; + p.setWidth(this->line_width); + + const bool selected{this->isSelected()}; + + std::pair control_points{calculate_control_points(start_coordinate, end_coordinate)}; + + QPainterPath cubic(start_coordinate); + cubic.cubicTo(control_points.first, control_points.second, end_coordinate); + + p.setColor(this->normal_color); + + if (selected) { + p.setColor(this->selected_color); + } + + painter->setPen(p); + painter->setBrush(Qt::NoBrush); + + painter->drawPath(cubic); + } + + painter->setBrush(this->connection_point_color); + + { + // Draw start point + QRectF start_rect(start_coordinate.x(), start_coordinate.y(), this->point_diameter, this->point_diameter); + + // Adjust the port rect to be centered + start_rect.adjust(-start_rect.width() * 0.5f, -start_rect.height() * 0.5f, -start_rect.width() * 0.5f, + -start_rect.height() * 0.5f); + + painter->drawEllipse(start_rect); + } + + { + // Draw end point + QRectF end_rect(end_coordinate.x(), end_coordinate.y(), this->point_diameter, this->point_diameter); + + // Adjust the port rect to be centered + end_rect.adjust(-end_rect.width() * 0.5f, -end_rect.height() * 0.5f, -end_rect.width() * 0.5f, + -end_rect.height() * 0.5f); + + painter->drawEllipse(end_rect); + } +} + +int VisualShaderConnectionGraphicsObject::detect_quadrant(const QPointF& reference, const QPointF& target) const { + float relative_x{(float)(target.x() - reference.x())}; + float relative_y{(float)(target.y() - reference.y())}; + + // Note that the default coordinate system in Qt is as follows: + // - X-axis: Positive to the right, negative to the left + // - Y-axis: Positive downwards, negative upwards + + // Check if the point is on an axis or the origin + if (relative_x == 0 && relative_y == 0) { + return 0; // Stack on the reference + } else if (relative_y == 0) { + return (relative_x > 0) ? 5 : 6; // On X-axis: 5 is the +ve part while 6 is the -ve one. + } else if (relative_x == 0) { + return (relative_y < 0) ? 7 : 8; // On Y-axis: 7 is the +ve part while 8 is the -ve one. + } + + // Determine the quadrant based on the relative coordinates + if (relative_x > 0 && relative_y < 0) { + return 1; // Quadrant I + } else if (relative_x < 0 && relative_y < 0) { + return 2; // Quadrant II + } else if (relative_x < 0 && relative_y > 0) { + return 3; // Quadrant III + } else if (relative_x > 0 && relative_y > 0) { + return 4; // Quadrant IV + } + + // Default case (should not reach here) + return -1; +} + +QRectF VisualShaderConnectionGraphicsObject::calculate_bounding_rect_from_coordinates( + const QPointF& start_coordinate, const QPointF& end_coordinate) const { + const float x1{(float)start_coordinate.x()}; + const float y1{(float)start_coordinate.y()}; + const float x2{(float)end_coordinate.x()}; + const float y2{(float)end_coordinate.y()}; + + // Calculate the expanded rect + const float min_x{qMin(x1, x2)}; + const float min_y{qMin(y1, y2)}; + const float max_x{qMax(x1, x2)}; + const float max_y{qMax(y1, y2)}; + + QRectF r({min_x, min_y}, QSizeF(max_x - min_x, max_y - min_y)); + + const bool in_h_abnormal_region{x2 < (x1 + h_abnormal_offset)}; + const bool in_v_abnormal_region{std::abs(y2 - y1) < v_abnormal_offset}; + + const int quadrant{detect_quadrant({x1, y1}, {x2, y2})}; + + // We will expand the bounding rect horizontally so that our connection don't get cut off + const float a_width_expansion{((x1 + h_abnormal_offset) - x2) * abnormal_face_to_back_control_width_expansion_factor}; + const float a_height_expansion{a_width_expansion * abnormal_face_to_back_control_height_expansion_factor}; + + if (in_h_abnormal_region) { + r.adjust(-a_width_expansion, 0.0f, a_width_expansion, 0.0f); + } + + switch (quadrant) { + case 2: // Quadrant II: Abnormal face to back + case 3: // Quadrant III: Abnormal face to back + case 6: // On -ve X-axis: Abnormal face to back + // Elipse like curve + if (in_v_abnormal_region) { + r.adjust(0.0f, -a_height_expansion, 0.0f, a_height_expansion); + } + break; + default: + break; + } + + return r; +} + +std::pair VisualShaderConnectionGraphicsObject::calculate_control_points( + const QPointF& start_coordinate, const QPointF& end_coordinated) const { + QPointF cp1; + QPointF cp2; + + const float x1{(float)start_coordinate.x()}; + const float y1{(float)start_coordinate.y()}; + const float x2{(float)end_coordinate.x()}; + const float y2{(float)end_coordinate.y()}; + + QRectF r{calculate_bounding_rect_from_coordinates(start_coordinate, end_coordinate)}; + + const bool in_h_abnormal_region{x2 < (x1 + h_abnormal_offset)}; + const bool in_v_abnormal_region{std::abs(y2 - y1) < v_abnormal_offset}; + + const int quadrant{detect_quadrant({x1, y1}, {x2, y2})}; + + // We will expand the bounding rect horizontally so that our connection don't get cut off + const float a_width_expansion{((x1 + h_abnormal_offset) - x2) * abnormal_face_to_back_control_width_expansion_factor}; + const float a_height_expansion{a_width_expansion * abnormal_face_to_back_control_height_expansion_factor}; + + const float cp_x_delta_factor{0.8f}; + const float cp_y_delta_factor{0.25f}; + + // Normal region control points deltas + const float cp_x_delta{(float)r.width() * cp_x_delta_factor}; + const float cp_y_delta{(float)r.height() * cp_y_delta_factor}; + + // Abnormal region control points deltas + const float a_cp_x_delta{((float)r.width() - a_width_expansion) * cp_x_delta_factor}; + const float a_cp_y_delta{((float)r.height() - a_height_expansion) * cp_y_delta_factor}; + + switch (quadrant) { + case 1: // Quadrant I: Normal face to back + // Find out if the connection is going from left to right normally + if (in_h_abnormal_region) { + // The connection is not going from left to right normally + // Our control points will be outside the end_coordinate and start_coordinate bounding rect + // We will expand the bounding rect horizontally to make it easier to get an accurate coordinate of the size + + // Here we cover cases of nodes not facing each other. + // This means we can't just send the path straight to the node. + + // Treated as inside Quadrant II + cp1 = {x1 + a_cp_x_delta, y1}; + cp2 = {x2 - a_cp_x_delta, y2}; + } else { + // Treated as inside Quadrant I + cp1 = {x1 + cp_x_delta, y1 - cp_y_delta}; + cp2 = {x2 - cp_x_delta, y2 + cp_y_delta}; + } + break; + case 2: // Quadrant II: Abnormal face to back + if (in_v_abnormal_region) { + cp1 = {x1 + a_cp_x_delta, y1 - a_cp_y_delta}; + cp2 = {x2 - a_cp_x_delta, y2 - a_cp_y_delta}; + } else { + cp1 = {x1 + a_cp_x_delta, y1}; + cp2 = {x2 - a_cp_x_delta, y2}; + } + break; + case 3: // Quadrant III: Abnormal face to back + if (in_v_abnormal_region) { + cp1 = {x1 + a_cp_x_delta, y1 - a_cp_y_delta}; + cp2 = {x2 - a_cp_x_delta, y2 - a_cp_y_delta}; + } else { + cp1 = {x1 + a_width_expansion, y1}; + cp2 = {x2 - a_width_expansion, y2}; + } + break; + case 4: // Quadrant IV: Normal face to back + if (in_h_abnormal_region) { + // Treated as inside Quadrant III + cp1 = {x1 + a_cp_x_delta, y1}; + cp2 = {x2 - a_cp_x_delta, y2}; + } else { + // Treated as inside Quadrant IV + cp1 = {x1 + cp_x_delta, y1 + cp_y_delta}; + cp2 = {x2 - cp_x_delta, y2 - cp_y_delta}; + } + break; + case 5: // On +ve X-axis: Normal face to back + // Straight line + cp1 = {x1, y1}; + cp2 = {x2, y2}; + break; + case 6: // On -ve X-axis: Abnormal face to back + // Elipse like curve + cp1 = {x1 + a_cp_x_delta, y1 - a_cp_y_delta}; + cp2 = {x2 - a_cp_x_delta, y2 - a_cp_y_delta}; + break; + case 7: // On +ve Y-axis: Abnormal face to back + case 8: // On -ve Y-axis: Abnormal face to back + cp1 = {x1 + a_cp_x_delta, y1}; + cp2 = {x2 - a_cp_x_delta, y2}; + break; + default: + return std::make_pair(start_coordinate, end_coordinate); + } + + return std::make_pair(cp1, cp2); +} + +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ +/***** *****/ +/***** Embed Widgets *****/ +/***** *****/ +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ + +VisualShaderNodeEmbedWidget::VisualShaderNodeEmbedWidget(const std::shared_ptr& node, QWidget* parent) + : QWidget(parent), layout(nullptr), preview_shader_button(nullptr), shader_previewer_widget(nullptr) { + layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + layout->setSizeConstraint(QLayout::SetNoConstraint); + layout->setSpacing(2); + layout->setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); + + if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeInputEmbedWidget* embed_widget = new VisualShaderNodeInputEmbedWidget(p); + layout->addWidget(embed_widget); + QObject::connect(embed_widget, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeFloatFuncEmbedWidget* embed_widget = new VisualShaderNodeFloatFuncEmbedWidget(p); + layout->addWidget(embed_widget); + QObject::connect(embed_widget, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeIntFuncEmbedWidget* embed_widget = new VisualShaderNodeIntFuncEmbedWidget(p); + layout->addWidget(embed_widget); + QObject::connect(embed_widget, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeUIntFuncEmbedWidget* embed_widget = new VisualShaderNodeUIntFuncEmbedWidget(p); + layout->addWidget(embed_widget); + QObject::connect(embed_widget, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeFloatOpEmbedWidget* embed_widget = new VisualShaderNodeFloatOpEmbedWidget(p); + layout->addWidget(embed_widget); + QObject::connect(embed_widget, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeIntOpEmbedWidget* embed_widget = new VisualShaderNodeIntOpEmbedWidget(p); + layout->addWidget(embed_widget); + QObject::connect(embed_widget, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeUIntOpEmbedWidget* embed_widget = new VisualShaderNodeUIntOpEmbedWidget(p); + layout->addWidget(embed_widget); + QObject::connect(embed_widget, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeValueNoiseEmbedWidget* embed_widget = new VisualShaderNodeValueNoiseEmbedWidget(p); + layout->addWidget(embed_widget); + QObject::connect(embed_widget, &QLineEdit::textChanged, this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodePerlinNoiseEmbedWidget* embed_widget = new VisualShaderNodePerlinNoiseEmbedWidget(p); + layout->addWidget(embed_widget); + QObject::connect(embed_widget, &QLineEdit::textChanged, this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeVoronoiNoiseAngleOffsetEmbedWidget* embed_widget = + new VisualShaderNodeVoronoiNoiseAngleOffsetEmbedWidget(p); + VisualShaderNodeVoronoiNoiseCellDensityEmbedWidget* embed_widget2 = + new VisualShaderNodeVoronoiNoiseCellDensityEmbedWidget(p); + layout->addWidget(embed_widget); + layout->addWidget(embed_widget2); + QObject::connect(embed_widget, &QLineEdit::textChanged, this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + QObject::connect(embed_widget2, &QLineEdit::textChanged, this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeColorConstantEmbedWidget* embed_widget = new VisualShaderNodeColorConstantEmbedWidget(p); + layout->addWidget(embed_widget); + QObject::connect(embed_widget, &VisualShaderNodeColorConstantEmbedWidget::color_changed, this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeBooleanConstantEmbedWidget* embed_widget = new VisualShaderNodeBooleanConstantEmbedWidget(p); + layout->addWidget(embed_widget); + QObject::connect(embed_widget, &QCheckBox::stateChanged, this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeFloatConstantEmbedWidget* embed_widget = new VisualShaderNodeFloatConstantEmbedWidget(p); + layout->addWidget(embed_widget); + QObject::connect(embed_widget, &QLineEdit::textChanged, this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeIntConstantEmbedWidget* embed_widget = new VisualShaderNodeIntConstantEmbedWidget(p); + layout->addWidget(embed_widget); + QObject::connect(embed_widget, &QLineEdit::textChanged, this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeUIntConstantEmbedWidget* embed_widget = new VisualShaderNodeUIntConstantEmbedWidget(p); + layout->addWidget(embed_widget); + QObject::connect(embed_widget, &QLineEdit::textChanged, this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeVec2ConstantEmbedWidget* embed_widget = new VisualShaderNodeVec2ConstantEmbedWidget(p); + layout->addLayout(embed_widget); + QObject::connect(embed_widget->get_x_edit_widget(), &QLineEdit::textChanged, this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + QObject::connect(embed_widget->get_y_edit_widget(), &QLineEdit::textChanged, this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeVec3ConstantEmbedWidget* embed_widget = new VisualShaderNodeVec3ConstantEmbedWidget(p); + layout->addLayout(embed_widget); + QObject::connect(embed_widget->get_x_edit_widget(), &QLineEdit::textChanged, this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + QObject::connect(embed_widget->get_y_edit_widget(), &QLineEdit::textChanged, this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + QObject::connect(embed_widget->get_z_edit_widget(), &QLineEdit::textChanged, this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeVec4ConstantEmbedWidget* embed_widget = new VisualShaderNodeVec4ConstantEmbedWidget(p); + layout->addLayout(embed_widget); + QObject::connect(embed_widget->get_x_edit_widget(), &QLineEdit::textChanged, this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + QObject::connect(embed_widget->get_y_edit_widget(), &QLineEdit::textChanged, this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + QObject::connect(embed_widget->get_z_edit_widget(), &QLineEdit::textChanged, this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + QObject::connect(embed_widget->get_w_edit_widget(), &QLineEdit::textChanged, this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeVectorBaseEmbedWidget* embed_widget = new VisualShaderNodeVectorBaseEmbedWidget(p); + layout->addWidget(embed_widget); + VisualShaderNodeVectorOpEmbedWidget* embed_widget2 = new VisualShaderNodeVectorOpEmbedWidget(p); + layout->addWidget(embed_widget2); + QObject::connect(embed_widget2, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeVectorBaseEmbedWidget* embed_widget = new VisualShaderNodeVectorBaseEmbedWidget(p); + layout->addWidget(embed_widget); + VisualShaderNodeVectorFuncEmbedWidget* embed_widget2 = new VisualShaderNodeVectorFuncEmbedWidget(p); + layout->addWidget(embed_widget2); + QObject::connect(embed_widget2, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeVectorBaseEmbedWidget* embed_widget = new VisualShaderNodeVectorBaseEmbedWidget(p); + layout->addWidget(embed_widget); + QObject::connect(embed_widget, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeEmbedWidget::on_node_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeVectorBaseEmbedWidget* embed_widget = new VisualShaderNodeVectorBaseEmbedWidget(p); + layout->addWidget(embed_widget); + QObject::connect(embed_widget, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeEmbedWidget::on_node_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeDerivativeFuncEmbedWidget* embed_widget = new VisualShaderNodeDerivativeFuncEmbedWidget(p); + layout->addLayout(embed_widget); + QObject::connect(embed_widget->get_op_type_combo_box(), QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + + QObject::connect(embed_widget->get_function_combo_box(), QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + QObject::connect(embed_widget->get_precision_combo_box(), QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeCompareEmbedWidget* embed_widget = new VisualShaderNodeCompareEmbedWidget(p); + layout->addLayout(embed_widget); + QObject::connect(embed_widget->get_comparison_type_combo_box(), QOverload::of(&QComboBox::currentIndexChanged), + this, &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + QObject::connect(embed_widget->get_func_combo_box(), QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + QObject::connect(embed_widget->get_condition_combo_box(), QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeSwitchEmbedWidget* embed_widget = new VisualShaderNodeSwitchEmbedWidget(p); + layout->addWidget(embed_widget); + QObject::connect(embed_widget, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } else if (auto p{std::dynamic_pointer_cast(node)}) { + VisualShaderNodeIsEmbedWidget* embed_widget = new VisualShaderNodeIsEmbedWidget(p); + layout->addWidget(embed_widget); + QObject::connect(embed_widget, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeEmbedWidget::on_shader_preview_update_requested); + } + + // Create the button that will show/hide the shader previewer + preview_shader_button = new QPushButton("Show Preview", this); + preview_shader_button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + preview_shader_button->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + preview_shader_button->setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + preview_shader_button->setToolTip("Create a new node"); + layout->addWidget(preview_shader_button); + QObject::connect(preview_shader_button, &QPushButton::pressed, this, + &VisualShaderNodeEmbedWidget::on_preview_shader_button_pressed); + + this->setContentsMargins(10, 10, 10, 10); // Left, top, right, bottom + setLayout(layout); +} + +VisualShaderNodeEmbedWidget::~VisualShaderNodeEmbedWidget() {} + +/*************************************/ +/* Input Node */ +/*************************************/ + +VisualShaderNodeInputEmbedWidget::VisualShaderNodeInputEmbedWidget(const std::shared_ptr& node) + : QComboBox(), node(node) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + // Add the default item + addItem(QString::fromStdString(node->get_input_name()), ""); + + const VisualShaderNodeInput::Port* ps{VisualShaderNodeInput::get_ports()}; + + int i{0}; + + while (ps[i].type != VisualShaderNode::PortType::PORT_TYPE_ENUM_SIZE) { + addItem(QString::fromStdString(ps[i].name)); + i++; + } + + QObject::connect(this, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeInputEmbedWidget::on_current_index_changed); +} + +VisualShaderNodeInputEmbedWidget::~VisualShaderNodeInputEmbedWidget() {} + +void VisualShaderNodeInputEmbedWidget::on_current_index_changed(const int& index) { + node->set_input_name(itemText(index).toStdString()); +} + +/*************************************/ +/* Float Op Node */ +/*************************************/ + +VisualShaderNodeFloatOpEmbedWidget::VisualShaderNodeFloatOpEmbedWidget( + const std::shared_ptr& node) + : QComboBox(), node(node) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + addItem("Add", (int)VisualShaderNodeFloatOp::OP_ADD); + addItem("Subtract", (int)VisualShaderNodeFloatOp::OP_SUB); + addItem("Multiply", (int)VisualShaderNodeFloatOp::OP_MUL); + addItem("Divide", (int)VisualShaderNodeFloatOp::OP_DIV); + addItem("Modulus", (int)VisualShaderNodeFloatOp::OP_MOD); + addItem("Power", (int)VisualShaderNodeFloatOp::OP_POW); + addItem("Maximum", (int)VisualShaderNodeFloatOp::OP_MAX); + addItem("Minimum", (int)VisualShaderNodeFloatOp::OP_MIN); + addItem("Arc Tangent 2", (int)VisualShaderNodeFloatOp::OP_ATAN2); + addItem("Step", (int)VisualShaderNodeFloatOp::OP_STEP); + + setCurrentIndex((int)node->get_operator()); + + QObject::connect(this, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeFloatOpEmbedWidget::on_current_index_changed); +} + +VisualShaderNodeFloatOpEmbedWidget::~VisualShaderNodeFloatOpEmbedWidget() {} + +void VisualShaderNodeFloatOpEmbedWidget::on_current_index_changed(const int& index) { + node->set_operator((VisualShaderNodeFloatOp::Operator)itemData(index).toInt()); +} + +/*************************************/ +/* Int Op Node */ +/*************************************/ + +VisualShaderNodeIntOpEmbedWidget::VisualShaderNodeIntOpEmbedWidget(const std::shared_ptr& node) + : QComboBox(), node(node) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + addItem("Add", (int)VisualShaderNodeIntOp::OP_ADD); + addItem("Subtract", (int)VisualShaderNodeIntOp::OP_SUB); + addItem("Multiply", (int)VisualShaderNodeIntOp::OP_MUL); + addItem("Divide", (int)VisualShaderNodeIntOp::OP_DIV); + addItem("Modulus", (int)VisualShaderNodeIntOp::OP_MOD); + addItem("Maximum", (int)VisualShaderNodeIntOp::OP_MAX); + addItem("Minimum", (int)VisualShaderNodeIntOp::OP_MIN); + addItem("Bitwise AND", (int)VisualShaderNodeIntOp::OP_BITWISE_AND); + addItem("Bitwise OR", (int)VisualShaderNodeIntOp::OP_BITWISE_OR); + addItem("Bitwise XOR", (int)VisualShaderNodeIntOp::OP_BITWISE_XOR); + addItem("Bitwise Left Shift", (int)VisualShaderNodeIntOp::OP_BITWISE_LEFT_SHIFT); + addItem("Bitwise Right Shift", (int)VisualShaderNodeIntOp::OP_BITWISE_RIGHT_SHIFT); + + setCurrentIndex((int)node->get_operator()); + + QObject::connect(this, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeIntOpEmbedWidget::on_current_index_changed); +} + +VisualShaderNodeIntOpEmbedWidget::~VisualShaderNodeIntOpEmbedWidget() {} + +void VisualShaderNodeIntOpEmbedWidget::on_current_index_changed(const int& index) { + node->set_operator((VisualShaderNodeIntOp::Operator)itemData(index).toInt()); +} + +/*************************************/ +/* UInt Op Node */ +/*************************************/ + +VisualShaderNodeUIntOpEmbedWidget::VisualShaderNodeUIntOpEmbedWidget( + const std::shared_ptr& node) + : QComboBox(), node(node) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + addItem("Add", (int)VisualShaderNodeUIntOp::OP_ADD); + addItem("Subtract", (int)VisualShaderNodeUIntOp::OP_SUB); + addItem("Multiply", (int)VisualShaderNodeUIntOp::OP_MUL); + addItem("Divide", (int)VisualShaderNodeUIntOp::OP_DIV); + addItem("Modulus", (int)VisualShaderNodeUIntOp::OP_MOD); + addItem("Maximum", (int)VisualShaderNodeUIntOp::OP_MAX); + addItem("Minimum", (int)VisualShaderNodeUIntOp::OP_MIN); + addItem("Bitwise AND", (int)VisualShaderNodeUIntOp::OP_BITWISE_AND); + addItem("Bitwise OR", (int)VisualShaderNodeUIntOp::OP_BITWISE_OR); + addItem("Bitwise XOR", (int)VisualShaderNodeUIntOp::OP_BITWISE_XOR); + addItem("Bitwise Left Shift", (int)VisualShaderNodeUIntOp::OP_BITWISE_LEFT_SHIFT); + addItem("Bitwise Right Shift", (int)VisualShaderNodeUIntOp::OP_BITWISE_RIGHT_SHIFT); + + setCurrentIndex((int)node->get_operator()); + + QObject::connect(this, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeUIntOpEmbedWidget::on_current_index_changed); +} + +VisualShaderNodeUIntOpEmbedWidget::~VisualShaderNodeUIntOpEmbedWidget() {} + +void VisualShaderNodeUIntOpEmbedWidget::on_current_index_changed(const int& index) { + node->set_operator((VisualShaderNodeUIntOp::Operator)itemData(index).toInt()); +} + +/*************************************/ +/* Float Funcs Node */ +/*************************************/ + +VisualShaderNodeFloatFuncEmbedWidget::VisualShaderNodeFloatFuncEmbedWidget( + const std::shared_ptr& node) + : QComboBox(), node(node) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + addItem("Sin", (int)VisualShaderNodeFloatFunc::FUNC_SIN); + addItem("Cos", (int)VisualShaderNodeFloatFunc::FUNC_COS); + addItem("Tan", (int)VisualShaderNodeFloatFunc::FUNC_TAN); + addItem("Arc Sine", (int)VisualShaderNodeFloatFunc::FUNC_ASIN); + addItem("Arc Cosine", (int)VisualShaderNodeFloatFunc::FUNC_ACOS); + addItem("Arc Tangent", (int)VisualShaderNodeFloatFunc::FUNC_ATAN); + addItem("Sine Hyperbolic", (int)VisualShaderNodeFloatFunc::FUNC_SINH); + addItem("Cosine Hyperbolic", (int)VisualShaderNodeFloatFunc::FUNC_COSH); + addItem("Tangent Hyperbolic", (int)VisualShaderNodeFloatFunc::FUNC_TANH); + addItem("Logarithm", (int)VisualShaderNodeFloatFunc::FUNC_LOG); + addItem("Exponential", (int)VisualShaderNodeFloatFunc::FUNC_EXP); + addItem("Square Root", (int)VisualShaderNodeFloatFunc::FUNC_SQRT); + addItem("Absolute", (int)VisualShaderNodeFloatFunc::FUNC_ABS); + addItem("Sign", (int)VisualShaderNodeFloatFunc::FUNC_SIGN); + addItem("Floor", (int)VisualShaderNodeFloatFunc::FUNC_FLOOR); + addItem("Round", (int)VisualShaderNodeFloatFunc::FUNC_ROUND); + addItem("Ceil", (int)VisualShaderNodeFloatFunc::FUNC_CEIL); + addItem("Fraction", (int)VisualShaderNodeFloatFunc::FUNC_FRACT); + addItem("Saturate", (int)VisualShaderNodeFloatFunc::FUNC_SATURATE); + addItem("Negate", (int)VisualShaderNodeFloatFunc::FUNC_NEGATE); + addItem("Arc Cosine Hyperbolic", (int)VisualShaderNodeFloatFunc::FUNC_ACOSH); + addItem("Arc Sine Hyperbolic", (int)VisualShaderNodeFloatFunc::FUNC_ASINH); + addItem("Arc Tangent Hyperbolic", (int)VisualShaderNodeFloatFunc::FUNC_ATANH); + addItem("Degrees", (int)VisualShaderNodeFloatFunc::FUNC_DEGREES); + addItem("Exponential 2", (int)VisualShaderNodeFloatFunc::FUNC_EXP2); + addItem("Inverse Square Root", (int)VisualShaderNodeFloatFunc::FUNC_INVERSE_SQRT); + addItem("Logarithm 2", (int)VisualShaderNodeFloatFunc::FUNC_LOG2); + addItem("Radians", (int)VisualShaderNodeFloatFunc::FUNC_RADIANS); + addItem("Reciprocal", (int)VisualShaderNodeFloatFunc::FUNC_RECIPROCAL); + addItem("Round Even", (int)VisualShaderNodeFloatFunc::FUNC_ROUNDEVEN); + addItem("Truncate", (int)VisualShaderNodeFloatFunc::FUNC_TRUNC); + addItem("One Minus", (int)VisualShaderNodeFloatFunc::FUNC_ONEMINUS); + + setCurrentIndex((int)node->get_function()); + + QObject::connect(this, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeFloatFuncEmbedWidget::on_current_index_changed); +} + +VisualShaderNodeFloatFuncEmbedWidget::~VisualShaderNodeFloatFuncEmbedWidget() {} + +void VisualShaderNodeFloatFuncEmbedWidget::on_current_index_changed(const int& index) { + node->set_function((VisualShaderNodeFloatFunc::Function)itemData(index).toInt()); +} + +/*************************************/ +/* Int Funcs Node */ +/*************************************/ + +VisualShaderNodeIntFuncEmbedWidget::VisualShaderNodeIntFuncEmbedWidget( + const std::shared_ptr& node) + : QComboBox(), node(node) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + addItem("Abs", (int)VisualShaderNodeIntFunc::FUNC_ABS); + addItem("Negate", (int)VisualShaderNodeIntFunc::FUNC_NEGATE); + addItem("Sign", (int)VisualShaderNodeIntFunc::FUNC_SIGN); + addItem("Bitwise NOT", (int)VisualShaderNodeIntFunc::FUNC_BITWISE_NOT); + + setCurrentIndex((int)node->get_function()); + + QObject::connect(this, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeIntFuncEmbedWidget::on_current_index_changed); +} + +VisualShaderNodeIntFuncEmbedWidget::~VisualShaderNodeIntFuncEmbedWidget() {} + +void VisualShaderNodeIntFuncEmbedWidget::on_current_index_changed(const int& index) { + node->set_function((VisualShaderNodeIntFunc::Function)itemData(index).toInt()); +} + +/*************************************/ +/* UInt Funcs Node */ +/*************************************/ + +VisualShaderNodeUIntFuncEmbedWidget::VisualShaderNodeUIntFuncEmbedWidget( + const std::shared_ptr& node) + : QComboBox(), node(node) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + addItem("Negate", (int)VisualShaderNodeUIntFunc::FUNC_NEGATE); + addItem("Bitwise NOT", (int)VisualShaderNodeUIntFunc::FUNC_BITWISE_NOT); + + setCurrentIndex((int)node->get_function()); + + QObject::connect(this, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeUIntFuncEmbedWidget::on_current_index_changed); +} + +VisualShaderNodeUIntFuncEmbedWidget::~VisualShaderNodeUIntFuncEmbedWidget() {} + +void VisualShaderNodeUIntFuncEmbedWidget::on_current_index_changed(const int& index) { + node->set_function((VisualShaderNodeUIntFunc::Function)itemData(index).toInt()); +} + +/*************************************/ +/* Vector Base */ +/*************************************/ + +VisualShaderNodeVectorBaseEmbedWidget::VisualShaderNodeVectorBaseEmbedWidget( + const std::shared_ptr& node) + : QComboBox(), node(node) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + addItem("Vector 2D", (int)VisualShaderNodeVectorBase::OP_TYPE_VECTOR_2D); + addItem("Vector 3D", (int)VisualShaderNodeVectorBase::OP_TYPE_VECTOR_3D); + addItem("Vector 4D", (int)VisualShaderNodeVectorBase::OP_TYPE_VECTOR_4D); + + // set the current index + setCurrentIndex((int)node->get_op_type()); + + QObject::connect(this, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeVectorBaseEmbedWidget::on_current_index_changed); +} + +VisualShaderNodeVectorBaseEmbedWidget::~VisualShaderNodeVectorBaseEmbedWidget() {} + +void VisualShaderNodeVectorBaseEmbedWidget::on_current_index_changed(const int& index) { + node->set_op_type((VisualShaderNodeVectorBase::OpType)itemData(index).toInt()); +} + +/*************************************/ +/* Vector Op Node */ +/*************************************/ + +VisualShaderNodeVectorOpEmbedWidget::VisualShaderNodeVectorOpEmbedWidget( + const std::shared_ptr& node) + : QComboBox(), node(node) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + addItem("Add", (int)VisualShaderNodeVectorOp::OP_ADD); + addItem("Subtract", (int)VisualShaderNodeVectorOp::OP_SUB); + addItem("Multiply", (int)VisualShaderNodeVectorOp::OP_MUL); + addItem("Divide", (int)VisualShaderNodeVectorOp::OP_DIV); + addItem("Modulus", (int)VisualShaderNodeVectorOp::OP_MOD); + addItem("Power", (int)VisualShaderNodeVectorOp::OP_POW); + addItem("Maximum", (int)VisualShaderNodeVectorOp::OP_MAX); + addItem("Minimum", (int)VisualShaderNodeVectorOp::OP_MIN); + addItem("Cross Product", (int)VisualShaderNodeVectorOp::OP_CROSS); + addItem("Arc Tangent 2", (int)VisualShaderNodeVectorOp::OP_ATAN2); + addItem("Reflect", (int)VisualShaderNodeVectorOp::OP_REFLECT); + addItem("Step", (int)VisualShaderNodeVectorOp::OP_STEP); + + setCurrentIndex((int)node->get_operator()); + + QObject::connect(this, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeVectorOpEmbedWidget::on_current_index_changed); +} + +VisualShaderNodeVectorOpEmbedWidget::~VisualShaderNodeVectorOpEmbedWidget() {} + +void VisualShaderNodeVectorOpEmbedWidget::on_current_index_changed(const int& index) { + node->set_operator((VisualShaderNodeVectorOp::Operator)itemData(index).toInt()); +} + +/*************************************/ +/* Vector Funcs Node */ +/*************************************/ + +VisualShaderNodeVectorFuncEmbedWidget::VisualShaderNodeVectorFuncEmbedWidget( + const std::shared_ptr& node) + : QComboBox(), node(node) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + addItem("Normalize", (int)VisualShaderNodeVectorFunc::FUNC_NORMALIZE); + addItem("Saturate", (int)VisualShaderNodeVectorFunc::FUNC_SATURATE); + addItem("Negate", (int)VisualShaderNodeVectorFunc::FUNC_NEGATE); + addItem("Reciprocal", (int)VisualShaderNodeVectorFunc::FUNC_RECIPROCAL); + addItem("Abs", (int)VisualShaderNodeVectorFunc::FUNC_ABS); + addItem("Arc Cosine", (int)VisualShaderNodeVectorFunc::FUNC_ACOS); + addItem("Arc Cosine Hyperbolic", (int)VisualShaderNodeVectorFunc::FUNC_ACOSH); + addItem("Arc Sine", (int)VisualShaderNodeVectorFunc::FUNC_ASIN); + addItem("Arc Sine Hyperbolic", (int)VisualShaderNodeVectorFunc::FUNC_ASINH); + addItem("Arc Tangent", (int)VisualShaderNodeVectorFunc::FUNC_ATAN); + addItem("Arc Tangent Hyperbolic", (int)VisualShaderNodeVectorFunc::FUNC_ATANH); + addItem("Ceil", (int)VisualShaderNodeVectorFunc::FUNC_CEIL); + addItem("Cos", (int)VisualShaderNodeVectorFunc::FUNC_COS); + addItem("Cosine Hyperbolic", (int)VisualShaderNodeVectorFunc::FUNC_COSH); + addItem("Degrees", (int)VisualShaderNodeVectorFunc::FUNC_DEGREES); + addItem("Exp", (int)VisualShaderNodeVectorFunc::FUNC_EXP); + addItem("Exp2", (int)VisualShaderNodeVectorFunc::FUNC_EXP2); + addItem("Floor", (int)VisualShaderNodeVectorFunc::FUNC_FLOOR); + addItem("Fraction", (int)VisualShaderNodeVectorFunc::FUNC_FRACT); + addItem("Inverse Square Root", (int)VisualShaderNodeVectorFunc::FUNC_INVERSE_SQRT); + addItem("Log", (int)VisualShaderNodeVectorFunc::FUNC_LOG); + addItem("Log2", (int)VisualShaderNodeVectorFunc::FUNC_LOG2); + addItem("Radians", (int)VisualShaderNodeVectorFunc::FUNC_RADIANS); + addItem("Round", (int)VisualShaderNodeVectorFunc::FUNC_ROUND); + addItem("Round Even", (int)VisualShaderNodeVectorFunc::FUNC_ROUNDEVEN); + addItem("Sign", (int)VisualShaderNodeVectorFunc::FUNC_SIGN); + addItem("Sin", (int)VisualShaderNodeVectorFunc::FUNC_SIN); + addItem("Sine Hyperbolic", (int)VisualShaderNodeVectorFunc::FUNC_SINH); + addItem("Sqrt", (int)VisualShaderNodeVectorFunc::FUNC_SQRT); + addItem("Tan", (int)VisualShaderNodeVectorFunc::FUNC_TAN); + addItem("Tangent Hyperbolic", (int)VisualShaderNodeVectorFunc::FUNC_TANH); + addItem("Truncate", (int)VisualShaderNodeVectorFunc::FUNC_TRUNC); + addItem("One Minus", (int)VisualShaderNodeVectorFunc::FUNC_ONEMINUS); + + setCurrentIndex((int)node->get_function()); + + QObject::connect(this, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeVectorFuncEmbedWidget::on_current_index_changed); +} + +VisualShaderNodeVectorFuncEmbedWidget::~VisualShaderNodeVectorFuncEmbedWidget() {} + +void VisualShaderNodeVectorFuncEmbedWidget::on_current_index_changed(const int& index) { + node->set_function((VisualShaderNodeVectorFunc::Function)itemData(index).toInt()); +} + +/*************************************/ +/* Color Constant Node */ +/*************************************/ + +VisualShaderNodeColorConstantEmbedWidget::VisualShaderNodeColorConstantEmbedWidget( + const std::shared_ptr& node) + : QPushButton(), node(node) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + { + QPalette palette{this->palette()}; + TColor c{node->get_constant()}; + palette.setColor(QPalette::Button, QColor(c.r, c.g, c.b, c.a)); + this->setPalette(palette); + } + + QObject::connect(this, &QPushButton::pressed, this, &VisualShaderNodeColorConstantEmbedWidget::on_pressed); +} + +VisualShaderNodeColorConstantEmbedWidget::~VisualShaderNodeColorConstantEmbedWidget() {} + +void VisualShaderNodeColorConstantEmbedWidget::on_pressed() { + TColor c{node->get_constant()}; + QColor color{QColorDialog::getColor(QColor(c.r, c.g, c.b, c.a), this, "Select Color")}; + + // If a valid color is picked, update the button and store the color + if (color.isValid()) { + node->set_constant({(float)color.red(), (float)color.green(), (float)color.blue(), (float)color.alpha()}); + QPalette palette{this->palette()}; + palette.setColor(QPalette::Button, color); + this->setPalette(palette); + this->update(); + Q_EMIT color_changed(); + } else { + // If the user cancels the color dialog, reset the button to the previous color + QColor previous_color{QColor(c.r, c.g, c.b, c.a)}; + QPalette palette{this->palette()}; + palette.setColor(QPalette::Button, previous_color); + this->setPalette(palette); + this->update(); + } +} + +/*************************************/ +/* Boolean Constant Node */ +/*************************************/ + +VisualShaderNodeBooleanConstantEmbedWidget::VisualShaderNodeBooleanConstantEmbedWidget( + const std::shared_ptr& node) + : QCheckBox(), node(node) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + setCheckState(node->get_constant() ? Qt::Checked : Qt::Unchecked); + + QObject::connect(this, &QCheckBox::stateChanged, this, &VisualShaderNodeBooleanConstantEmbedWidget::on_state_changed); +} + +VisualShaderNodeBooleanConstantEmbedWidget::~VisualShaderNodeBooleanConstantEmbedWidget() {} + +void VisualShaderNodeBooleanConstantEmbedWidget::on_state_changed(const int& state) { + node->set_constant(state == Qt::Checked); +} + +/*************************************/ +/* Float Constant */ +/*************************************/ + +VisualShaderNodeFloatConstantEmbedWidget::VisualShaderNodeFloatConstantEmbedWidget( + const std::shared_ptr& node) + : QLineEdit(), node(node) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + setPlaceholderText("Value"); + + setText(QString::number(node->get_constant())); + + QObject::connect(this, &QLineEdit::textChanged, this, &VisualShaderNodeFloatConstantEmbedWidget::on_text_changed); +} + +VisualShaderNodeFloatConstantEmbedWidget::~VisualShaderNodeFloatConstantEmbedWidget() {} + +void VisualShaderNodeFloatConstantEmbedWidget::on_text_changed(const QString& text) { + if (text.isEmpty()) { + node->set_constant(0.0f); + } else { + bool ok; + text.toFloat(&ok); + if (ok) { + node->set_constant(text.toFloat()); + } else { + node->set_constant(0.0f); + setText(""); + } + } +} + +/*************************************/ +/* Int Constant */ +/*************************************/ + +VisualShaderNodeIntConstantEmbedWidget::VisualShaderNodeIntConstantEmbedWidget( + const std::shared_ptr& node) + : QLineEdit(), node(node) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + setPlaceholderText("Value"); + + setText(QString::number(node->get_constant())); + + QObject::connect(this, &QLineEdit::textChanged, this, &VisualShaderNodeIntConstantEmbedWidget::on_text_changed); +} + +VisualShaderNodeIntConstantEmbedWidget::~VisualShaderNodeIntConstantEmbedWidget() {} + +void VisualShaderNodeIntConstantEmbedWidget::on_text_changed(const QString& text) { + if (text.isEmpty()) { + node->set_constant(0); + } else { + bool ok; + text.toInt(&ok); + if (ok) { + node->set_constant(text.toInt()); + } else { + node->set_constant(0); + setText(""); + } + } +} + +/*************************************/ +/* UInt Constant */ +/*************************************/ + +VisualShaderNodeUIntConstantEmbedWidget::VisualShaderNodeUIntConstantEmbedWidget( + const std::shared_ptr& node) + : QLineEdit(), node(node) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + setPlaceholderText("Value"); + + setText(QString::number(node->get_constant())); + + QObject::connect(this, &QLineEdit::textChanged, this, &VisualShaderNodeUIntConstantEmbedWidget::on_text_changed); +} + +VisualShaderNodeUIntConstantEmbedWidget::~VisualShaderNodeUIntConstantEmbedWidget() {} + +void VisualShaderNodeUIntConstantEmbedWidget::on_text_changed(const QString& text) { + if (text.isEmpty()) { + node->set_constant(0); + } else { + bool ok; + text.toUInt(&ok); + if (ok) { + node->set_constant(text.toUInt()); + } else { + node->set_constant(0); + setText(""); + } + } +} + +/*************************************/ +/* Vec2 Constant Node */ +/*************************************/ + +VisualShaderNodeVec2ConstantEmbedWidget::VisualShaderNodeVec2ConstantEmbedWidget( + const std::shared_ptr& node) + : QVBoxLayout(), node(node) { + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + setSizeConstraint(QLayout::SetNoConstraint); + setSpacing(2); + setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); + + x_edit_widget = new QLineEdit(); + y_edit_widget = new QLineEdit(); + + x_edit_widget->setPlaceholderText("X"); + y_edit_widget->setPlaceholderText("Y"); + + x_edit_widget->setText(QString::number(node->get_constant().x)); + y_edit_widget->setText(QString::number(node->get_constant().y)); + + QObject::connect(x_edit_widget, &QLineEdit::textChanged, this, + &VisualShaderNodeVec2ConstantEmbedWidget::on_x_text_changed); + QObject::connect(y_edit_widget, &QLineEdit::textChanged, this, + &VisualShaderNodeVec2ConstantEmbedWidget::on_y_text_changed); + + addWidget(x_edit_widget); + addWidget(y_edit_widget); +} + +VisualShaderNodeVec2ConstantEmbedWidget::~VisualShaderNodeVec2ConstantEmbedWidget() {} + +void VisualShaderNodeVec2ConstantEmbedWidget::on_x_text_changed(const QString& text) { + if (text.isEmpty()) { + node->set_constant({0.0f, node->get_constant().y}); + } else { + bool ok; + text.toFloat(&ok); + if (ok) { + node->set_constant({text.toFloat(), node->get_constant().y}); + } else { + node->set_constant({0.0f, node->get_constant().y}); + x_edit_widget->setText(""); + } + } +} + +void VisualShaderNodeVec2ConstantEmbedWidget::on_y_text_changed(const QString& text) { + if (text.isEmpty()) { + node->set_constant({node->get_constant().x, 0.0f}); + } else { + bool ok; + text.toFloat(&ok); + if (ok) { + node->set_constant({node->get_constant().x, text.toFloat()}); + } else { + node->set_constant({node->get_constant().x, 0.0f}); + y_edit_widget->setText(""); + } + } +} + +/*************************************/ +/* Vec3 Constant Node */ +/*************************************/ + +VisualShaderNodeVec3ConstantEmbedWidget::VisualShaderNodeVec3ConstantEmbedWidget( + const std::shared_ptr& node) + : QVBoxLayout(), node(node) { + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + setSizeConstraint(QLayout::SetNoConstraint); + setSpacing(2); + setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); + + x_edit_widget = new QLineEdit(); + y_edit_widget = new QLineEdit(); + z_edit_widget = new QLineEdit(); + + x_edit_widget->setPlaceholderText("X"); + y_edit_widget->setPlaceholderText("Y"); + z_edit_widget->setPlaceholderText("Z"); + + x_edit_widget->setText(QString::number(node->get_constant().x)); + y_edit_widget->setText(QString::number(node->get_constant().y)); + z_edit_widget->setText(QString::number(node->get_constant().z)); + + QObject::connect(x_edit_widget, &QLineEdit::textChanged, this, + &VisualShaderNodeVec3ConstantEmbedWidget::on_x_text_changed); + QObject::connect(y_edit_widget, &QLineEdit::textChanged, this, + &VisualShaderNodeVec3ConstantEmbedWidget::on_y_text_changed); + QObject::connect(z_edit_widget, &QLineEdit::textChanged, this, + &VisualShaderNodeVec3ConstantEmbedWidget::on_z_text_changed); + + addWidget(x_edit_widget); + addWidget(y_edit_widget); + addWidget(z_edit_widget); +} + +VisualShaderNodeVec3ConstantEmbedWidget::~VisualShaderNodeVec3ConstantEmbedWidget() {} + +void VisualShaderNodeVec3ConstantEmbedWidget::on_x_text_changed(const QString& text) { + if (text.isEmpty()) { + node->set_constant({0.0f, node->get_constant().y, node->get_constant().z}); + } else { + bool ok; + text.toFloat(&ok); + if (ok) { + node->set_constant({text.toFloat(), node->get_constant().y, node->get_constant().z}); + } else { + node->set_constant({0.0f, node->get_constant().y, node->get_constant().z}); + x_edit_widget->setText(""); + } + } +} + +void VisualShaderNodeVec3ConstantEmbedWidget::on_y_text_changed(const QString& text) { + if (text.isEmpty()) { + node->set_constant({node->get_constant().x, 0.0f, node->get_constant().z}); + } else { + bool ok; + text.toFloat(&ok); + if (ok) { + node->set_constant({node->get_constant().x, text.toFloat(), node->get_constant().z}); + } else { + node->set_constant({node->get_constant().x, 0.0f, node->get_constant().z}); + y_edit_widget->setText(""); + } + } +} + +void VisualShaderNodeVec3ConstantEmbedWidget::on_z_text_changed(const QString& text) { + if (text.isEmpty()) { + node->set_constant({node->get_constant().x, node->get_constant().y, 0.0f}); + } else { + bool ok; + text.toFloat(&ok); + if (ok) { + node->set_constant({node->get_constant().x, node->get_constant().y, text.toFloat()}); + } else { + node->set_constant({node->get_constant().x, node->get_constant().y, 0.0f}); + z_edit_widget->setText(""); + } + } +} + +/*************************************/ +/* Vec4 Constant Node */ +/*************************************/ + +VisualShaderNodeVec4ConstantEmbedWidget::VisualShaderNodeVec4ConstantEmbedWidget( + const std::shared_ptr& node) + : QVBoxLayout(), node(node) { + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + setSizeConstraint(QLayout::SetNoConstraint); + setSpacing(2); + setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); + + x_edit_widget = new QLineEdit(); + y_edit_widget = new QLineEdit(); + z_edit_widget = new QLineEdit(); + w_edit_widget = new QLineEdit(); + + x_edit_widget->setPlaceholderText("X"); + y_edit_widget->setPlaceholderText("Y"); + z_edit_widget->setPlaceholderText("Z"); + w_edit_widget->setPlaceholderText("W"); + + x_edit_widget->setText(QString::number(node->get_constant().x)); + y_edit_widget->setText(QString::number(node->get_constant().y)); + z_edit_widget->setText(QString::number(node->get_constant().z)); + w_edit_widget->setText(QString::number(node->get_constant().w)); + + QObject::connect(x_edit_widget, &QLineEdit::textChanged, this, + &VisualShaderNodeVec4ConstantEmbedWidget::on_x_text_changed); + QObject::connect(y_edit_widget, &QLineEdit::textChanged, this, + &VisualShaderNodeVec4ConstantEmbedWidget::on_y_text_changed); + QObject::connect(z_edit_widget, &QLineEdit::textChanged, this, + &VisualShaderNodeVec4ConstantEmbedWidget::on_z_text_changed); + QObject::connect(w_edit_widget, &QLineEdit::textChanged, this, + &VisualShaderNodeVec4ConstantEmbedWidget::on_w_text_changed); + + addWidget(x_edit_widget); + addWidget(y_edit_widget); + addWidget(z_edit_widget); + addWidget(w_edit_widget); +} + +VisualShaderNodeVec4ConstantEmbedWidget::~VisualShaderNodeVec4ConstantEmbedWidget() {} + +void VisualShaderNodeVec4ConstantEmbedWidget::on_x_text_changed(const QString& text) { + if (text.isEmpty()) { + node->set_constant({0.0f, node->get_constant().y, node->get_constant().z, node->get_constant().w}); + } else { + bool ok; + text.toFloat(&ok); + if (ok) { + node->set_constant({text.toFloat(), node->get_constant().y, node->get_constant().z, node->get_constant().w}); + } else { + node->set_constant({0.0f, node->get_constant().y, node->get_constant().z, node->get_constant().w}); + x_edit_widget->setText(""); + } + } +} + +void VisualShaderNodeVec4ConstantEmbedWidget::on_y_text_changed(const QString& text) { + if (text.isEmpty()) { + node->set_constant({node->get_constant().x, 0.0f, node->get_constant().z, node->get_constant().w}); + } else { + bool ok; + text.toFloat(&ok); + if (ok) { + node->set_constant({node->get_constant().x, text.toFloat(), node->get_constant().z, node->get_constant().w}); + } else { + node->set_constant({node->get_constant().x, 0.0f, node->get_constant().z, node->get_constant().w}); + y_edit_widget->setText(""); + } + } +} + +void VisualShaderNodeVec4ConstantEmbedWidget::on_z_text_changed(const QString& text) { + if (text.isEmpty()) { + node->set_constant({node->get_constant().x, node->get_constant().y, 0.0f, node->get_constant().w}); + } else { + bool ok; + text.toFloat(&ok); + if (ok) { + node->set_constant({node->get_constant().x, node->get_constant().y, text.toFloat(), node->get_constant().w}); + } else { + node->set_constant({node->get_constant().x, node->get_constant().y, 0.0f, node->get_constant().w}); + z_edit_widget->setText(""); + } + } +} + +void VisualShaderNodeVec4ConstantEmbedWidget::on_w_text_changed(const QString& text) { + if (text.isEmpty()) { + node->set_constant({node->get_constant().x, node->get_constant().y, node->get_constant().z, 0.0f}); + } else { + bool ok; + text.toFloat(&ok); + if (ok) { + node->set_constant({node->get_constant().x, node->get_constant().y, node->get_constant().z, text.toFloat()}); + } else { + node->set_constant({node->get_constant().x, node->get_constant().y, node->get_constant().z, 0.0f}); + w_edit_widget->setText(""); + } + } +} + +/*************************************/ +/* Derivative Func Node */ +/*************************************/ + +VisualShaderNodeDerivativeFuncEmbedWidget::VisualShaderNodeDerivativeFuncEmbedWidget( + const std::shared_ptr& node) + : QVBoxLayout(), node(node) { + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + setSizeConstraint(QLayout::SetNoConstraint); + setSpacing(2); + setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); + + op_type_combo_box = new QComboBox(); + function_combo_box = new QComboBox(); + precision_combo_box = new QComboBox(); + + op_type_combo_box->addItem("Scalar", (int)VisualShaderNodeDerivativeFunc::OP_TYPE_SCALAR); + op_type_combo_box->addItem("Vector 2D", (int)VisualShaderNodeDerivativeFunc::OP_TYPE_VECTOR_2D); + op_type_combo_box->addItem("Vector 3D", (int)VisualShaderNodeDerivativeFunc::OP_TYPE_VECTOR_3D); + op_type_combo_box->addItem("Vector 4D", (int)VisualShaderNodeDerivativeFunc::OP_TYPE_VECTOR_4D); + + op_type_combo_box->setCurrentIndex((int)node->get_op_type()); + + function_combo_box->addItem("Sum", (int)VisualShaderNodeDerivativeFunc::FUNC_SUM); + function_combo_box->addItem("X", (int)VisualShaderNodeDerivativeFunc::FUNC_X); + function_combo_box->addItem("Y", (int)VisualShaderNodeDerivativeFunc::FUNC_Y); + + function_combo_box->setCurrentIndex((int)node->get_function()); + + precision_combo_box->addItem("None", (int)VisualShaderNodeDerivativeFunc::PRECISION_NONE); + precision_combo_box->addItem("Coarse", (int)VisualShaderNodeDerivativeFunc::PRECISION_COARSE); + precision_combo_box->addItem("Fine", (int)VisualShaderNodeDerivativeFunc::PRECISION_FINE); + + precision_combo_box->setCurrentIndex((int)node->get_precision()); + + QObject::connect(op_type_combo_box, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeDerivativeFuncEmbedWidget::on_op_type_current_index_changed); + QObject::connect(function_combo_box, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeDerivativeFuncEmbedWidget::on_function_current_index_changed); + QObject::connect(precision_combo_box, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeDerivativeFuncEmbedWidget::on_precision_current_index_changed); + + addWidget(op_type_combo_box); + addWidget(function_combo_box); + addWidget(precision_combo_box); +} + +VisualShaderNodeDerivativeFuncEmbedWidget::~VisualShaderNodeDerivativeFuncEmbedWidget() {} + +void VisualShaderNodeDerivativeFuncEmbedWidget::on_op_type_current_index_changed(const int& index) { + node->set_op_type((VisualShaderNodeDerivativeFunc::OpType)op_type_combo_box->itemData(index).toInt()); +} + +void VisualShaderNodeDerivativeFuncEmbedWidget::on_function_current_index_changed(const int& index) { + node->set_function((VisualShaderNodeDerivativeFunc::Function)function_combo_box->itemData(index).toInt()); +} + +void VisualShaderNodeDerivativeFuncEmbedWidget::on_precision_current_index_changed(const int& index) { + node->set_precision((VisualShaderNodeDerivativeFunc::Precision)precision_combo_box->itemData(index).toInt()); +} + +/*************************************/ +/* Value Noise Node */ +/*************************************/ + +VisualShaderNodeValueNoiseEmbedWidget::VisualShaderNodeValueNoiseEmbedWidget( + const std::shared_ptr& node) + : QLineEdit(), node(node) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + setPlaceholderText("Scale"); + + setText(QString::number(node->get_scale())); + + QObject::connect(this, &QLineEdit::textChanged, this, &VisualShaderNodeValueNoiseEmbedWidget::on_text_changed); +} + +VisualShaderNodeValueNoiseEmbedWidget::~VisualShaderNodeValueNoiseEmbedWidget() {} + +void VisualShaderNodeValueNoiseEmbedWidget::on_text_changed(const QString& text) { + if (text.isEmpty()) { + node->set_scale(100.0f); + } else { + bool ok; + text.toFloat(&ok); + if (ok) { + node->set_scale(text.toFloat()); + } else { + node->set_scale(0.0f); + setText(""); + } + } +} + +/*************************************/ +/* Perlin Noise Node */ +/*************************************/ + +VisualShaderNodePerlinNoiseEmbedWidget::VisualShaderNodePerlinNoiseEmbedWidget( + const std::shared_ptr& node) + : QLineEdit(), node(node) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + setPlaceholderText("Scale"); + + setText(QString::number(node->get_scale())); + + QObject::connect(this, &QLineEdit::textChanged, this, &VisualShaderNodePerlinNoiseEmbedWidget::on_text_changed); +} + +VisualShaderNodePerlinNoiseEmbedWidget::~VisualShaderNodePerlinNoiseEmbedWidget() {} + +void VisualShaderNodePerlinNoiseEmbedWidget::on_text_changed(const QString& text) { + if (text.isEmpty()) { + node->set_scale(10.0f); + } else { + bool ok; + text.toFloat(&ok); + if (ok) { + node->set_scale(text.toFloat()); + } else { + node->set_scale(0.0f); + setText(""); + } + } +} + +/*************************************/ +/* Voronoi Noise Node */ +/*************************************/ + +VisualShaderNodeVoronoiNoiseAngleOffsetEmbedWidget::VisualShaderNodeVoronoiNoiseAngleOffsetEmbedWidget( + const std::shared_ptr& node) + : QLineEdit(), node(node) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + setPlaceholderText("Angle Offset"); + + setText(QString::number(node->get_angle_offset())); + + QObject::connect(this, &QLineEdit::textChanged, this, + &VisualShaderNodeVoronoiNoiseAngleOffsetEmbedWidget::on_text_changed); +} + +VisualShaderNodeVoronoiNoiseAngleOffsetEmbedWidget::~VisualShaderNodeVoronoiNoiseAngleOffsetEmbedWidget() {} + +void VisualShaderNodeVoronoiNoiseAngleOffsetEmbedWidget::on_text_changed(const QString& text) { + if (text.isEmpty()) { + node->set_angle_offset(10.0f); + } else { + bool ok; + text.toFloat(&ok); + if (ok) { + node->set_angle_offset(text.toFloat()); + } else { + node->set_angle_offset(0.0f); + setText(""); + } + } +} + +VisualShaderNodeVoronoiNoiseCellDensityEmbedWidget::VisualShaderNodeVoronoiNoiseCellDensityEmbedWidget( + const std::shared_ptr& node) + : QLineEdit(), node(node) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + setPlaceholderText("Cell Density"); + + setText(QString::number(node->get_cell_density())); + + QObject::connect(this, &QLineEdit::textChanged, this, + &VisualShaderNodeVoronoiNoiseCellDensityEmbedWidget::on_text_changed); +} + +VisualShaderNodeVoronoiNoiseCellDensityEmbedWidget::~VisualShaderNodeVoronoiNoiseCellDensityEmbedWidget() {} + +void VisualShaderNodeVoronoiNoiseCellDensityEmbedWidget::on_text_changed(const QString& text) { + if (text.isEmpty()) { + node->set_cell_density(10.0f); + } else { + bool ok; + text.toFloat(&ok); + if (ok) { + node->set_cell_density(text.toFloat()); + } else { + node->set_cell_density(0.0f); + setText(""); + } + } +} + +/*************************************/ +/* Logic */ +/*************************************/ + +/*************************************/ +/* Compare Node */ +/*************************************/ + +VisualShaderNodeCompareEmbedWidget::VisualShaderNodeCompareEmbedWidget( + const std::shared_ptr& node) + : QVBoxLayout(), node(node) { + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + setSizeConstraint(QLayout::SetNoConstraint); + setSpacing(2); + setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); + + comparison_type_combo_box = new QComboBox(); + func_combo_box = new QComboBox(); + condition_combo_box = new QComboBox(); + + comparison_type_combo_box->addItem("Scalar", (int)VisualShaderNodeCompare::CMP_TYPE_SCALAR); + comparison_type_combo_box->addItem("Scalar Int", (int)VisualShaderNodeCompare::CMP_TYPE_SCALAR_INT); + comparison_type_combo_box->addItem("Scalar UInt", (int)VisualShaderNodeCompare::CMP_TYPE_SCALAR_UINT); + comparison_type_combo_box->addItem("Vector 2D", (int)VisualShaderNodeCompare::CMP_TYPE_VECTOR_2D); + comparison_type_combo_box->addItem("Vector 3D", (int)VisualShaderNodeCompare::CMP_TYPE_VECTOR_3D); + comparison_type_combo_box->addItem("Vector 4D", (int)VisualShaderNodeCompare::CMP_TYPE_VECTOR_4D); + comparison_type_combo_box->addItem("Boolean", (int)VisualShaderNodeCompare::CMP_TYPE_BOOLEAN); + + comparison_type_combo_box->setCurrentIndex((int)node->get_comparison_type()); + + func_combo_box->addItem("==", (int)VisualShaderNodeCompare::FUNC_EQUAL); + func_combo_box->addItem("!=", (int)VisualShaderNodeCompare::FUNC_NOT_EQUAL); + func_combo_box->addItem(">", (int)VisualShaderNodeCompare::FUNC_GREATER_THAN); + func_combo_box->addItem(">=", (int)VisualShaderNodeCompare::FUNC_GREATER_THAN_EQUAL); + func_combo_box->addItem("<", (int)VisualShaderNodeCompare::FUNC_LESS_THAN); + func_combo_box->addItem("<=", (int)VisualShaderNodeCompare::FUNC_LESS_THAN_EQUAL); + + func_combo_box->setCurrentIndex((int)node->get_function()); + + condition_combo_box->addItem("All", (int)VisualShaderNodeCompare::COND_ALL); + condition_combo_box->addItem("Any", (int)VisualShaderNodeCompare::COND_ANY); + + condition_combo_box->setCurrentIndex((int)node->get_condition()); + + addWidget(comparison_type_combo_box); + addWidget(func_combo_box); + addWidget(condition_combo_box); +} + +VisualShaderNodeCompareEmbedWidget::~VisualShaderNodeCompareEmbedWidget() {} + +void VisualShaderNodeCompareEmbedWidget::on_comparison_type_current_index_changed(const int& index) { + node->set_comparison_type( + (VisualShaderNodeCompare::ComparisonType)comparison_type_combo_box->itemData(index).toInt()); +} +void VisualShaderNodeCompareEmbedWidget::on_func_current_index_changed(const int& index) { + node->set_function((VisualShaderNodeCompare::Function)func_combo_box->itemData(index).toInt()); +} +void VisualShaderNodeCompareEmbedWidget::on_condition_current_index_changed(const int& index) { + node->set_condition((VisualShaderNodeCompare::Condition)condition_combo_box->itemData(index).toInt()); +} + +/*************************************/ +/* Switch Node */ +/*************************************/ + +VisualShaderNodeSwitchEmbedWidget::VisualShaderNodeSwitchEmbedWidget( + const std::shared_ptr& node) + : QComboBox(), node(node) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + addItem("Float", (int)VisualShaderNodeSwitch::OP_TYPE_FLOAT); + addItem("Int", (int)VisualShaderNodeSwitch::OP_TYPE_INT); + addItem("UInt", (int)VisualShaderNodeSwitch::OP_TYPE_UINT); + addItem("Vector 2D", (int)VisualShaderNodeSwitch::OP_TYPE_VECTOR_2D); + addItem("Vector 3D", (int)VisualShaderNodeSwitch::OP_TYPE_VECTOR_3D); + addItem("Vector 4D", (int)VisualShaderNodeSwitch::OP_TYPE_VECTOR_4D); + addItem("Boolean", (int)VisualShaderNodeSwitch::OP_TYPE_BOOLEAN); + + setCurrentIndex((int)node->get_op_type()); + + QObject::connect(this, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeSwitchEmbedWidget::on_current_index_changed); +} + +VisualShaderNodeSwitchEmbedWidget::~VisualShaderNodeSwitchEmbedWidget() {} + +void VisualShaderNodeSwitchEmbedWidget::on_current_index_changed(const int& index) { + node->set_op_type((VisualShaderNodeSwitch::OpType)itemData(index).toInt()); +} + +/*************************************/ +/* Is Node */ +/*************************************/ + +VisualShaderNodeIsEmbedWidget::VisualShaderNodeIsEmbedWidget(const std::shared_ptr& node) + : QComboBox(), node(node) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setContentsMargins(0, 0, 0, 0); // Left, top, right, bottom + + addItem("Is Inf", (int)VisualShaderNodeIs::FUNC_IS_INF); + addItem("Is NaN", (int)VisualShaderNodeIs::FUNC_IS_NAN); + + setCurrentIndex((int)node->get_function()); + + QObject::connect(this, QOverload::of(&QComboBox::currentIndexChanged), this, + &VisualShaderNodeIsEmbedWidget::on_current_index_changed); +} + +VisualShaderNodeIsEmbedWidget::~VisualShaderNodeIsEmbedWidget() {} + +void VisualShaderNodeIsEmbedWidget::on_current_index_changed(const int& index) { + node->set_function((VisualShaderNodeIs::Function)itemData(index).toInt()); +} diff --git a/Editors/VisualShaderEditor.h b/Editors/VisualShaderEditor.h new file mode 100644 index 000000000..71b5d2480 --- /dev/null +++ b/Editors/VisualShaderEditor.h @@ -0,0 +1,1516 @@ +/*********************************************************************************/ +/* */ +/* Copyright (C) 2024 Saif Kandil (k0T0z) */ +/* */ +/* This file is a part of the ENIGMA Development Environment. */ +/* */ +/* */ +/* ENIGMA is free software: you can redistribute it and/or modify it under the */ +/* terms of the GNU General Public License as published by the Free Software */ +/* Foundation, version 3 of the license or any later version. */ +/* */ +/* This application and its source code is distributed AS-IS, WITHOUT ANY */ +/* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS */ +/* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more */ +/* details. */ +/* */ +/* You should have recieved a copy of the GNU General Public License along */ +/* with this code. If not, see */ +/* */ +/* ENIGMA is an environment designed to create games and other programs with a */ +/* high-level, fully compilable language. Developers of ENIGMA or anything */ +/* associated with ENIGMA are in no way responsible for its users or */ +/* applications created by its users, or damages caused by the environment */ +/* or programs made in the environment. */ +/* */ +/*********************************************************************************/ + +#ifndef ENIGMA_VISUAL_SHADER_EDITOR_H +#define ENIGMA_VISUAL_SHADER_EDITOR_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// #include +#include +#include +#include +#include // https://stackoverflow.com/a/64288966/14629018 explains why we need this. +#include + +#include +#include + +#include "Editors/BaseEditor.h" +#include "ResourceTransformations/VisualShader/visual_shader.h" +#include "ResourceTransformations/VisualShader/visual_shader_nodes.h" +#include "ResourceTransformations/VisualShader/vs_noise_nodes.h" + +class VisualShaderGraphicsScene; +class VisualShaderGraphicsView; +class VisualShaderNodeGraphicsObject; +class VisualShaderConnectionGraphicsObject; +class CreateNodeDialog; +class VisualShaderInputPortGraphicsObject; +class VisualShaderOutputPortGraphicsObject; + +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ +/***** *****/ +/***** VisualShaderEditor *****/ +/***** *****/ +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ + +// Add const to any function that does not modify the object. + +class VisualShaderEditor : public BaseEditor { + Q_OBJECT + + public: + /** + * @brief This constructor is meant to be used for testing purposes. As + * it doesn't require a MessageModel object. + * + * @param parent + */ + VisualShaderEditor(QWidget* parent = nullptr); + VisualShaderEditor(MessageModel* model, QWidget* parent = nullptr); + ~VisualShaderEditor() override; + + VisualShaderGraphicsScene* get_scene() const { return scene; } + VisualShaderGraphicsView* get_view() const { return view; } + + Q_SIGNALS: + /** + * @brief Request the dialog that has all kinds of nodes we can + * create. + * + * @note This signal is emitted from two sources: + * @c VisualShaderEditor::on_create_node_button_pressed and + * @c VisualShaderGraphicsView::on_create_node_action_triggered slots + * and it is connected to the @c VisualShaderEditor::show_create_node_dialog + * function. + * + * @param coordinate + */ + void create_node_dialog_requested(const QPointF& coordinate); + + private Q_SLOTS: + /** + * @brief Called when @c VisualShaderEditor::create_node_button is pressed. + * + * @note Connected in @c VisualShaderEditor::init function + * to @c QPushButton::pressed signal. + * + * @note EMITS @c VisualShaderEditor::create_node_dialog_requested signal. + * + */ + void on_create_node_button_pressed(); + void on_preview_shader_button_pressed(); + + void on_menu_button_pressed(); + void on_load_image_button_pressed(); + void on_match_image_button_pressed(); + + private: + VisualShader* visual_shader; + MessageModel* visual_shader_model; + + QHBoxLayout* layout; + + QWidget* side_widget; + QVBoxLayout* side_outer_layout; + QVBoxLayout* side_layout; + QLineEdit* name_edit; + QPushButton* save_button; + + QHBoxLayout* scene_layer_layout; + QWidget* scene_layer; // Layer having the scene. + VisualShaderGraphicsScene* scene; + VisualShaderGraphicsView* view; + + QWidget* top_layer; // Layer having the menu bar. + QHBoxLayout* menu_bar; + + QPushButton* menu_button; + QPushButton* create_node_button; + QPushButton* preview_shader_button; + QPushButton* zoom_in_button; + QPushButton* reset_zoom_button; + QPushButton* zoom_out_button; + + QPushButton* load_image_button; + QPushButton* match_image_button; + + QAction* create_node_action; + + //////////////////////////////////// + // Code Previewer + //////////////////////////////////// + + QDialog* code_previewer_dialog; + QVBoxLayout* code_previewer_layout; + QPlainTextEdit* code_previewer; + + //////////////////////////////////// + // CreateNodeDialog Nodes Tree + //////////////////////////////////// + + struct CreateNodeDialogNodesTreeItem { + std::string name; + std::string category_path; + std::string type; + std::string description; + + CreateNodeDialogNodesTreeItem(const std::string& name = std::string(), + const std::string& category_path = std::string(), + const std::string& type = std::string(), + const std::string& description = std::string()) + : name(name), category_path(category_path), type(type), description(description) {} + }; + + static const VisualShaderEditor::CreateNodeDialogNodesTreeItem create_node_dialog_nodes_tree_items[]; + + CreateNodeDialog* create_node_dialog; + + /** + * @brief Initializes the UI + * + * @note To be called from different constructors. This function shouldn't contain + * any code related to MessageModel class as this will break the tests. + * + */ + void init(); + + /** + * @brief The VisualShader class may have some nodes at the beginning. This function + * is meant to add those nodes to the scene. + * + */ + void init_graph(); + + void create_node(const QPointF& coordinate); + + void add_node(QTreeWidgetItem* selected_item, const QPointF& coordinate); + + void show_create_node_dialog(const QPointF& coordinate); + + std::vector parse_node_category_path(const std::string& node_category_path); + QTreeWidgetItem* find_or_create_category_item(QTreeWidgetItem* parent, const std::string& category, + const std::string& category_path, + QTreeWidget* create_node_dialog_nodes_tree, + std::unordered_map& category_path_map); +}; + +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ +/***** *****/ +/***** CreateNodeDialog *****/ +/***** *****/ +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ + +class CreateNodeDialog : public QDialog { + Q_OBJECT + + public: + CreateNodeDialog(QWidget* parent = nullptr); + ~CreateNodeDialog(); + + QTreeWidget* get_nodes_tree() const { return create_node_dialog_nodes_tree; } + + QTreeWidgetItem* get_selected_item() const { return selected_item; } + + private Q_SLOTS: + void on_create_node_button_pressed(); + void on_cancel_node_creation_button_pressed(); + + void update_selected_item(); + + private: + QVBoxLayout* layout; + + QVBoxLayout* create_node_dialog_nodes_tree_layout; + + QTreeWidget* create_node_dialog_nodes_tree; + QTextEdit* create_node_dialog_nodes_description; + + QHBoxLayout* buttons_layout; + QPushButton* create_button; + QPushButton* cancel_button; + + QTreeWidgetItem* selected_item; +}; + +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ +/***** *****/ +/***** OriginalMatchingImageWidget *****/ +/***** *****/ +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ + +class OriginalMatchingImageWidget : public QWidget { + public: + OriginalMatchingImageWidget(QWidget* parent = nullptr) : QWidget(parent) { + pixmap = QPixmap(size()); + pixmap.fill(Qt::red); // Fill it with the red color + } + + protected: + // Override the paintEvent to display the pixmap + void paintEvent(QPaintEvent* event) override { + QPainter painter(this); + painter.drawPixmap(0, 0, pixmap); // Draw the pixmap starting at (0, 0) + } + + private: + QPixmap pixmap; +}; + +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ +/***** *****/ +/***** ShaderPreviewerWidget *****/ +/***** *****/ +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ + +/** + * @brief This class is meant to be a temporary solution to preview the shader + * code. We should preview the shader code using ENIGMA's Graphics System. + * + * @todo Replace this class with ENIGMA's Graphics System. + * + */ +class ShaderPreviewerWidget : public QOpenGLWidget { + Q_OBJECT + + public: + ShaderPreviewerWidget(QWidget* parent = nullptr); + ~ShaderPreviewerWidget() override; + + void set_code(const std::string& code); + + Q_SIGNALS: + void scene_update_requested(); + + protected: + void initializeGL() override; + void resizeGL(int w, int h) override; + void paintGL() override; + + void showEvent(QShowEvent* event) override; + void hideEvent(QHideEvent* event) override; + + private: + std::unique_ptr shader_program; + GLuint VAO, VBO; + QElapsedTimer timer; + + std::string code; + bool shader_needs_update{false}; + + void init_shaders(); + void init_buffers(); + void update_shader_program(); + + /** + * @brief Cleans up the OpenGL resources. + * + * @note This function is called automatically when the widget is destroyed. + * It is connected @c QOpenGLContext::aboutToBeDestroyed signal. + * + * @note DON'T call this function in the destructor as it is + * called automatically. If you call it from the destructor, + * it will crash as @c makeCurrent() won't be able to make the + * context current. + * + */ + void cleanup(); +}; + +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ +/***** *****/ +/***** VisualShaderGraphicsScene *****/ +/***** *****/ +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ + +class VisualShaderGraphicsScene : public QGraphicsScene { + Q_OBJECT + + public: + VisualShaderGraphicsScene(VisualShader* vs, QObject* parent = nullptr); + + ~VisualShaderGraphicsScene(); + + bool add_node(const std::string& type, const QPointF& coordinate); + bool add_node(const int& n_id, const std::shared_ptr& n, const QPointF& coordinate); + bool delete_node(const int& n_id); + + VisualShaderEditor* get_editor() const { return editor; } + void set_editor(VisualShaderEditor* editor) const { this->editor = editor; } + + /** + * @brief + * + * @note This function sets the @c temporary_connection_graphics_object if + * we have a valid @c from_node_id and @c from_port_index only. Then it + * resets it again if we have a valid @c to_node_id and @c to_port_index and + * this is important because inside the drag and drop event, we need to know + * if we have a valid temporary connection or not. + * + * @param from_node_id + * @param from_port_index + * @param to_node_id + * @param to_port_index + * @return true + * @return false + */ + bool add_connection(const int& from_node_id, const int& from_port_index, + const int& to_node_id = (int)VisualShader::NODE_ID_INVALID, + const int& to_port_index = (int)VisualShader::PORT_INDEX_INVALID); + + bool delete_connection(const int& from_node_id, const int& from_port_index, + const int& to_node_id = (int)VisualShader::NODE_ID_INVALID, + const int& to_port_index = (int)VisualShader::PORT_INDEX_INVALID); + + VisualShaderNodeGraphicsObject* get_node_graphics_object(const int& n_id) const; + + private Q_SLOTS: + /** + * @brief Called when an interaction with a port is made. + * + * @note Connected in @c VisualShaderNodeGraphicsObject::paint function + * to @c VisualShaderInputPortGraphicsObject::port_* signals. + * + * @param port + * @param coordinate + */ + void on_port_pressed(QGraphicsObject* port, const QPointF& coordinate); + void on_port_dragged(QGraphicsObject* port, const QPointF& coordinate); + void on_port_dropped(QGraphicsObject* port, const QPointF& coordinate); + + /** + * @brief Called when a node is moved. + * + * @note Connected in @c VisualShaderGraphicsScene::VisualShaderGraphicsScene constructor + * to @c VisualShaderGraphicsScene::node_moved signal. + * + * @param n_id + * @param new_coordinate + */ + void on_node_moved(const int& n_id, const QPointF& new_coordinate); + + /** + * @brief Called when a delete node action is triggered. + * + * @note Connected in @c VisualShaderGraphicsScene::add_node function + * to @c VisualShaderNodeGraphicsObject::node_deleted signal. + * + * @param n_id + */ + void on_node_deleted(const int& n_id); + + /** + * @brief Updates the code inside all the nodes except the output node. + * + */ + void on_update_shader_previewer_widgets_requested(); + + void on_scene_update_requested(); + + void on_in_port_remove_requested(VisualShaderInputPortGraphicsObject* in_port); + void on_out_port_remove_requested(VisualShaderOutputPortGraphicsObject* out_port); + + private: + VisualShader* vs; + mutable VisualShaderEditor* editor; + + std::unordered_map node_graphics_objects; + + VisualShaderConnectionGraphicsObject* temporary_connection_graphics_object; + + void remove_item(QGraphicsItem* item); +}; + +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ +/***** *****/ +/***** VisualShaderGraphicsView *****/ +/***** *****/ +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ + +class VisualShaderGraphicsView : public QGraphicsView { + Q_OBJECT + + public: + VisualShaderGraphicsView(VisualShaderGraphicsScene* scene, QWidget* parent = nullptr); + + ~VisualShaderGraphicsView(); + + float get_x() const { return rect_x; } + float get_y() const { return rect_y; } + float get_width() const { return rect_width; } + float get_height() const { return rect_height; } + + public Q_SLOTS: + /** + * @todo If the button is pressed then zoom in from the center of the view. + */ + void zoom_in(); + void reset_zoom(); + void zoom_out(); + + private Q_SLOTS: + /** + * @brief Called when @c VisualShaderGraphicsView::create_node_action is triggered. + * + * @note Connected in @c VisualShaderGraphicsView::VisualShaderGraphicsView constructor + * to @c QAction::triggered signal. + * + * @note EMITS @c VisualShaderEditor::create_node_dialog_requested signal. + * + */ + void on_create_node_action_triggered(); + + Q_SIGNALS: + void zoom_changed(const float& zoom); + + private: + VisualShaderGraphicsScene* scene; + + // Style + QColor background_color = QColor(40, 40, 40); // Dark Charcoal + QColor fine_grid_color = QColor(50, 50, 50); // Soft Dark Gray + QColor coarse_grid_color = QColor(30, 30, 30); // Muted Deep Gray + + // Scene Rect + float t_size = std::numeric_limits::max(); // 32767 + float rect_x = -1.0f * t_size * 0.5f; + float rect_y = -1.0f * t_size * 0.5f; + float rect_width = t_size; + float rect_height = t_size; + + float fit_in_view_margin = 50.0f; + + // Zoom + float zoom = 1.0f; + float zoom_step = 1.2f; + float zoom_min; + float zoom_max; + + QMenu* context_menu; + QPointF last_context_menu_coordinate = {0, 0}; + QAction* create_node_action; + + QAction* zoom_in_action; + QAction* reset_zoom_action; + QAction* zoom_out_action; + + QPointF last_click_coordinate; + + void drawBackground(QPainter* painter, const QRectF& r) override; + void contextMenuEvent(QContextMenuEvent* event) override; + void wheelEvent(QWheelEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + void showEvent(QShowEvent* event) override; + + void move_view_to_fit_items(); +}; + +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ +/***** *****/ +/***** VisualShaderNodeGraphicsObject *****/ +/***** *****/ +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ + +class VisualShaderNodeGraphicsObject : public QGraphicsObject { + Q_OBJECT + + public: + VisualShaderNodeGraphicsObject(const int& n_id, const QPointF& coordinate, + const std::shared_ptr& node, QGraphicsItem* parent = nullptr); + ~VisualShaderNodeGraphicsObject(); + + VisualShaderInputPortGraphicsObject* get_input_port_graphics_object(const int& p_index) const; + VisualShaderOutputPortGraphicsObject* get_output_port_graphics_object(const int& p_index) const; + + QWidget* get_embed_widget() const { return embed_widget; } + void set_embed_widget(QWidget* embed_widget) { this->embed_widget = embed_widget; } + + ShaderPreviewerWidget* get_shader_previewer_widget() const { return shader_previewer_widget; } + + public Q_SLOTS: + void on_node_update_requested(); + + Q_SIGNALS: + /** + * @brief Send a request to delete a node. + * + * @note EMITTED from @c VisualShaderNodeGraphicsObject::on_delete_node_action_triggered slot. + * + * @note Connected in @c VisualShaderGraphicsScene::add_node function to + * @c VisualShaderGraphicsScene::on_node_deleted slot. + * + * @param n_id + */ + void node_deleted(const int& n_id); + + /** + * @brief Notify the scene that a node has been moved. + * + * @note EMITTED from @c VisualShaderNodeGraphicsObject::itemChange function. + * + * @note Connected to @c VisualShaderGraphicsScene::on_node_moved slot in + * @c VisualShaderGraphicsScene::VisualShaderGraphicsScene constructor. + * + * @param n_id + * @param new_coordinate + */ + void node_moved(const int& n_id, const QPointF& new_coordinate); + + void in_port_pressed(VisualShaderInputPortGraphicsObject* port, const QPointF& coordinate); + void in_port_dragged(VisualShaderInputPortGraphicsObject* port, const QPointF& coordinate); + void in_port_dropped(VisualShaderInputPortGraphicsObject* port, const QPointF& coordinate); + + void out_port_pressed(VisualShaderOutputPortGraphicsObject* port, const QPointF& coordinate); + void out_port_dragged(VisualShaderOutputPortGraphicsObject* port, const QPointF& coordinate); + void out_port_dropped(VisualShaderOutputPortGraphicsObject* port, const QPointF& coordinate); + + void scene_update_requested(); + + void in_port_remove_requested(VisualShaderInputPortGraphicsObject* in_port); + void out_port_remove_requested(VisualShaderOutputPortGraphicsObject* out_port); + + private Q_SLOTS: + /** + * @brief Called when @c VisualShaderNodeGraphicsObject::delete_node_action is triggered. + * + * @note Connected in @c VisualShaderNodeGraphicsObject::VisualShaderNodeGraphicsObject constructor + * to @c QAction::triggered signal. + * + * @note EMITS @c VisualShaderNodeGraphicsObject::node_deleted signal. + * + */ + void on_delete_node_action_triggered(); + + void on_in_port_pressed(VisualShaderInputPortGraphicsObject* port, const QPointF& coordinate) { + Q_EMIT in_port_pressed(port, coordinate); + } + void on_in_port_dragged(VisualShaderInputPortGraphicsObject* port, const QPointF& coordinate) { + Q_EMIT in_port_dragged(port, coordinate); + } + void on_in_port_dropped(VisualShaderInputPortGraphicsObject* port, const QPointF& coordinate) { + Q_EMIT in_port_dropped(port, coordinate); + } + + void on_out_port_pressed(VisualShaderOutputPortGraphicsObject* port, const QPointF& coordinate) { + Q_EMIT out_port_pressed(port, coordinate); + } + void on_out_port_dragged(VisualShaderOutputPortGraphicsObject* port, const QPointF& coordinate) { + Q_EMIT out_port_dragged(port, coordinate); + } + void on_out_port_dropped(VisualShaderOutputPortGraphicsObject* port, const QPointF& coordinate) { + Q_EMIT out_port_dropped(port, coordinate); + } + + private: + int n_id; + QPointF coordinate; + std::shared_ptr node; + + QMenu* context_menu; + QAction* delete_node_action; + + std::unordered_map in_port_graphics_objects; + std::unordered_map out_port_graphics_objects; + + // Style + QColor normal_boundary_color = QColor(220, 20, 60); // Crimson Red + QColor selected_boundary_color = QColor(255, 69, 0); // Red-Orange + QColor font_color = QColor(255, 255, 255); // Pure White + QColor fill_color = QColor(40, 40, 40, 200); // Semi-transparent Dark Gray + + float pen_width = 1.0f; + + float opacity = 0.8f; + float corner_radius = 3.0f; + float port_diameter = 8.0f; + + float caption_h_padding = 10.0f; + float caption_v_padding = 8.0f; + + mutable float rect_width; // Calculated in boundingRect() + mutable float caption_rect_height; // Calculated in boundingRect() + + mutable float rect_height; // Calculated in boundingRect() + float body_rect_header_height = 30.0f; + float body_rect_port_step = 40.0f; + float body_rect_footer_height = 30.0f; + + mutable float rect_padding; // Calculated in boundingRect() + mutable float rect_margin; // Calculated in boundingRect() + + float port_caption_spacing = 4.0f; // Distance between the port and its caption + + // Ports Style + float connected_port_diameter = 8.0f; + float unconnected_port_diameter = 6.0f; + + // Caption + float caption_font_size = 18.0f; + float port_caption_font_size = 8.0f; + + QWidget* embed_widget; + float embed_widget_h_padding = 15.0f; + float embed_widget_v_padding = 5.0f; + + OriginalMatchingImageWidget* matching_image_widget; + float spacing_between_output_node_and_matching_image = 10.0f; + + ShaderPreviewerWidget* shader_previewer_widget; + float spacing_between_current_node_and_shader_previewer = 10.0f; + + QRectF boundingRect() const override; + + /** + * @brief + * + * @note This function contains some commented code that is meant to be used + * for debugging purposes. + * + * @param painter + * @param option + * @param widget + */ + void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget = nullptr) override; + QVariant itemChange(GraphicsItemChange change, const QVariant& value) override; + void contextMenuEvent(QGraphicsSceneContextMenuEvent* event) override; +}; + +class VisualShaderInputPortGraphicsObject : public QGraphicsObject { + Q_OBJECT + + public: + VisualShaderInputPortGraphicsObject(const QRectF& rect, const int& n_id, const int& p_index, + QGraphicsItem* parent = nullptr); + ~VisualShaderInputPortGraphicsObject(); + + QPointF get_global_coordinate() const { return mapToScene(rect.center()); } + + int get_node_id() const { return n_id; } + int get_port_index() const { return p_index; } + + VisualShaderConnectionGraphicsObject* get_connection_graphics_object() const { return connection_graphics_object; } + void connect(VisualShaderConnectionGraphicsObject* c_g_o) const { this->connection_graphics_object = c_g_o; } + void detach_connection() const { this->connection_graphics_object = nullptr; } + bool is_connected() const { return connection_graphics_object != nullptr; } + + Q_SIGNALS: + /** + * @brief Called when the an interaction with the port is made. + * + * @note Connected in @c VisualShaderNodeGraphicsObject::paint function to + * @c VisualShaderGraphicsScene::on_port_* slots. + * + * @param port + * @param coordinate + */ + void port_pressed(VisualShaderInputPortGraphicsObject* port, const QPointF& coordinate); + void port_dragged(VisualShaderInputPortGraphicsObject* port, const QPointF& coordinate); + void port_dropped(VisualShaderInputPortGraphicsObject* port, const QPointF& coordinate); + + private: + int n_id; + int p_index; + QRectF rect; + + mutable VisualShaderConnectionGraphicsObject* connection_graphics_object; + + float padding = 0.5f; + + // Style + QColor font_color = QColor(255, 255, 255); + QColor connection_point_color = QColor(220, 20, 60); // Crimson + + float opacity = 1.0f; + + QRectF boundingRect() const override; + void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override; + void mousePressEvent(QGraphicsSceneMouseEvent* event) override; + void mouseMoveEvent(QGraphicsSceneMouseEvent* event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent* event) override; +}; + +class VisualShaderOutputPortGraphicsObject : public QGraphicsObject { + Q_OBJECT + + public: + VisualShaderOutputPortGraphicsObject(const QRectF& rect, const int& n_id, const int& p_index, + QGraphicsItem* parent = nullptr); + ~VisualShaderOutputPortGraphicsObject(); + + QPointF get_global_coordinate() const { return mapToScene(rect.center()); } + + int get_node_id() const { return n_id; } + int get_port_index() const { return p_index; } + + std::vector get_connection_graphics_objects() const { + return connection_graphics_objects; + } + VisualShaderConnectionGraphicsObject* get_connection_graphics_object(const int& to_node_id, + const int& to_port_index) const; + void connect(VisualShaderConnectionGraphicsObject* c_o) { this->connection_graphics_objects.emplace_back(c_o); } + void detach_connection(VisualShaderConnectionGraphicsObject* c_o) { + connection_graphics_objects.erase( + std::remove(connection_graphics_objects.begin(), connection_graphics_objects.end(), c_o), + connection_graphics_objects.end()); + } + bool is_connected() const { return connection_graphics_objects.size() > 0; } + + Q_SIGNALS: + /** + * @brief Called when the an interaction with the port is made. + * + * @note Connected in @c VisualShaderNodeGraphicsObject::paint function to + * @c VisualShaderGraphicsScene::on_port_* slots. + * + * @param port + * @param coordinate + */ + void port_pressed(VisualShaderOutputPortGraphicsObject* port, const QPointF& coordinate); + void port_dragged(VisualShaderOutputPortGraphicsObject* port, const QPointF& coordinate); + void port_dropped(VisualShaderOutputPortGraphicsObject* port, const QPointF& coordinate); + + private: + int n_id; + int p_index; + QRectF rect; + + // An output port can have multiple connections. + std::vector connection_graphics_objects; + + float padding = 0.5f; + + // Style + QColor font_color = QColor(255, 255, 255); + QColor connection_point_color = QColor(220, 20, 60); // Crimson + + float opacity = 1.0f; + + QRectF boundingRect() const override; + void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override; + void mousePressEvent(QGraphicsSceneMouseEvent* event) override; + void mouseMoveEvent(QGraphicsSceneMouseEvent* event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent* event) override; +}; + +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ +/***** *****/ +/***** VisualShaderConnectionGraphicsObject *****/ +/***** *****/ +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ + +class VisualShaderConnectionGraphicsObject : public QGraphicsObject { + Q_OBJECT + + public: + VisualShaderConnectionGraphicsObject(const int& from_n_id, const int& from_p_index, const QPointF& start_coordinate, + QGraphicsItem* parent = nullptr); + ~VisualShaderConnectionGraphicsObject(); + + int get_from_node_id() const { return from_n_id; } + int get_from_port_index() const { return from_p_index; } + + int get_to_node_id() const { return to_n_id; } + int get_to_port_index() const { return to_p_index; } + + void detach_end() const { + this->set_to_node_id((int)VisualShader::NODE_ID_INVALID); + this->set_to_port_index((int)VisualShader::PORT_INDEX_INVALID); + } + + void set_to_node_id(const int& to_n_id) const { this->to_n_id = to_n_id; } + void set_to_port_index(const int& to_p_index) const { this->to_p_index = to_p_index; } + + void set_start_coordinate(const QPointF& start_coordinate) { + this->start_coordinate = start_coordinate; + update(); + } + void set_end_coordinate(const QPointF& end_coordinate) { + this->end_coordinate = end_coordinate; + update(); + } + + private: + int from_n_id; + mutable int to_n_id; + int from_p_index; + mutable int to_p_index; + + QPointF start_coordinate; + QPointF end_coordinate; + + // Style + QColor construction_color = QColor(139, 0, 0); // Dark Red + QColor normal_color = QColor(178, 34, 34); // Firebrick Red + QColor selected_color = QColor(55, 55, 55); // Dark Gray + QColor connection_point_color = QColor(211, 211, 211); + + float line_width = 3.0f; + float construction_line_width = 2.0f; + float point_diameter = 10.0f; + + mutable float rect_padding; // Calculated in boundingRect() + + float h_abnormal_offset = 50.0f; + float v_abnormal_offset = 40.0f; + float abnormal_face_to_back_control_width_expansion_factor = 0.5f; + float abnormal_face_to_back_control_height_expansion_factor = 1.0f; + + QRectF boundingRect() const override; + void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget = nullptr) override; + + int detect_quadrant(const QPointF& reference, const QPointF& target) const; + QRectF calculate_bounding_rect_from_coordinates(const QPointF& start_coordinate, const QPointF& end_coordinate) const; + std::pair calculate_control_points(const QPointF& start_coordinate, + const QPointF& end_coordinate) const; +}; + +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ +/***** *****/ +/***** Embed Widgets *****/ +/***** *****/ +/**********************************************************************/ +/**********************************************************************/ +/**********************************************************************/ + +class VisualShaderNodeEmbedWidget : public QWidget { + Q_OBJECT + + public: + VisualShaderNodeEmbedWidget(const std::shared_ptr& node, QWidget* parent = nullptr); + ~VisualShaderNodeEmbedWidget(); + + void set_shader_previewer_widget(QWidget* shader_previewer_widget) { + this->shader_previewer_widget = shader_previewer_widget; + } + + Q_SIGNALS: + void shader_preview_update_requested(); + void node_update_requested(); + + private Q_SLOTS: + void on_preview_shader_button_pressed() { + bool is_visible{shader_previewer_widget->isVisible()}; + shader_previewer_widget->setVisible(!is_visible); + preview_shader_button->setText(!is_visible ? "Hide Preview" : "Show Preview"); + } + + void on_shader_preview_update_requested() { Q_EMIT shader_preview_update_requested(); } + + void on_node_update_requested() { Q_EMIT node_update_requested(); } + + private: + QVBoxLayout* layout; + + QPushButton* preview_shader_button; + + QWidget* shader_previewer_widget; +}; + +/*************************************/ +/* Input Node */ +/*************************************/ + +class VisualShaderNodeInputEmbedWidget : public QComboBox { + Q_OBJECT + + public: + VisualShaderNodeInputEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeInputEmbedWidget(); + + void set_current_index(const int& index) { this->setCurrentIndex(index); } + + private Q_SLOTS: + void on_current_index_changed(const int& index); + + private: + std::shared_ptr node; +}; + +/*************************************/ +/* Float Op Node */ +/*************************************/ + +class VisualShaderNodeFloatOpEmbedWidget : public QComboBox { + Q_OBJECT + + public: + VisualShaderNodeFloatOpEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeFloatOpEmbedWidget(); + + void set_current_index(const int& index) { this->setCurrentIndex(index); } + + private Q_SLOTS: + void on_current_index_changed(const int& index); + + private: + std::shared_ptr node; +}; + +/*************************************/ +/* Int Op Node */ +/*************************************/ + +class VisualShaderNodeIntOpEmbedWidget : public QComboBox { + Q_OBJECT + + public: + VisualShaderNodeIntOpEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeIntOpEmbedWidget(); + + void set_current_index(const int& index) { this->setCurrentIndex(index); } + + private Q_SLOTS: + void on_current_index_changed(const int& index); + + private: + std::shared_ptr node; +}; + +/*************************************/ +/* UInt Op Node */ +/*************************************/ + +class VisualShaderNodeUIntOpEmbedWidget : public QComboBox { + Q_OBJECT + + public: + VisualShaderNodeUIntOpEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeUIntOpEmbedWidget(); + + void set_current_index(const int& index) { this->setCurrentIndex(index); } + + private Q_SLOTS: + void on_current_index_changed(const int& index); + + private: + std::shared_ptr node; +}; + +/*************************************/ +/* Float Funcs Node */ +/*************************************/ + +class VisualShaderNodeFloatFuncEmbedWidget : public QComboBox { + Q_OBJECT + + public: + VisualShaderNodeFloatFuncEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeFloatFuncEmbedWidget(); + + void set_current_index(const int& index) { this->setCurrentIndex(index); } + + private Q_SLOTS: + void on_current_index_changed(const int& index); + + private: + std::shared_ptr node; +}; + +/*************************************/ +/* Int Funcs Node */ +/*************************************/ + +class VisualShaderNodeIntFuncEmbedWidget : public QComboBox { + Q_OBJECT + + public: + VisualShaderNodeIntFuncEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeIntFuncEmbedWidget(); + + void set_current_index(const int& index) { this->setCurrentIndex(index); } + + private Q_SLOTS: + void on_current_index_changed(const int& index); + + private: + std::shared_ptr node; +}; + +/*************************************/ +/* UInt Funcs Node */ +/*************************************/ + +class VisualShaderNodeUIntFuncEmbedWidget : public QComboBox { + Q_OBJECT + + public: + VisualShaderNodeUIntFuncEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeUIntFuncEmbedWidget(); + + void set_current_index(const int& index) { this->setCurrentIndex(index); } + + private Q_SLOTS: + void on_current_index_changed(const int& index); + + private: + std::shared_ptr node; +}; + +/*************************************/ +/* Vector Base */ +/*************************************/ + +class VisualShaderNodeVectorBaseEmbedWidget : public QComboBox { + Q_OBJECT + + public: + VisualShaderNodeVectorBaseEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeVectorBaseEmbedWidget(); + + void set_current_index(const int& index) { this->setCurrentIndex(index); } + + private Q_SLOTS: + void on_current_index_changed(const int& index); + + private: + std::shared_ptr node; +}; + +/*************************************/ +/* Vector Op Node */ +/*************************************/ + +class VisualShaderNodeVectorOpEmbedWidget : public QComboBox { + Q_OBJECT + + public: + VisualShaderNodeVectorOpEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeVectorOpEmbedWidget(); + + void set_current_index(const int& index) { this->setCurrentIndex(index); } + + private Q_SLOTS: + void on_current_index_changed(const int& index); + + private: + std::shared_ptr node; +}; + +/*************************************/ +/* Vector Funcs Node */ +/*************************************/ + +class VisualShaderNodeVectorFuncEmbedWidget : public QComboBox { + Q_OBJECT + + public: + VisualShaderNodeVectorFuncEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeVectorFuncEmbedWidget(); + + void set_current_index(const int& index) { this->setCurrentIndex(index); } + + private Q_SLOTS: + void on_current_index_changed(const int& index); + + private: + std::shared_ptr node; +}; + +/*************************************/ +/* Color Constant Node */ +/*************************************/ + +class VisualShaderNodeColorConstantEmbedWidget : public QPushButton { + Q_OBJECT + + public: + VisualShaderNodeColorConstantEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeColorConstantEmbedWidget(); + + Q_SIGNALS: + void color_changed(); + + private Q_SLOTS: + void on_pressed(); + + private: + std::shared_ptr node; +}; + +/*************************************/ +/* Boolean Constant Node */ +/*************************************/ + +class VisualShaderNodeBooleanConstantEmbedWidget : public QCheckBox { + Q_OBJECT + + public: + VisualShaderNodeBooleanConstantEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeBooleanConstantEmbedWidget(); + + private Q_SLOTS: + void on_state_changed(const int& state); + + private: + std::shared_ptr node; +}; + +/*************************************/ +/* Float Constant */ +/*************************************/ + +class VisualShaderNodeFloatConstantEmbedWidget : public QLineEdit { + Q_OBJECT + + public: + VisualShaderNodeFloatConstantEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeFloatConstantEmbedWidget(); + + private Q_SLOTS: + void on_text_changed(const QString& text); + + private: + std::shared_ptr node; +}; + +/*************************************/ +/* Int Constant */ +/*************************************/ + +class VisualShaderNodeIntConstantEmbedWidget : public QLineEdit { + Q_OBJECT + + public: + VisualShaderNodeIntConstantEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeIntConstantEmbedWidget(); + + private Q_SLOTS: + void on_text_changed(const QString& text); + + private: + std::shared_ptr node; +}; + +/*************************************/ +/* UInt Constant */ +/*************************************/ + +class VisualShaderNodeUIntConstantEmbedWidget : public QLineEdit { + Q_OBJECT + + public: + VisualShaderNodeUIntConstantEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeUIntConstantEmbedWidget(); + + private Q_SLOTS: + void on_text_changed(const QString& text); + + private: + std::shared_ptr node; +}; + +/*************************************/ +/* Vec2 Constant Node */ +/*************************************/ + +class VisualShaderNodeVec2ConstantEmbedWidget : public QVBoxLayout { + Q_OBJECT + + public: + VisualShaderNodeVec2ConstantEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeVec2ConstantEmbedWidget(); + + QLineEdit* get_x_edit_widget() const { return x_edit_widget; } + QLineEdit* get_y_edit_widget() const { return y_edit_widget; } + + private Q_SLOTS: + + void on_x_text_changed(const QString& text); + void on_y_text_changed(const QString& text); + + private: + std::shared_ptr node; + + QLineEdit* x_edit_widget; + QLineEdit* y_edit_widget; +}; + +/*************************************/ +/* Vec3 Constant Node */ +/*************************************/ + +class VisualShaderNodeVec3ConstantEmbedWidget : public QVBoxLayout { + Q_OBJECT + + public: + VisualShaderNodeVec3ConstantEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeVec3ConstantEmbedWidget(); + + QLineEdit* get_x_edit_widget() const { return x_edit_widget; } + QLineEdit* get_y_edit_widget() const { return y_edit_widget; } + QLineEdit* get_z_edit_widget() const { return z_edit_widget; } + + private Q_SLOTS: + + void on_x_text_changed(const QString& text); + void on_y_text_changed(const QString& text); + void on_z_text_changed(const QString& text); + + private: + std::shared_ptr node; + + QLineEdit* x_edit_widget; + QLineEdit* y_edit_widget; + QLineEdit* z_edit_widget; +}; + +/*************************************/ +/* Vec4 Constant Node */ +/*************************************/ + +class VisualShaderNodeVec4ConstantEmbedWidget : public QVBoxLayout { + Q_OBJECT + + public: + VisualShaderNodeVec4ConstantEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeVec4ConstantEmbedWidget(); + + QLineEdit* get_x_edit_widget() const { return x_edit_widget; } + QLineEdit* get_y_edit_widget() const { return y_edit_widget; } + QLineEdit* get_z_edit_widget() const { return z_edit_widget; } + QLineEdit* get_w_edit_widget() const { return w_edit_widget; } + + private Q_SLOTS: + + void on_x_text_changed(const QString& text); + void on_y_text_changed(const QString& text); + void on_z_text_changed(const QString& text); + void on_w_text_changed(const QString& text); + + private: + std::shared_ptr node; + + QLineEdit* x_edit_widget; + QLineEdit* y_edit_widget; + QLineEdit* z_edit_widget; + QLineEdit* w_edit_widget; +}; + +/*************************************/ +/* Derivative Func Node */ +/*************************************/ + +class VisualShaderNodeDerivativeFuncEmbedWidget : public QVBoxLayout { + Q_OBJECT + + public: + VisualShaderNodeDerivativeFuncEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeDerivativeFuncEmbedWidget(); + + QComboBox* get_op_type_combo_box() const { return op_type_combo_box; } + QComboBox* get_function_combo_box() const { return function_combo_box; } + QComboBox* get_precision_combo_box() const { return precision_combo_box; } + + private Q_SLOTS: + void on_op_type_current_index_changed(const int& index); + void on_function_current_index_changed(const int& index); + void on_precision_current_index_changed(const int& index); + + private: + std::shared_ptr node; + + QComboBox* op_type_combo_box; + QComboBox* function_combo_box; + QComboBox* precision_combo_box; +}; + +/*************************************/ +/* Value Noise Node */ +/*************************************/ + +class VisualShaderNodeValueNoiseEmbedWidget : public QLineEdit { + Q_OBJECT + + public: + VisualShaderNodeValueNoiseEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeValueNoiseEmbedWidget(); + + private Q_SLOTS: + void on_text_changed(const QString& text); + + private: + std::shared_ptr node; +}; + +/*************************************/ +/* Perlin Noise Node */ +/*************************************/ + +class VisualShaderNodePerlinNoiseEmbedWidget : public QLineEdit { + Q_OBJECT + + public: + VisualShaderNodePerlinNoiseEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodePerlinNoiseEmbedWidget(); + + private Q_SLOTS: + void on_text_changed(const QString& text); + + private: + std::shared_ptr node; +}; + +/*************************************/ +/* Voronoi Noise Node */ +/*************************************/ + +class VisualShaderNodeVoronoiNoiseAngleOffsetEmbedWidget : public QLineEdit { + Q_OBJECT + + public: + VisualShaderNodeVoronoiNoiseAngleOffsetEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeVoronoiNoiseAngleOffsetEmbedWidget(); + + private Q_SLOTS: + void on_text_changed(const QString& text); + + private: + std::shared_ptr node; +}; + +class VisualShaderNodeVoronoiNoiseCellDensityEmbedWidget : public QLineEdit { + Q_OBJECT + + public: + VisualShaderNodeVoronoiNoiseCellDensityEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeVoronoiNoiseCellDensityEmbedWidget(); + + private Q_SLOTS: + void on_text_changed(const QString& text); + + private: + std::shared_ptr node; +}; + +/*************************************/ +/* Logic */ +/*************************************/ + +/*************************************/ +/* Compare Node */ +/*************************************/ + +class VisualShaderNodeCompareEmbedWidget : public QVBoxLayout { + Q_OBJECT + + public: + VisualShaderNodeCompareEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeCompareEmbedWidget(); + + QComboBox* get_comparison_type_combo_box() const { return comparison_type_combo_box; } + QComboBox* get_func_combo_box() const { return func_combo_box; } + QComboBox* get_condition_combo_box() const { return condition_combo_box; } + + private Q_SLOTS: + void on_comparison_type_current_index_changed(const int& index); + void on_func_current_index_changed(const int& index); + void on_condition_current_index_changed(const int& index); + + private: + std::shared_ptr node; + + QComboBox* comparison_type_combo_box; + QComboBox* func_combo_box; + QComboBox* condition_combo_box; +}; + +/*************************************/ +/* Switch Node */ +/*************************************/ + +class VisualShaderNodeSwitchEmbedWidget : public QComboBox { + Q_OBJECT + + public: + VisualShaderNodeSwitchEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeSwitchEmbedWidget(); + + void set_current_index(const int& index) { this->setCurrentIndex(index); } + + private Q_SLOTS: + void on_current_index_changed(const int& index); + + private: + std::shared_ptr node; +}; + +/*************************************/ +/* Is Node */ +/*************************************/ + +class VisualShaderNodeIsEmbedWidget : public QComboBox { + Q_OBJECT + + public: + VisualShaderNodeIsEmbedWidget(const std::shared_ptr& node); + ~VisualShaderNodeIsEmbedWidget(); + + void set_current_index(const int& index) { this->setCurrentIndex(index); } + + private Q_SLOTS: + void on_current_index_changed(const int& index); + + private: + std::shared_ptr node; +}; + +#endif // ENIGMA_VISUAL_SHADER_EDITOR_H diff --git a/MainWindow.cpp b/MainWindow.cpp index 9a9ad6985..c80ababbe 100644 --- a/MainWindow.cpp +++ b/MainWindow.cpp @@ -12,6 +12,7 @@ #include "Editors/ScriptEditor.h" #include "Editors/SettingsEditor.h" #include "Editors/ShaderEditor.h" +#include "Editors/VisualShaderEditor.h" #include "Editors/SoundEditor.h" #include "Editors/SpriteEditor.h" #include "Editors/TimelineEditor.h" @@ -35,13 +36,13 @@ #undef GetMessage -QList MainWindow::EnigmaSearchPaths = {QDir::currentPath(), "./enigma-dev", "../enigma-dev", - "../RadialGM/Submodules/enigma-dev", "/opt/enigma-dev/", "/usr/lib/enigma-dev"}; +QList MainWindow::EnigmaSearchPaths = {"/opt/enigma-dev/", "/usr/lib/enigma-dev", ENIGMA_DIR}; QFileInfo MainWindow::EnigmaRoot = MainWindow::getEnigmaRoot(); QList MainWindow::systemCache; MainWindow *MainWindow::_instance = nullptr; ResourceModelMap *MainWindow::resourceMap = nullptr; TreeModel *MainWindow::treeModel = nullptr; +MessageModel *MainWindow::protoModel = nullptr; std::unique_ptr MainWindow::_event_data; static QTextEdit *diagnosticTextEdit = nullptr; @@ -168,15 +169,14 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), _ui(new Ui::MainW settingsButton->setToolButtonStyle(Qt::ToolButtonStyle::ToolButtonTextBesideIcon); _ui->actionSettings->setMenu(_ui->menuChangeGameSettings); + ///////////////////////////// + // Create the server plugin + ///////////////////////////// + RGMPlugin *pluginServer = new ServerPlugin(*this); auto outputTextBrowser = this->_ui->outputTextBrowser; connect(pluginServer, &RGMPlugin::LogOutput, outputTextBrowser, &QTextBrowser::append); - connect(pluginServer, &RGMPlugin::CompileStatusChanged, [=](bool finished) { - _ui->outputDockWidget->show(); - _ui->actionRun->setEnabled(finished); - _ui->actionDebug->setEnabled(finished); - _ui->actionCreateExecutable->setEnabled(finished); - }); + connect(pluginServer, &RGMPlugin::CompileStatusChanged, this, &MainWindow::on_compileStatusChanged); connect(this, &MainWindow::CurrentConfigChanged, pluginServer, &RGMPlugin::SetCurrentConfig); connect(_ui->actionRun, &QAction::triggered, pluginServer, &RGMPlugin::Run); connect(_ui->actionDebug, &QAction::triggered, pluginServer, &RGMPlugin::Debug); @@ -186,6 +186,11 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), _ui(new Ui::MainW } MainWindow::~MainWindow() { + if (protoModel) delete protoModel; + if (treeModel) delete treeModel; + if (resourceMap) delete resourceMap; + if (toggleDiagnosticsAction) delete toggleDiagnosticsAction; + // if (this->pluginServer) delete this->pluginServer; diagnosticTextEdit = nullptr; delete _ui; } @@ -300,7 +305,7 @@ void MainWindow::openNewProject() { auto newProject = std::make_unique(); auto *root = newProject->mutable_game()->mutable_root(); QList defaultGroups = {tr("Sprites"), tr("Sounds"), tr("Backgrounds"), tr("Paths"), - tr("Scripts"), tr("Shaders"), tr("Fonts"), tr("Timelines"), + tr("Scripts"), tr("Shaders"), tr("Visual Shaders"), tr("Fonts"), tr("Timelines"), tr("Objects"), tr("Rooms"), tr("Includes"), tr("Configs")}; // We can edit the proto directly, here, since the model doesn't exist, yet. for (const auto &groupName : defaultGroups) { @@ -371,6 +376,7 @@ void MainWindow::openProject(std::unique_ptr openedProject) { treeConf.UseEditorLauncher(Launch(this)); treeConf.UseEditorLauncher(Launch(this)); treeConf.UseEditorLauncher(Launch(this)); + treeConf.UseEditorLauncher(Launch(this)); treeConf.UseEditorLauncher(Launch(this)); treeConf.UseEditorLauncher(Launch(this)); treeConf.UseEditorLauncher(Launch(this)); @@ -385,6 +391,7 @@ void MainWindow::openProject(std::unique_ptr openedProject) { msgConf.SetDefaultIcon("path"); msgConf.SetDefaultIcon("script"); msgConf.SetDefaultIcon("shader"); + msgConf.SetDefaultIcon("visual_shader"); msgConf.SetDefaultIcon("font"); msgConf.SetDefaultIcon("timeline"); msgConf.SetDefaultIcon("object"); @@ -416,13 +423,14 @@ void MainWindow::openProject(std::unique_ptr openedProject) { treeConf.SetMessagePassthrough(); treeConf.DisableOneofReassignment(); - delete resourceMap; + if (resourceMap) delete resourceMap; resourceMap = new ResourceModelMap(this); - auto pm = new MessageModel(ProtoModel::NonProtoParent{this}, _project->mutable_game()->mutable_root()); + if (protoModel) delete protoModel; + protoModel = new MessageModel(ProtoModel::NonProtoParent{this}, _project->mutable_game()->mutable_root()); // Connect methods to auto update fields with extensions - connect(pm, &ProtoModel::ModelConstructed, [](ProtoModel *model) { + connect(protoModel, &ProtoModel::ModelConstructed, [](ProtoModel *model) { PrimitiveModel *primitive_model = model->TryCastAsPrimitiveModel(); if (primitive_model) { const FieldDescriptor *const field = primitive_model->GetFieldDescriptor(); @@ -437,13 +445,14 @@ void MainWindow::openProject(std::unique_ptr openedProject) { } }); - pm->RebuildSubModels(); + protoModel->RebuildSubModels(); + + protoModel->SetDisplayConfig(msgConf); - pm->SetDisplayConfig(msgConf); + resourceMap->TreeChanged(protoModel); - resourceMap->TreeChanged(pm); - delete treeModel; - treeModel = new TreeModel(pm, nullptr, treeConf); + if (treeModel) delete treeModel; + treeModel = new TreeModel(protoModel, nullptr, treeConf); _ui->treeView->setModel(treeModel); connect(treeModel, &TreeModel::ItemRenamed, resourceMap, @@ -451,7 +460,7 @@ void MainWindow::openProject(std::unique_ptr openedProject) { connect(treeModel, &TreeModel::TreeChanged, resourceMap, &ResourceModelMap::TreeChanged); connect(treeModel, &TreeModel::ItemRemoved, resourceMap, &ResourceModelMap::ResourceRemoved, Qt::DirectConnection); - connect(pm, &ProtoModel::dataChanged, resourceMap, &ResourceModelMap::dataChanged, + connect(protoModel, &ProtoModel::dataChanged, resourceMap, &ResourceModelMap::dataChanged, Qt::DirectConnection); connect(treeModel, &TreeModel::ModelAboutToBeDeleted, this, &MainWindow::ResourceModelDeleted, Qt::DirectConnection); @@ -639,6 +648,8 @@ void MainWindow::on_actionCreateScript_triggered() { CreateResource(TypeCase::kS void MainWindow::on_actionCreateShader_triggered() { CreateResource(TypeCase::kShader); } +void MainWindow::on_actionCreateVisualShader_triggered() { CreateResource(TypeCase::kVisualShader); } + void MainWindow::on_actionCreateFont_triggered() { CreateResource(TypeCase::kFont); } void MainWindow::on_actionCreateTimeline_triggered() { CreateResource(TypeCase::kTimeline); } @@ -717,3 +728,10 @@ void MainWindow::on_actionSortByName_triggered() { void MainWindow::on_treeView_customContextMenuRequested(const QPoint &pos) { _ui->menuEdit->exec(_ui->treeView->mapToGlobal(pos)); } + +void MainWindow::on_compileStatusChanged(bool finished) { + _ui->outputDockWidget->show(); + _ui->actionRun->setEnabled(finished); + _ui->actionDebug->setEnabled(finished); + _ui->actionCreateExecutable->setEnabled(finished); +} diff --git a/MainWindow.h b/MainWindow.h index 56cffccf9..dbe9862f7 100644 --- a/MainWindow.h +++ b/MainWindow.h @@ -21,6 +21,10 @@ class MainWindow; #include #include +#ifndef ENIGMA_DIR +#error "ENIGMA_DIR not defined" +#endif + namespace Ui { class MainWindow; } @@ -32,6 +36,7 @@ class MainWindow : public QMainWindow { static ResourceModelMap* resourceMap; static MessageModel* resourceModel; static TreeModel* treeModel; + static MessageModel* protoModel; static QList systemCache; explicit MainWindow(QWidget *parent); @@ -82,6 +87,7 @@ class MainWindow : public QMainWindow { void on_actionCreatePath_triggered(); void on_actionCreateScript_triggered(); void on_actionCreateShader_triggered(); + void on_actionCreateVisualShader_triggered(); void on_actionCreateFont_triggered(); void on_actionCreateTimeline_triggered(); void on_actionCreateObject_triggered(); @@ -109,6 +115,8 @@ class MainWindow : public QMainWindow { void on_treeView_doubleClicked(const QModelIndex &index); void on_treeView_customContextMenuRequested(const QPoint &pos); + void on_compileStatusChanged(bool finished); + private: void closeEvent(QCloseEvent *event) override; diff --git a/MainWindow.ui b/MainWindow.ui index b8926488a..2077e17eb 100644 --- a/MainWindow.ui +++ b/MainWindow.ui @@ -229,6 +229,7 @@ + @@ -981,6 +982,11 @@ Ctrl+Shift+G + + + Create Visual Shader + + diff --git a/Plugins/ServerPlugin.cpp b/Plugins/ServerPlugin.cpp index 57a9d4452..2ff724730 100644 --- a/Plugins/ServerPlugin.cpp +++ b/Plugins/ServerPlugin.cpp @@ -249,18 +249,17 @@ void CompilerClient::UpdateLoop(void* got_tag, bool ok) { } ServerPlugin::ServerPlugin(MainWindow& mainWindow) : RGMPlugin(mainWindow) { + // TODO: Check if emake is already running and connect to it instead of starting a new one. + // create a new child process for us to launch an emake server process = new QProcess(this); - connect(process, &QProcess::errorOccurred, [&](QProcess::ProcessError error) { - qDebug() << "QProcess error: " << error << endl; - }); - connect(process, &QProcess::readyReadStandardOutput, [&]() { - emit LogOutput(process->readAllStandardOutput()); - }); - connect(process, &QProcess::readyReadStandardError, [&]() { - emit LogOutput(process->readAllStandardError()); - }); + connect(process, &QProcess::errorOccurred, this, &ServerPlugin::onErrorOccurred); + connect(process, QOverload::of(&QProcess::finished), this, &ServerPlugin::onProcessFinished); + connect(process, &QProcess::readyReadStandardError, this, &ServerPlugin::onReadyReadStandardError); + connect(process, &QProcess::readyReadStandardOutput, this, &ServerPlugin::onReadyReadStandardOutput); + connect(process, &QProcess::started, this, &ServerPlugin::onProcessStarted); + connect(process, &QProcess::stateChanged, this, &ServerPlugin::onStateChanged); #ifdef _WIN32 //TODO: Make all this stuff configurable in IDE @@ -296,19 +295,19 @@ ServerPlugin::ServerPlugin(MainWindow& mainWindow) : RGMPlugin(mainWindow) { } if (emakeFileInfo.filePath().isEmpty()) { - qDebug() << "Error: Failed to locate emake. Compiling and syntax check will not work.\n" << "Search Paths:\n" << MainWindow::EnigmaSearchPaths; + qDebug() << "Error: Failed to locate emake. Compiling and syntax check will not work.\n" << "Search Paths:\n" << MainWindow::EnigmaSearchPaths << Qt::endl; return; } if (MainWindow::EnigmaRoot.filePath().isEmpty()) { - qDebug() << "Error: Failed to locate ENIGMA sources. Compiling and syntax check will not work.\n" << "Search Paths:\n" << MainWindow::EnigmaSearchPaths; + qDebug() << "Error: Failed to locate ENIGMA sources. Compiling and syntax check will not work.\n" << "Search Paths:\n" << MainWindow::EnigmaSearchPaths << Qt::endl; return; } // use the closest matching emake file we found and launch it in a child process - qDebug() << "Using emake exe at: " << emakeFileInfo.absolutePath(); - qDebug() << "Using ENIGMA sources at: " << MainWindow::EnigmaRoot.absolutePath(); - process->setWorkingDirectory(emakeFileInfo.absolutePath()); + qDebug() << "Using emake exe at: " << emakeFileInfo.absolutePath() << Qt::endl; + qDebug() << "Using ENIGMA sources at: " << MainWindow::EnigmaRoot.absolutePath() << Qt::endl; + process->setWorkingDirectory(emakeFileInfo.absolutePath()); // Since emake depends on other libraries in the same directory. QString program = emakeFileInfo.fileName(); QStringList arguments; arguments << "--server" @@ -321,8 +320,14 @@ ServerPlugin::ServerPlugin(MainWindow& mainWindow) : RGMPlugin(mainWindow) { qDebug() << "Running: " << program << " " << arguments; - process->start(program, arguments); - process->waitForStarted(); + process->start(emakeFileInfo.filePath(), arguments); + + // TODO: This blocks the main thread, we should probably move this to a separate thread. + if (!process->waitForStarted(-1)) { + qDebug() << "Failed to start the emake server!" << Qt::endl; + qDebug() << process->errorString() << Qt::endl; + return; + } // construct the channel and connect to the server running in the process // Note: gRPC is too dumb to resolve localhost on linux @@ -340,7 +345,15 @@ ServerPlugin::ServerPlugin(MainWindow& mainWindow) : RGMPlugin(mainWindow) { ServerPlugin::~ServerPlugin() { compilerClient->TearDown(); - process->waitForFinished(); + + if (!process->waitForFinished(-1)) { + qDebug() << "Failed to stop the emake server!" << Qt::endl; + qDebug() << process->errorString() << Qt::endl; + return; + } + + if (compilerClient) delete compilerClient; + if (process) delete process; } void ServerPlugin::Run() { compilerClient->CompileBuffer(mainWindow.Game(), CompileRequest::RUN); } @@ -352,8 +365,81 @@ void ServerPlugin::CreateExecutable() { QFileDialog::getSaveFileName(&mainWindow, tr("Create Executable"), "", tr("Executable (*.exe);;All Files (*)")); if (!fileName.isEmpty()) compilerClient->CompileBuffer(mainWindow.Game(), CompileRequest::COMPILE, fileName.toStdString()); -}; +} void ServerPlugin::SetCurrentConfig(const resources::Settings& settings) { compilerClient->SetCurrentConfig(settings); -}; +} + +void ServerPlugin::onErrorOccurred(QProcess::ProcessError error) { + qDebug() << "QProcess error: " << error << Qt::endl; + switch (error) { + case QProcess::FailedToStart: + qDebug() << "Error: The emake server failed to start. Either the invoked program is missing, or you may have insufficient permissions to invoke the program." << Qt::endl; + break; + case QProcess::Crashed: + qDebug() << "Error: The emake server crashed some time after starting successfully." << Qt::endl; + break; + case QProcess::Timedout: + qDebug() << "Error: The last waitFor...() function timed out. The state of QProcess is unchanged, and you can try calling waitFor...() again." << Qt::endl; + break; + case QProcess::WriteError: + qDebug() << "Error: An error occurred when attempting to write to the emake server. For example, the emake server may not be running, or it may have closed its input channel." << Qt::endl; + break; + case QProcess::ReadError: + qDebug() << "Error: An error occurred when attempting to read from the emake server. For example, the emake server may not be running." << Qt::endl; + break; + case QProcess::UnknownError: + qDebug() << "Error: An unknown error occurred. This is the default return value of error()." << Qt::endl; + break; + default: + qDebug() << "Error: Unrecognized error code." << Qt::endl; + break; + } +} + +void ServerPlugin::onProcessFinished(int exit_code, QProcess::ExitStatus exit_status) { + qDebug() << "Exit Code: " << exit_code << Qt::endl; + switch (exit_status) { + case QProcess::NormalExit: + qDebug() << "Exit Status: The emake server exited normally." << Qt::endl; + break; + case QProcess::CrashExit: + qDebug() << "Exit Status: The emake server crashed." << Qt::endl; + break; + default: + qDebug() << "Exit Status: Unrecognized exit status code." << Qt::endl; + break; + } +} + +void ServerPlugin::onReadyReadStandardError() { + qDebug() << "Standard Error: " << process->readAllStandardError().constData() << Qt::endl; + emit LogOutput(process->readAllStandardError()); +} + +void ServerPlugin::onReadyReadStandardOutput() { + qDebug() << "Standard Output: " << process->readAllStandardOutput().constData() << Qt::endl; + emit LogOutput(process->readAllStandardOutput()); +} + +void ServerPlugin::onProcessStarted() { + qDebug() << "The emake server started successfully!" << Qt::endl; +} + +void ServerPlugin::onStateChanged(QProcess::ProcessState state) { + switch (state) { + case QProcess::NotRunning: + qDebug() << "State: The emake server is not running." << Qt::endl; + break; + case QProcess::Starting: + qDebug() << "State: The emake server is starting, but the program has not yet been invoked." << Qt::endl; + break; + case QProcess::Running: + qDebug() << "State: The emake server is running and is ready for reading and writing." << Qt::endl; + break; + default: + qDebug() << "State: Unrecognized state code." << Qt::endl; + break; + } +} diff --git a/Plugins/ServerPlugin.h b/Plugins/ServerPlugin.h index 1f7fdd85f..2c439b64e 100644 --- a/Plugins/ServerPlugin.h +++ b/Plugins/ServerPlugin.h @@ -92,6 +92,14 @@ class ServerPlugin : public RGMPlugin { void CreateExecutable() override; void SetCurrentConfig(const buffers::resources::Settings& settings) override; + private slots: + void onErrorOccurred(QProcess::ProcessError error); + void onProcessFinished(int exit_code, QProcess::ExitStatus exit_status); + void onReadyReadStandardError(); + void onReadyReadStandardOutput(); + void onProcessStarted(); + void onStateChanged(QProcess::ProcessState state); + private: QProcess* process; CompilerClient* compilerClient; diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt new file mode 100644 index 000000000..683dfab9c --- /dev/null +++ b/Tests/CMakeLists.txt @@ -0,0 +1,219 @@ +################################################################################### +## ## +## Copyright (C) 2024 Saif Kandil (k0T0z) ## +## ## +## This file is a part of the ENIGMA Development Environment. ## +## ## +## ## +## ENIGMA is free software: you can redistribute it and/or modify it under the ## +## terms of the GNU General Public License as published by the Free Software ## +## Foundation, version 3 of the license or any later version. ## +## ## +## This application and its source code is distributed AS-IS, WITHOUT ANY ## +## WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS ## +## FOR A PARTICULAR PURPOSE. See the GNU General Public License for more ## +## details. ## +## ## +## You should have recieved a copy of the GNU General Public License along ## +## with this code. If not, see ## +## ## +## ENIGMA is an environment designed to create games and other programs with a ## +## high-level, fully compilable language. Developers of ENIGMA or anything ## +## associated with ENIGMA are in no way responsible for its users or ## +## applications created by its users, or damages caused by the environment ## +## or programs made in the environment. ## +## ## +################################################################################### + +set(RGM_TESTS "RGM-Tests" CACHE STRING "RGM Tests") + +project(${RGM_TESTS} LANGUAGES CXX) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(CMAKE_AUTOUIC_SEARCH_PATHS "${RGM_ROOTDIR}/" "${RGM_ROOTDIR}/Dialogs" "${RGM_ROOTDIR}/Editors") + +file(GLOB RGM_TESTS_SRC_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/tests_main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/MainWindowTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Editors/VisualShaderEditorTests.cpp +) + +file(GLOB RGM_TESTS_H_FILES + ${RGM_ROOTDIR}/MainWindow.h + ${RGM_ROOTDIR}/Models/MessageModel.h + ${RGM_ROOTDIR}/Models/ProtoModel.h + ${RGM_ROOTDIR}/Models/TreeModel.h + ${RGM_ROOTDIR}/Models/PrimitiveModel.h + ${RGM_ROOTDIR}/Models/RepeatedMessageModel.h + ${RGM_ROOTDIR}/Models/EventTypesListSortFilterProxyModel.h + ${RGM_ROOTDIR}/Models/ResourceModelMap.h + ${RGM_ROOTDIR}/Models/EventTypesListModel.h + ${RGM_ROOTDIR}/Models/ImmediateMapper.h + ${RGM_ROOTDIR}/Models/RepeatedModel.h + ${RGM_ROOTDIR}/Models/EventsListModel.h + ${RGM_ROOTDIR}/Models/ModelMapper.h + ${RGM_ROOTDIR}/Models/RepeatedSortFilterProxyModel.h + ${RGM_ROOTDIR}/Models/TreeSortFilterProxyModel.h + ${RGM_ROOTDIR}/Components/RecentFiles.h + ${RGM_ROOTDIR}/Components/QMenuView_p.h + ${RGM_ROOTDIR}/Components/Utility.h + ${RGM_ROOTDIR}/Components/QMenuView.h + ${RGM_ROOTDIR}/Components/Logger.h + ${RGM_ROOTDIR}/Components/ArtManager.h + ${RGM_ROOTDIR}/Editors/ObjectEditor.h + ${RGM_ROOTDIR}/Editors/PathEditor.h + ${RGM_ROOTDIR}/Editors/ScriptEditor.h + ${RGM_ROOTDIR}/Editors/CodeEditor.h + ${RGM_ROOTDIR}/Editors/SoundEditor.h + ${RGM_ROOTDIR}/Editors/IncludeEditor.h + ${RGM_ROOTDIR}/Editors/InformationEditor.h + ${RGM_ROOTDIR}/Editors/TimelineEditor.h + ${RGM_ROOTDIR}/Editors/SettingsEditor.h + ${RGM_ROOTDIR}/Editors/ShaderEditor.h + ${RGM_ROOTDIR}/Editors/RoomEditor.h + ${RGM_ROOTDIR}/Editors/BaseEditor.h + ${RGM_ROOTDIR}/Editors/FontEditor.h + ${RGM_ROOTDIR}/Editors/SpriteEditor.h + ${RGM_ROOTDIR}/Editors/BackgroundEditor.h + ${RGM_ROOTDIR}/Editors/VisualShaderEditor.h + ${RGM_ROOTDIR}/Plugins/RGMPlugin.h + ${RGM_ROOTDIR}/Plugins/ServerPlugin.h + ${RGM_ROOTDIR}/Dialogs/EventArgumentsDialog.h + ${RGM_ROOTDIR}/Dialogs/PreferencesDialog.h + ${RGM_ROOTDIR}/Dialogs/PreferencesKeys.h + ${RGM_ROOTDIR}/Dialogs/TimelineChangeMoment.h + ${RGM_ROOTDIR}/Dialogs/KeyBindingPreferences.h + ${RGM_ROOTDIR}/Utils/SafeCasts.h + ${RGM_ROOTDIR}/Utils/ProtoManip.h + ${RGM_ROOTDIR}/Utils/FieldPath.h + ${RGM_ROOTDIR}/Utils/QBoilerplate.h + ${RGM_ROOTDIR}/Widgets/BackgroundView.h + ${RGM_ROOTDIR}/Widgets/CodeWidget.h + ${RGM_ROOTDIR}/Widgets/ResourceSelector.h + ${RGM_ROOTDIR}/Widgets/ColorPicker.h + ${RGM_ROOTDIR}/Widgets/AssetScrollArea.h + ${RGM_ROOTDIR}/Widgets/SpriteView.h + ${RGM_ROOTDIR}/Widgets/AssetView.h + ${RGM_ROOTDIR}/Widgets/PathView.h + ${RGM_ROOTDIR}/Widgets/StackedCodeWidget.h + ${RGM_ROOTDIR}/Widgets/SpriteSubimageListView.h + ${RGM_ROOTDIR}/Widgets/AssetScrollAreaBackground.h + ${RGM_ROOTDIR}/Widgets/RoomView.h +) + +file(GLOB RGM_TESTS_DEPS_FILES + ${RGM_ROOTDIR}/MainWindow.cpp + ${RGM_ROOTDIR}/Models/RepeatedMessageModel.cpp + ${RGM_ROOTDIR}/Models/TreeSortFilterProxyModel.cpp + ${RGM_ROOTDIR}/Models/PrimitiveModel.cpp + ${RGM_ROOTDIR}/Models/EventsListModel.cpp + ${RGM_ROOTDIR}/Models/ModelMapper.cpp + ${RGM_ROOTDIR}/Models/RepeatedModel.cpp + ${RGM_ROOTDIR}/Models/MessageModel.cpp + ${RGM_ROOTDIR}/Models/TreeModel.cpp + ${RGM_ROOTDIR}/Models/EventTypesListModel.cpp + ${RGM_ROOTDIR}/Models/ResourceModelMap.cpp + ${RGM_ROOTDIR}/Models/ImmediateMapper.cpp + ${RGM_ROOTDIR}/Models/ProtoModel.cpp + ${RGM_ROOTDIR}/Models/EventTypesListSortFilterProxyModel.cpp + ${RGM_ROOTDIR}/Models/RepeatedSortFilterProxyModel.cpp + ${RGM_ROOTDIR}/Components/Utility.cpp + ${RGM_ROOTDIR}/Components/RecentFiles.cpp + ${RGM_ROOTDIR}/Components/QMenuView.cpp + ${RGM_ROOTDIR}/Components/ArtManager.cpp + ${RGM_ROOTDIR}/Editors/VisualShaderEditor.cpp + ${RGM_ROOTDIR}/Editors/PathEditor.cpp + ${RGM_ROOTDIR}/Editors/RoomEditor.cpp + ${RGM_ROOTDIR}/Editors/ObjectEditor.cpp + ${RGM_ROOTDIR}/Editors/FontEditor.cpp + ${RGM_ROOTDIR}/Editors/SpriteEditor.cpp + ${RGM_ROOTDIR}/Editors/BackgroundEditor.cpp + ${RGM_ROOTDIR}/Editors/SettingsEditor.cpp + ${RGM_ROOTDIR}/Editors/ShaderEditor.cpp + ${RGM_ROOTDIR}/Editors/SoundEditor.cpp + ${RGM_ROOTDIR}/Editors/IncludeEditor.cpp + ${RGM_ROOTDIR}/Editors/InformationEditor.cpp + ${RGM_ROOTDIR}/Editors/BaseEditor.cpp + ${RGM_ROOTDIR}/Editors/ScriptEditor.cpp + ${RGM_ROOTDIR}/Editors/CodeEditor.cpp + ${RGM_ROOTDIR}/Editors/TimelineEditor.cpp + ${RGM_ROOTDIR}/Editors/VisualShaderEditor.cpp + ${RGM_ROOTDIR}/Plugins/RGMPlugin.cpp + ${RGM_ROOTDIR}/Plugins/ServerPlugin.cpp + ${RGM_ROOTDIR}/Dialogs/EventArgumentsDialog.cpp + ${RGM_ROOTDIR}/Dialogs/TimelineChangeMoment.cpp + ${RGM_ROOTDIR}/Dialogs/PreferencesDialog.cpp + ${RGM_ROOTDIR}/Dialogs/PreferencesKeys.cpp + ${RGM_ROOTDIR}/Dialogs/KeyBindingPreferences.cpp + ${RGM_ROOTDIR}/Utils/ProtoManip.cpp + ${RGM_ROOTDIR}/Utils/FieldPath.cpp + ${RGM_ROOTDIR}/Widgets/BackgroundView.cpp + ${RGM_ROOTDIR}/Widgets/ColorPicker.cpp + ${RGM_ROOTDIR}/Widgets/AssetView.cpp + ${RGM_ROOTDIR}/Widgets/PathView.cpp + ${RGM_ROOTDIR}/Widgets/RoomView.cpp + ${RGM_ROOTDIR}/Widgets/SpriteView.cpp + ${RGM_ROOTDIR}/Widgets/CodeWidget.cpp + ${RGM_ROOTDIR}/Widgets/CodeWidgetPlain.cpp # Ignore checking for QScintilla + ${RGM_ROOTDIR}/Widgets/SpriteSubimageListView.cpp + ${RGM_ROOTDIR}/Widgets/StackedCodeWidget.cpp + ${RGM_ROOTDIR}/Widgets/AssetScrollAreaBackground.cpp +) + +file(GLOB RGM_TESTS_UI_FILES + ${RGM_ROOTDIR}/MainWindow.ui + ${RGM_ROOTDIR}/Dialogs/TimelineChangeMoment.ui + ${RGM_ROOTDIR}/Dialogs/PreferencesDialog.ui + ${RGM_ROOTDIR}/Dialogs/AddImageDialog.ui + ${RGM_ROOTDIR}/Editors/CodeEditor.ui + ${RGM_ROOTDIR}/Editors/SoundEditor.ui + ${RGM_ROOTDIR}/Editors/BackgroundEditor.ui + ${RGM_ROOTDIR}/Editors/SpriteEditor.ui + ${RGM_ROOTDIR}/Editors/PathEditor.ui + ${RGM_ROOTDIR}/Editors/SettingsEditor.ui + ${RGM_ROOTDIR}/Editors/RoomEditor.ui + ${RGM_ROOTDIR}/Editors/FontEditor.ui + ${RGM_ROOTDIR}/Editors/TimelineEditor.ui + ${RGM_ROOTDIR}/Editors/ObjectEditor.ui + ${RGM_ROOTDIR}/Editors/InformationEditor.ui + ${RGM_ROOTDIR}/Editors/IncludeEditor.ui +) + +file(GLOB RGM_TESTS_QRC_FILES + ${RGM_ROOTDIR}/images.qrc + ${RGM_ROOTDIR}/resources.rc +) + +enable_testing() + +add_executable(${RGM_TESTS} ${RGM_TESTS_H_FILES} ${RGM_TESTS_SRC_FILES} ${RGM_TESTS_DEPS_FILES} ${RGM_TESTS_UI_FILES} ${RGM_TESTS_QRC_FILES}) + +# Find Protobuf +find_package(Protobuf CONFIG REQUIRED) +target_link_libraries(${RGM_TESTS} PRIVATE protobuf::libprotobuf) + +# Find gRPC +find_package(gRPC CONFIG REQUIRED) +target_link_libraries(${RGM_TESTS} PRIVATE gRPC::gpr gRPC::grpc gRPC::grpc++) + +find_package(Qt5 COMPONENTS Core Widgets Gui PrintSupport Multimedia REQUIRED) +target_link_libraries(${RGM_TESTS} PRIVATE Qt5::Core Qt5::Widgets Qt5::Gui Qt5::PrintSupport Qt5::Multimedia) + +find_package(Qt5Test REQUIRED) +target_link_libraries(${RGM_TESTS} PRIVATE Qt5::Test) + +target_include_directories(${RGM_TESTS} PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/" + "${RGM_ROOTDIR}/" +) + +target_compile_definitions(${RGM_TESTS} PRIVATE QT_MESSAGELOGCONTEXT ENIGMA_DIR="${ENIGMA_DIR}") + +target_link_libraries(${RGM_TESTS} PRIVATE ${LIB_EGM} ${LIB_PROTO} ${SHARED_LIB}) + +add_test(NAME ${RGM_TESTS} COMMAND ${RGM_TESTS}) diff --git a/Tests/Editors/VisualShaderEditorTests.cpp b/Tests/Editors/VisualShaderEditorTests.cpp new file mode 100644 index 000000000..2b6135760 --- /dev/null +++ b/Tests/Editors/VisualShaderEditorTests.cpp @@ -0,0 +1,172 @@ +/*********************************************************************************/ +/* */ +/* Copyright (C) 2024 Saif Kandil (k0T0z) */ +/* */ +/* This file is a part of the ENIGMA Development Environment. */ +/* */ +/* */ +/* ENIGMA is free software: you can redistribute it and/or modify it under the */ +/* terms of the GNU General Public License as published by the Free Software */ +/* Foundation, version 3 of the license or any later version. */ +/* */ +/* This application and its source code is distributed AS-IS, WITHOUT ANY */ +/* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS */ +/* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more */ +/* details. */ +/* */ +/* You should have recieved a copy of the GNU General Public License along */ +/* with this code. If not, see */ +/* */ +/* ENIGMA is an environment designed to create games and other programs with a */ +/* high-level, fully compilable language. Developers of ENIGMA or anything */ +/* associated with ENIGMA are in no way responsible for its users or */ +/* applications created by its users, or damages caused by the environment */ +/* or programs made in the environment. */ +/* */ +/*********************************************************************************/ + +#include "Editors/VisualShaderEditorTests.h" + +#include +#include + +void TestVisualShaderEditor::initTestCase() { + editor = new VisualShaderEditor(); + + // Get the scene + VisualShaderGraphicsScene* scene{editor->get_scene()}; + QVERIFY(scene != nullptr); + + { + // Add an input UV node + bool result{scene->add_node("VisualShaderNodeInput", {-200, 0})}; + QVERIFY(result); + } + + { + // Add an input TIME node + bool result{scene->add_node("VisualShaderNodeInput", {-200, 350})}; + QVERIFY(result); + } + + { + // Add a value noise node + bool result{scene->add_node("VisualShaderNodeValueNoise", {-50, 0})}; + QVERIFY(result); + } + + { + // Add a float function node: sin + bool result{scene->add_node("VisualShaderNodeFloatFunc", {-50, 350})}; + QVERIFY(result); + } + + { + // Add a divide operator node + bool result{scene->add_node("VisualShaderNodeFloatOp", {150, 150})}; + QVERIFY(result); + } +} +void TestVisualShaderEditor::init() {} + +void TestVisualShaderEditor::cleanupTestCase() { delete editor; } +void TestVisualShaderEditor::cleanup() {} + +void TestVisualShaderEditor::test_create_full_graph() { + editor->show(); + + // Wait for the editor to be shown + QVERIFY(QTest::qWaitForWindowExposed(editor)); + + // Get the scene + VisualShaderGraphicsScene* scene{editor->get_scene()}; + QVERIFY(scene != nullptr); + VisualShaderGraphicsView* view{editor->get_view()}; + QVERIFY(view != nullptr); + + VisualShaderOutputPortGraphicsObject* o_p_uv{nullptr}; + + { + VisualShaderNodeGraphicsObject* uv_node{scene->get_node_graphics_object(1)}; // 1 is the node id + QVERIFY(uv_node != nullptr); + o_p_uv = uv_node->get_output_port_graphics_object(0); // The only output port + QVERIFY(o_p_uv != nullptr); + QWidget* embed_widget{uv_node->get_embed_widget()}; + QVERIFY(embed_widget != nullptr); + VisualShaderNodeInputEmbedWidget* uv_embed_widget{dynamic_cast(embed_widget)}; + QVERIFY(uv_embed_widget != nullptr); + uv_embed_widget->set_current_index(1); // 1 is UV + } + + VisualShaderOutputPortGraphicsObject* o_p_time{nullptr}; + + { + VisualShaderNodeGraphicsObject* uv_node{scene->get_node_graphics_object(2)}; // 2 is the node id + QVERIFY(uv_node != nullptr); + o_p_time = uv_node->get_output_port_graphics_object(0); // The only output port + QVERIFY(o_p_time != nullptr); + QWidget* embed_widget{uv_node->get_embed_widget()}; + QVERIFY(embed_widget != nullptr); + VisualShaderNodeInputEmbedWidget* uv_embed_widget{dynamic_cast(embed_widget)}; + QVERIFY(uv_embed_widget != nullptr); + uv_embed_widget->set_current_index(2); // 2 is TIME + } + + VisualShaderInputPortGraphicsObject* i_p_noise{nullptr}; + VisualShaderOutputPortGraphicsObject* o_p_noise{nullptr}; + + { + VisualShaderNodeGraphicsObject* noise_node{scene->get_node_graphics_object(3)}; // 3 is the node id + QVERIFY(noise_node != nullptr); + i_p_noise = noise_node->get_input_port_graphics_object(0); // The only input port + QVERIFY(i_p_noise != nullptr); + o_p_noise = noise_node->get_output_port_graphics_object(0); // The only output port + QVERIFY(o_p_noise != nullptr); + } + + VisualShaderInputPortGraphicsObject* i_p_sin{nullptr}; + VisualShaderOutputPortGraphicsObject* o_p_sin{nullptr}; + + { + VisualShaderNodeGraphicsObject* sin_node{scene->get_node_graphics_object(4)}; // 4 is the node id + QVERIFY(sin_node != nullptr); + i_p_sin = sin_node->get_input_port_graphics_object(0); // The only input port + QVERIFY(i_p_sin != nullptr); + o_p_sin = sin_node->get_output_port_graphics_object(0); // The only output port + QVERIFY(o_p_sin != nullptr); + QWidget* embed_widget{sin_node->get_embed_widget()}; + QVERIFY(embed_widget != nullptr); + VisualShaderNodeFloatFuncEmbedWidget* sin_embed_widget{ + dynamic_cast(embed_widget)}; + QVERIFY(sin_embed_widget != nullptr); + sin_embed_widget->set_current_index(0); // 0 is sin + } + + VisualShaderInputPortGraphicsObject* i1_p_divide{nullptr}; + VisualShaderInputPortGraphicsObject* i2_p_divide{nullptr}; + VisualShaderOutputPortGraphicsObject* o_p_divide{nullptr}; + + { + VisualShaderNodeGraphicsObject* divide_node{scene->get_node_graphics_object(5)}; // 5 is the node id + QVERIFY(divide_node != nullptr); + i1_p_divide = divide_node->get_input_port_graphics_object(0); // The first input port + QVERIFY(i1_p_divide != nullptr); + i2_p_divide = divide_node->get_input_port_graphics_object(1); // The second input port + QVERIFY(i2_p_divide != nullptr); + o_p_divide = divide_node->get_output_port_graphics_object(0); // The only output port + QVERIFY(o_p_divide != nullptr); + QWidget* embed_widget{divide_node->get_embed_widget()}; + QVERIFY(embed_widget != nullptr); + VisualShaderNodeFloatOpEmbedWidget* divide_embed_widget{ + dynamic_cast(embed_widget)}; + QVERIFY(divide_embed_widget != nullptr); + divide_embed_widget->set_current_index(3); // 3 is divide + } + + { + // Connect UV to noise + } + + // Wait + QTest::qWait(10000); +} diff --git a/Tests/Editors/VisualShaderEditorTests.h b/Tests/Editors/VisualShaderEditorTests.h new file mode 100644 index 000000000..243afb13b --- /dev/null +++ b/Tests/Editors/VisualShaderEditorTests.h @@ -0,0 +1,49 @@ +/*********************************************************************************/ +/* */ +/* Copyright (C) 2024 Saif Kandil (k0T0z) */ +/* */ +/* This file is a part of the ENIGMA Development Environment. */ +/* */ +/* */ +/* ENIGMA is free software: you can redistribute it and/or modify it under the */ +/* terms of the GNU General Public License as published by the Free Software */ +/* Foundation, version 3 of the license or any later version. */ +/* */ +/* This application and its source code is distributed AS-IS, WITHOUT ANY */ +/* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS */ +/* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more */ +/* details. */ +/* */ +/* You should have recieved a copy of the GNU General Public License along */ +/* with this code. If not, see */ +/* */ +/* ENIGMA is an environment designed to create games and other programs with a */ +/* high-level, fully compilable language. Developers of ENIGMA or anything */ +/* associated with ENIGMA are in no way responsible for its users or */ +/* applications created by its users, or damages caused by the environment */ +/* or programs made in the environment. */ +/* */ +/*********************************************************************************/ + +#ifndef VISUAL_SHADER_EDITOR_TESTS_H +#define VISUAL_SHADER_EDITOR_TESTS_H + +#include "Editors/VisualShaderEditor.h" + +class TestVisualShaderEditor : public QObject { + Q_OBJECT + + private slots: + void initTestCase(); // Will be called before the first test function is executed. + void init(); // Will be called before each test function is executed. + + void cleanupTestCase(); // Will be called after the last test function was executed. + void cleanup(); // Will be called after every test function. + + void test_create_full_graph(); + + private: + VisualShaderEditor* editor = nullptr; +}; + +#endif // VISUAL_SHADER_EDITOR_TESTS_H diff --git a/Tests/MainWindowTests.cpp b/Tests/MainWindowTests.cpp new file mode 100644 index 000000000..500a8609a --- /dev/null +++ b/Tests/MainWindowTests.cpp @@ -0,0 +1,46 @@ +/*********************************************************************************/ +/* */ +/* Copyright (C) 2024 Saif Kandil (k0T0z) */ +/* */ +/* This file is a part of the ENIGMA Development Environment. */ +/* */ +/* */ +/* ENIGMA is free software: you can redistribute it and/or modify it under the */ +/* terms of the GNU General Public License as published by the Free Software */ +/* Foundation, version 3 of the license or any later version. */ +/* */ +/* This application and its source code is distributed AS-IS, WITHOUT ANY */ +/* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS */ +/* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more */ +/* details. */ +/* */ +/* You should have recieved a copy of the GNU General Public License along */ +/* with this code. If not, see */ +/* */ +/* ENIGMA is an environment designed to create games and other programs with a */ +/* high-level, fully compilable language. Developers of ENIGMA or anything */ +/* associated with ENIGMA are in no way responsible for its users or */ +/* applications created by its users, or damages caused by the environment */ +/* or programs made in the environment. */ +/* */ +/*********************************************************************************/ + +#include "MainWindowTests.h" + +#include +#include + +void TestMainWindow::initTestCase() { + mw = new MainWindow(nullptr); +} +void TestMainWindow::init() { } + +void TestMainWindow::cleanupTestCase() { delete mw; } +void TestMainWindow::cleanup() { } + +void TestMainWindow::testVisualShaderEditor() { + mw->show(); + + // Wait for the window to be shown + QTest::qWait(1000); +} diff --git a/Tests/MainWindowTests.h b/Tests/MainWindowTests.h new file mode 100644 index 000000000..a10e8c004 --- /dev/null +++ b/Tests/MainWindowTests.h @@ -0,0 +1,49 @@ +/*********************************************************************************/ +/* */ +/* Copyright (C) 2024 Saif Kandil (k0T0z) */ +/* */ +/* This file is a part of the ENIGMA Development Environment. */ +/* */ +/* */ +/* ENIGMA is free software: you can redistribute it and/or modify it under the */ +/* terms of the GNU General Public License as published by the Free Software */ +/* Foundation, version 3 of the license or any later version. */ +/* */ +/* This application and its source code is distributed AS-IS, WITHOUT ANY */ +/* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS */ +/* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more */ +/* details. */ +/* */ +/* You should have recieved a copy of the GNU General Public License along */ +/* with this code. If not, see */ +/* */ +/* ENIGMA is an environment designed to create games and other programs with a */ +/* high-level, fully compilable language. Developers of ENIGMA or anything */ +/* associated with ENIGMA are in no way responsible for its users or */ +/* applications created by its users, or damages caused by the environment */ +/* or programs made in the environment. */ +/* */ +/*********************************************************************************/ + +#ifndef MAIN_WINDOW_TESTS_H +#define MAIN_WINDOW_TESTS_H + +#include "MainWindow.h" + +class TestMainWindow : public QObject { + Q_OBJECT + + private slots: + void initTestCase(); // Will be called before the first test function is executed. + void init(); // Will be called before each test function is executed. + + void cleanupTestCase(); // Will be called after the last test function was executed. + void cleanup(); // Will be called after every test function. + + void testVisualShaderEditor(); + + private: + MainWindow* mw; +}; + +#endif // MAIN_WINDOW_TESTS_H diff --git a/Tests/tests_main.cpp b/Tests/tests_main.cpp new file mode 100644 index 000000000..2e1c53d1d --- /dev/null +++ b/Tests/tests_main.cpp @@ -0,0 +1,49 @@ +/*********************************************************************************/ +/* */ +/* Copyright (C) 2024 Saif Kandil (k0T0z) */ +/* */ +/* This file is a part of the ENIGMA Development Environment. */ +/* */ +/* */ +/* ENIGMA is free software: you can redistribute it and/or modify it under the */ +/* terms of the GNU General Public License as published by the Free Software */ +/* Foundation, version 3 of the license or any later version. */ +/* */ +/* This application and its source code is distributed AS-IS, WITHOUT ANY */ +/* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS */ +/* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more */ +/* details. */ +/* */ +/* You should have recieved a copy of the GNU General Public License along */ +/* with this code. If not, see */ +/* */ +/* ENIGMA is an environment designed to create games and other programs with a */ +/* high-level, fully compilable language. Developers of ENIGMA or anything */ +/* associated with ENIGMA are in no way responsible for its users or */ +/* applications created by its users, or damages caused by the environment */ +/* or programs made in the environment. */ +/* */ +/*********************************************************************************/ + +#include + +#include "MainWindowTests.h" +#include "Editors/VisualShaderEditorTests.h" + +#define RUN_TEST(CLASS_NAME) \ + { \ + ::QTest::Internal::callInitMain(); \ + CLASS_NAME t; \ + int r {QTest::qExec(&t, argc, argv)}; \ + if (r != 0) return 1; \ + } + +int main(int argc, char* argv[]) { + QApplication app(argc, argv); + app.setAttribute(Qt::AA_Use96Dpi, true); + + RUN_TEST(TestVisualShaderEditor); + // RUN_TEST(TestMainWindow); + + return 0; +} diff --git a/main.cpp b/main.cpp index 5694ab251..35868ddd4 100644 --- a/main.cpp +++ b/main.cpp @@ -1,4 +1,3 @@ -#include "main.h" #include "MainWindow.h" #include "Dialogs/PreferencesKeys.h" @@ -8,8 +7,6 @@ #include #include -QString defaultStyle = ""; - int main(int argc, char *argv[]) { // must enable high DPI before QApplication constructor QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); diff --git a/main.h b/main.h deleted file mode 100644 index 62bf1da0e..000000000 --- a/main.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef MAIN_H -#define MAIN_H - -#include - -// Qt doesn't have a way of getting the default style -// so we store it for later when the user restores -// default settings so we can apply it again -extern QString defaultStyle; - -#endif // MAIN_H