diff --git a/.gitignore b/.gitignore index a4fb4fb..a35b9d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ build/ .cache/ +Images-thingy/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 8181195..d0fcfce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,12 +51,35 @@ endif() # ==================== Optional PhysX ==================== option(MODULARITY_ENABLE_PHYSX "Enable PhysX physics integration" ON) +option(MODULARITY_BUILD_EDITOR "Build the Modularity editor target" ON) # ==================== Third-party libraries ==================== add_subdirectory(src/ThirdParty/glfw EXCLUDE_FROM_ALL) find_package(OpenGL REQUIRED) +# ==================== Mono (managed scripting) ==================== +option(MODULARITY_USE_MONO "Enable Mono embedding for managed scripts" ON) + +if(MODULARITY_USE_MONO) + set(MONO_ROOT ${PROJECT_SOURCE_DIR}/src/ThirdParty/mono CACHE PATH "Mono root directory") + find_path(MONO_INCLUDE_DIR mono/jit/jit.h + HINTS + ${MONO_ROOT}/include/mono-2.0 + ${MONO_ROOT}/include + ) + find_library(MONO_LIBRARY + NAMES mono-2.0-sgen mono-2.0 monosgen-2.0 + HINTS + ${MONO_ROOT}/lib + ${MONO_ROOT}/lib64 + ) + if(NOT MONO_INCLUDE_DIR OR NOT MONO_LIBRARY) + message(WARNING "Mono not found. Disabling MODULARITY_USE_MONO. Set MONO_ROOT to a Mono runtime with include/mono-2.0 and libmono-2.0-sgen.") + set(MODULARITY_USE_MONO OFF CACHE BOOL "Enable Mono embedding for managed scripts" FORCE) + endif() +endif() + # GLAD add_library(glad STATIC src/ThirdParty/glad/glad.c) target_include_directories(glad PUBLIC src/ThirdParty/glad) @@ -105,6 +128,7 @@ file(GLOB_RECURSE ENGINE_HEADERS CONFIGURE_DEPENDS list(FILTER ENGINE_SOURCES EXCLUDE REGEX ".*/ThirdParty/assimp/.*") list(FILTER ENGINE_SOURCES EXCLUDE REGEX ".*/ThirdParty/PhysX/.*") +list(FILTER ENGINE_SOURCES EXCLUDE REGEX ".*/main_player.cpp") list(FILTER ENGINE_HEADERS EXCLUDE REGEX ".*/ThirdParty/assimp/.*") list(FILTER ENGINE_HEADERS EXCLUDE REGEX ".*/ThirdParty/PhysX/.*") @@ -122,8 +146,29 @@ target_include_directories(core PUBLIC ${PROJECT_SOURCE_DIR}/src/ThirdParty/assimp/include ) target_link_libraries(core PUBLIC glad glm imgui imguizmo) +if(MODULARITY_USE_MONO) + target_include_directories(core PUBLIC ${MONO_INCLUDE_DIR}) + target_link_libraries(core PUBLIC ${MONO_LIBRARY}) +endif() +target_compile_definitions(core PUBLIC MODULARITY_USE_MONO=$) target_compile_options(core PRIVATE ${MODULARITY_WARNING_FLAGS}) +add_library(core_player STATIC ${ENGINE_SOURCES} ${ENGINE_HEADERS}) +target_compile_definitions(core_player PUBLIC MODULARITY_PLAYER) +target_link_libraries(core_player PUBLIC assimp) +target_include_directories(core_player PUBLIC + ${PROJECT_SOURCE_DIR}/src + ${PROJECT_SOURCE_DIR}/include + ${PROJECT_SOURCE_DIR}/src/ThirdParty/assimp/include +) +target_link_libraries(core_player PUBLIC glad glm imgui imguizmo) +if(MODULARITY_USE_MONO) + target_include_directories(core_player PUBLIC ${MONO_INCLUDE_DIR}) + target_link_libraries(core_player PUBLIC ${MONO_LIBRARY}) +endif() +target_compile_definitions(core_player PUBLIC MODULARITY_USE_MONO=$) +target_compile_options(core_player PRIVATE ${MODULARITY_WARNING_FLAGS}) + if(MODULARITY_ENABLE_PHYSX) set(PHYSX_ROOT_DIR ${PROJECT_SOURCE_DIR}/src/ThirdParty/PhysX/physx CACHE PATH "PhysX root directory") set(TARGET_BUILD_PLATFORM "linux" CACHE STRING "PhysX build platform (linux/windows)") @@ -134,19 +179,47 @@ if(MODULARITY_ENABLE_PHYSX) target_include_directories(core PUBLIC ${PHYSX_ROOT_DIR}/include) target_compile_definitions(core PUBLIC MODULARITY_ENABLE_PHYSX PX_PHYSX_STATIC_LIB) target_link_libraries(core PUBLIC PhysX PhysXCommon PhysXFoundation PhysXExtensions PhysXCooking) + target_include_directories(core_player PUBLIC ${PHYSX_ROOT_DIR}/include) + target_compile_definitions(core_player PUBLIC MODULARITY_ENABLE_PHYSX PX_PHYSX_STATIC_LIB) + target_link_libraries(core_player PUBLIC PhysX PhysXCommon PhysXFoundation PhysXExtensions PhysXCooking) endif() # ==================== Executable ==================== -add_executable(Modularity src/main.cpp) -target_compile_options(Modularity PRIVATE ${MODULARITY_WARNING_FLAGS}) +if(MODULARITY_BUILD_EDITOR) + add_executable(Modularity src/main.cpp) + target_compile_options(Modularity PRIVATE ${MODULARITY_WARNING_FLAGS}) +endif() +add_executable(ModularityPlayer src/main_player.cpp) +target_compile_options(ModularityPlayer PRIVATE ${MODULARITY_WARNING_FLAGS}) # Link order matters on Linux if(NOT WIN32) find_package(X11 REQUIRED) - target_include_directories(Modularity PRIVATE ${X11_INCLUDE_DIR}) + if(MODULARITY_BUILD_EDITOR) + target_include_directories(Modularity PRIVATE ${X11_INCLUDE_DIR}) + target_link_libraries(Modularity PRIVATE + core + imgui + imguizmo + glad + glm + glfw + OpenGL::GL + pthread + dl + ${X11_LIBRARIES} + Xrandr + Xi + Xinerama + Xcursor + ) + # Export symbols so runtime-loaded scripts can resolve ImGui/engine symbols. + target_link_options(Modularity PRIVATE "-rdynamic") + endif() - target_link_libraries(Modularity PRIVATE - core + target_include_directories(ModularityPlayer PRIVATE ${X11_INCLUDE_DIR}) + target_link_libraries(ModularityPlayer PRIVATE + core_player imgui imguizmo glad @@ -161,15 +234,25 @@ if(NOT WIN32) Xinerama Xcursor ) - # Export symbols so runtime-loaded scripts can resolve ImGui/engine symbols. - target_link_options(Modularity PRIVATE "-rdynamic") + target_link_options(ModularityPlayer PRIVATE "-rdynamic") else() - target_link_libraries(Modularity PRIVATE core glfw OpenGL::GL) + if(MODULARITY_BUILD_EDITOR) + target_link_libraries(Modularity PRIVATE core glfw OpenGL::GL) + endif() + target_link_libraries(ModularityPlayer PRIVATE core_player glfw OpenGL::GL) endif() # ==================== Copy Resources folder after build ==================== -add_custom_command(TARGET Modularity POST_BUILD +if(MODULARITY_BUILD_EDITOR) + add_custom_command(TARGET Modularity POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_SOURCE_DIR}/Resources + $/Resources + ) +endif() + +add_custom_command(TARGET ModularityPlayer POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/Resources - $/Resources + $/Resources ) diff --git a/Resources/Fonts/TheSunset.ttf b/Resources/Fonts/TheSunset.ttf new file mode 100644 index 0000000..c903940 Binary files /dev/null and b/Resources/Fonts/TheSunset.ttf differ diff --git a/Resources/Fonts/Thesunsethd-Regular (1).ttf b/Resources/Fonts/Thesunsethd-Regular (1).ttf new file mode 100644 index 0000000..02235ac Binary files /dev/null and b/Resources/Fonts/Thesunsethd-Regular (1).ttf differ diff --git a/Resources/Shaders/skinned_vert.glsl b/Resources/Shaders/skinned_vert.glsl new file mode 100644 index 0000000..e787f5a --- /dev/null +++ b/Resources/Shaders/skinned_vert.glsl @@ -0,0 +1,44 @@ +#version 330 core +layout (location = 0) in vec3 aPos; +layout (location = 1) in vec3 aNormal; +layout (location = 2) in vec2 aTexCoord; +layout (location = 3) in ivec4 aBoneIds; +layout (location = 4) in vec4 aBoneWeights; + +out vec3 FragPos; +out vec3 Normal; +out vec2 TexCoord; + +uniform mat4 model; +uniform mat4 view; +uniform mat4 projection; +uniform mat4 bones[256]; +uniform int boneCount; +uniform bool useSkinning; + +void main() +{ + vec4 localPos = vec4(aPos, 1.0); + vec3 localNormal = aNormal; + if (useSkinning) { + vec4 skinnedPos = vec4(0.0); + vec3 skinnedNormal = vec3(0.0); + for (int i = 0; i < 4; ++i) { + int id = aBoneIds[i]; + float w = aBoneWeights[i]; + if (w <= 0.0 || id < 0 || id >= boneCount) continue; + mat4 b = bones[id]; + skinnedPos += (b * localPos) * w; + skinnedNormal += mat3(b) * localNormal * w; + } + localPos = skinnedPos; + localNormal = skinnedNormal; + } + + vec4 worldPos = model * localPos; + FragPos = vec3(worldPos); + Normal = mat3(transpose(inverse(model))) * localNormal; + TexCoord = aTexCoord; + + gl_Position = projection * view * worldPos; +} diff --git a/Resources/anim.ini b/Resources/anim.ini new file mode 100644 index 0000000..ea0f594 --- /dev/null +++ b/Resources/anim.ini @@ -0,0 +1,125 @@ +[Window][Debug##Default] +Pos=60,60 +Size=400,400 +Collapsed=0 + +[Window][Modularity - Project Launcher] +Pos=569,288 +Size=720,480 +Collapsed=0 + +[Window][New Project] +Pos=679,403 +Size=500,250 +Collapsed=0 + +[Window][DockSpace] +Pos=0,24 +Size=1000,776 +Collapsed=0 + +[Window][Viewport] +Pos=304,48 +Size=329,752 +Collapsed=0 +DockId=0x0000000B,0 + +[Window][Hierarchy] +Pos=0,48 +Size=304,617 +Collapsed=0 +DockId=0x0000000D,0 + +[Window][Inspector] +Pos=633,48 +Size=367,557 +Collapsed=0 +DockId=0x00000001,0 + +[Window][File Browser] +Pos=756,836 +Size=753,221 +Collapsed=0 +DockId=0xD71539A0,1 + +[Window][Console] +Pos=0,665 +Size=304,135 +Collapsed=0 +DockId=0x0000000E,0 + +[Window][Project] +Pos=633,605 +Size=367,195 +Collapsed=0 +DockId=0x00000002,0 + +[Window][Launcher] +Pos=0,0 +Size=1000,800 +Collapsed=0 + +[Window][Camera] +Pos=0,48 +Size=304,747 +Collapsed=0 +DockId=0x00000005,1 + +[Window][Environment] +Pos=1553,48 +Size=347,747 +Collapsed=0 +DockId=0x00000005,1 + +[Window][Project Manager] +Pos=787,785 +Size=784,221 +Collapsed=0 +DockId=0xD71539A0,1 + +[Window][Game Viewport] +Pos=304,48 +Size=329,752 +Collapsed=0 +DockId=0x0000000B,1 + +[Window][Project Settings] +Pos=633,48 +Size=367,557 +Collapsed=0 +DockId=0x00000001,1 + +[Window][Animation] +Pos=304,804 +Size=1229,218 +Collapsed=0 +DockId=0x0000000C,0 + +[Window][Scripting] +Pos=304,48 +Size=329,752 +Collapsed=0 +DockId=0x0000000B,2 + +[Table][0xFF88847C,2] +RefScale=16 +Column 0 Width=220 +Column 1 Weight=1.0000 + +[Docking][Data] +DockSpace ID=0xD71539A0 Window=0x3DA2F1DE Pos=0,48 Size=1000,752 Split=Y + DockNode ID=0x00000005 Parent=0xD71539A0 SizeRef=1249,733 Split=X Selected=0xC450F867 + DockNode ID=0x00000007 Parent=0x00000005 SizeRef=304,227 Split=Y Selected=0xBABDAE5E + DockNode ID=0x0000000D Parent=0x00000007 SizeRef=304,799 Selected=0xBABDAE5E + DockNode ID=0x0000000E Parent=0x00000007 SizeRef=304,175 Selected=0xEA83D666 + DockNode ID=0x00000008 Parent=0x00000005 SizeRef=1596,227 Split=X Selected=0xE9044848 + DockNode ID=0x00000003 Parent=0x00000008 SizeRef=1229,227 Split=Y Selected=0xE9044848 + DockNode ID=0x0000000B Parent=0x00000003 SizeRef=1213,756 CentralNode=1 Selected=0xE9044848 + DockNode ID=0x0000000C Parent=0x00000003 SizeRef=1213,218 Selected=0x5921A509 + DockNode ID=0x00000004 Parent=0x00000008 SizeRef=367,227 Split=Y Selected=0x36DC96AB + DockNode ID=0x00000001 Parent=0x00000004 SizeRef=797,722 Selected=0x3F1379AF + DockNode ID=0x00000002 Parent=0x00000004 SizeRef=797,252 Selected=0x9C21DE82 + DockNode ID=0x00000006 Parent=0xD71539A0 SizeRef=1249,241 Split=X Selected=0x9C21DE82 + DockNode ID=0x00000009 Parent=0x00000006 SizeRef=383,278 Selected=0x9C21DE82 + DockNode ID=0x0000000A Parent=0x00000006 SizeRef=866,278 Selected=0xEA83D666 + diff --git a/Resources/imgui.ini b/Resources/imgui.ini index 2d561b2..c9493df 100644 --- a/Resources/imgui.ini +++ b/Resources/imgui.ini @@ -14,45 +14,45 @@ Size=500,250 Collapsed=0 [Window][DockSpace] -Pos=0,23 -Size=1920,983 +Pos=0,24 +Size=1900,998 Collapsed=0 [Window][Viewport] -Pos=306,46 -Size=1265,739 +Pos=304,48 +Size=1249,747 Collapsed=0 -DockId=0x00000002,0 +DockId=0x0000000F,0 [Window][Hierarchy] -Pos=0,46 -Size=304,739 +Pos=0,48 +Size=304,747 Collapsed=0 -DockId=0x00000001,0 +DockId=0x00000007,0 [Window][Inspector] -Pos=1573,46 -Size=347,960 +Pos=1553,48 +Size=347,747 Collapsed=0 -DockId=0x00000008,0 +DockId=0x00000010,0 [Window][File Browser] Pos=756,836 Size=753,221 Collapsed=0 -DockId=0x00000006,1 +DockId=0xD71539A0,1 [Window][Console] -Pos=0,787 -Size=785,219 +Pos=939,795 +Size=961,227 Collapsed=0 -DockId=0x00000005,0 +DockId=0x00000014,0 [Window][Project] -Pos=787,787 -Size=784,219 +Pos=0,795 +Size=939,227 Collapsed=0 -DockId=0x00000006,0 +DockId=0x00000013,0 [Window][Launcher] Pos=0,0 @@ -60,43 +60,64 @@ Size=1000,800 Collapsed=0 [Window][Camera] -Pos=0,46 -Size=304,739 +Pos=0,48 +Size=304,747 Collapsed=0 -DockId=0x00000001,1 +DockId=0x00000007,1 [Window][Environment] -Pos=1573,46 -Size=347,960 +Pos=1553,48 +Size=347,747 Collapsed=0 -DockId=0x00000008,1 +DockId=0x00000010,1 [Window][Project Manager] Pos=787,785 Size=784,221 Collapsed=0 -DockId=0x00000006,1 +DockId=0xD71539A0,1 [Window][Game Viewport] -Pos=306,46 -Size=1265,739 +Pos=304,48 +Size=1249,747 Collapsed=0 -DockId=0x00000002,1 +DockId=0x0000000F,1 [Window][Project Settings] -Pos=306,46 -Size=1265,739 +Pos=304,48 +Size=1249,747 Collapsed=0 -DockId=0x00000002,2 +DockId=0x0000000F,2 + +[Window][Animation] +Pos=583,795 +Size=738,227 +Collapsed=0 +DockId=0x00000013,0 + +[Window][Scripting] +Pos=304,48 +Size=1249,747 +Collapsed=0 +DockId=0x0000000F,3 + +[Table][0xFF88847C,2] +RefScale=16 +Column 0 Width=220 +Column 1 Weight=1.0000 [Docking][Data] -DockSpace ID=0xD71539A0 Window=0x3DA2F1DE Pos=0,46 Size=1920,960 Split=X - DockNode ID=0x00000007 Parent=0xD71539A0 SizeRef=1509,1015 Split=Y - DockNode ID=0x00000003 Parent=0x00000007 SizeRef=1858,739 Split=X - DockNode ID=0x00000001 Parent=0x00000003 SizeRef=304,758 Selected=0xBABDAE5E - DockNode ID=0x00000002 Parent=0x00000003 SizeRef=694,758 CentralNode=1 Selected=0xC450F867 - DockNode ID=0x00000004 Parent=0x00000007 SizeRef=1858,219 Split=X Selected=0xEA83D666 - DockNode ID=0x00000005 Parent=0x00000004 SizeRef=929,221 Selected=0xEA83D666 - DockNode ID=0x00000006 Parent=0x00000004 SizeRef=927,221 Selected=0x9C21DE82 - DockNode ID=0x00000008 Parent=0xD71539A0 SizeRef=347,1015 Selected=0x36DC96AB +DockSpace ID=0xD71539A0 Window=0x3DA2F1DE Pos=0,48 Size=1900,974 Split=Y + DockNode ID=0x00000005 Parent=0xD71539A0 SizeRef=1249,733 Split=Y Selected=0xC450F867 + DockNode ID=0x00000001 Parent=0x00000005 SizeRef=1900,747 Split=X Selected=0xE9044848 + DockNode ID=0x00000007 Parent=0x00000001 SizeRef=304,486 Selected=0xBABDAE5E + DockNode ID=0x00000008 Parent=0x00000001 SizeRef=1596,486 Split=X Selected=0xE9044848 + DockNode ID=0x0000000F Parent=0x00000008 SizeRef=1249,486 Selected=0xE9044848 + DockNode ID=0x00000010 Parent=0x00000008 SizeRef=347,486 Selected=0x36DC96AB + DockNode ID=0x00000002 Parent=0x00000005 SizeRef=1900,227 Split=X Selected=0x3F1379AF + DockNode ID=0x00000013 Parent=0x00000002 SizeRef=939,488 CentralNode=1 Selected=0x9C21DE82 + DockNode ID=0x00000014 Parent=0x00000002 SizeRef=961,488 Selected=0xEA83D666 + DockNode ID=0x00000006 Parent=0xD71539A0 SizeRef=1249,241 Split=X Selected=0x9C21DE82 + DockNode ID=0x00000009 Parent=0x00000006 SizeRef=383,278 Selected=0x9C21DE82 + DockNode ID=0x0000000A Parent=0x00000006 SizeRef=866,278 Selected=0xEA83D666 diff --git a/Resources/scripter.ini b/Resources/scripter.ini new file mode 100644 index 0000000..fdaae38 --- /dev/null +++ b/Resources/scripter.ini @@ -0,0 +1,121 @@ +[Window][Debug##Default] +Pos=60,60 +Size=400,400 +Collapsed=0 + +[Window][Modularity - Project Launcher] +Pos=569,288 +Size=720,480 +Collapsed=0 + +[Window][New Project] +Pos=679,403 +Size=500,250 +Collapsed=0 + +[Window][DockSpace] +Pos=0,24 +Size=1900,998 +Collapsed=0 + +[Window][Viewport] +Pos=260,48 +Size=741,772 +Collapsed=0 +DockId=0x00000003,0 + +[Window][Hierarchy] +Pos=0,48 +Size=260,772 +Collapsed=0 +DockId=0x00000007,1 + +[Window][Inspector] +Pos=1001,48 +Size=899,772 +Collapsed=0 +DockId=0x00000004,1 + +[Window][File Browser] +Pos=756,836 +Size=753,221 +Collapsed=0 +DockId=0xD71539A0,1 + +[Window][Console] +Pos=0,48 +Size=260,772 +Collapsed=0 +DockId=0x00000007,0 + +[Window][Project] +Pos=0,820 +Size=1900,202 +Collapsed=0 +DockId=0x0000000C,0 + +[Window][Launcher] +Pos=0,0 +Size=1900,1022 +Collapsed=0 + +[Window][Camera] +Pos=0,48 +Size=304,747 +Collapsed=0 +DockId=0x00000005,1 + +[Window][Environment] +Pos=1553,48 +Size=347,747 +Collapsed=0 +DockId=0x00000005,1 + +[Window][Project Manager] +Pos=787,785 +Size=784,221 +Collapsed=0 +DockId=0xD71539A0,1 + +[Window][Game Viewport] +Pos=260,48 +Size=741,772 +Collapsed=0 +DockId=0x00000003,1 + +[Window][Project Settings] +Pos=1201,48 +Size=699,772 +Collapsed=0 +DockId=0x00000004,2 + +[Window][Animation] +Pos=583,795 +Size=738,227 +Collapsed=0 +DockId=0x00000003,0 + +[Window][Scripting] +Pos=1001,48 +Size=899,772 +Collapsed=0 +DockId=0x00000004,0 + +[Table][0xFF88847C,2] +RefScale=16 +Column 0 Width=220 +Column 1 Weight=1.0000 + +[Docking][Data] +DockSpace ID=0xD71539A0 Window=0x3DA2F1DE Pos=0,48 Size=1900,974 Split=Y + DockNode ID=0x00000005 Parent=0xD71539A0 SizeRef=1249,733 Split=Y Selected=0xC450F867 + DockNode ID=0x0000000B Parent=0x00000005 SizeRef=1900,772 Split=X Selected=0xE9044848 + DockNode ID=0x00000007 Parent=0x0000000B SizeRef=260,114 Selected=0xBABDAE5E + DockNode ID=0x00000008 Parent=0x0000000B SizeRef=1640,114 Split=X Selected=0xE9044848 + DockNode ID=0x00000003 Parent=0x00000008 SizeRef=741,114 CentralNode=1 Selected=0xC450F867 + DockNode ID=0x00000004 Parent=0x00000008 SizeRef=899,114 Selected=0xBC881222 + DockNode ID=0x0000000C Parent=0x00000005 SizeRef=1900,202 Selected=0x9C21DE82 + DockNode ID=0x00000006 Parent=0xD71539A0 SizeRef=1249,241 Split=X Selected=0x9C21DE82 + DockNode ID=0x00000009 Parent=0x00000006 SizeRef=383,278 Selected=0x9C21DE82 + DockNode ID=0x0000000A Parent=0x00000006 SizeRef=866,278 Selected=0xEA83D666 + diff --git a/Scripts/AnimationWindow.cpp b/Scripts/AnimationWindow.cpp new file mode 100644 index 0000000..ce08b3f --- /dev/null +++ b/Scripts/AnimationWindow.cpp @@ -0,0 +1,282 @@ +#include "ScriptRuntime.h" +#include "SceneObject.h" +#include "ThirdParty/imgui/imgui.h" +#include +#include +#include +#include + +namespace { +struct Keyframe { + float time = 0.0f; + glm::vec3 position = glm::vec3(0.0f); + glm::vec3 rotation = glm::vec3(0.0f); + glm::vec3 scale = glm::vec3(1.0f); +}; + +int targetId = -1; +char targetName[128] = ""; +std::vector keyframes; +int selectedKey = -1; + +float clipLength = 2.0f; +float currentTime = 0.0f; +float playSpeed = 1.0f; +bool isPlaying = false; +bool loop = true; +bool applyOnScrub = true; + +glm::vec3 lerpVec3(const glm::vec3& a, const glm::vec3& b, float t) { + return a + (b - a) * t; +} + +float clampFloat(float value, float minValue, float maxValue) { + return std::max(minValue, std::min(value, maxValue)); +} + +SceneObject* resolveTarget(ScriptContext& ctx) { + if (targetId >= 0) { + if (auto* obj = ctx.FindObjectById(targetId)) { + return obj; + } + } + if (targetName[0] != '\0') { + if (auto* obj = ctx.FindObjectByName(targetName)) { + targetId = obj->id; + return obj; + } + } + return nullptr; +} + +void syncTargetLabel(SceneObject* obj) { + if (!obj) return; + strncpy(targetName, obj->name.c_str(), sizeof(targetName) - 1); + targetName[sizeof(targetName) - 1] = '\0'; +} + +void captureKeyframe(SceneObject& obj, float time) { + float clamped = clampFloat(time, 0.0f, clipLength); + auto it = std::find_if(keyframes.begin(), keyframes.end(), + [&](const Keyframe& k) { return std::abs(k.time - clamped) < 0.0001f; }); + if (it == keyframes.end()) { + keyframes.push_back(Keyframe{clamped, obj.position, obj.rotation, obj.scale}); + } else { + it->position = obj.position; + it->rotation = obj.rotation; + it->scale = obj.scale; + } + std::sort(keyframes.begin(), keyframes.end(), + [](const Keyframe& a, const Keyframe& b) { return a.time < b.time; }); +} + +void deleteKeyframe(int index) { + if (index < 0 || index >= static_cast(keyframes.size())) return; + keyframes.erase(keyframes.begin() + index); + if (selectedKey == index) selectedKey = -1; + if (selectedKey > index) selectedKey--; +} + +void applyPoseAtTime(ScriptContext& ctx, SceneObject& obj, float time) { + if (keyframes.empty()) return; + + if (time <= keyframes.front().time) { + ctx.SetPosition(keyframes.front().position); + ctx.SetRotation(keyframes.front().rotation); + ctx.SetScale(keyframes.front().scale); + ctx.MarkDirty(); + return; + } + if (time >= keyframes.back().time) { + ctx.SetPosition(keyframes.back().position); + ctx.SetRotation(keyframes.back().rotation); + ctx.SetScale(keyframes.back().scale); + ctx.MarkDirty(); + return; + } + + for (size_t i = 0; i + 1 < keyframes.size(); ++i) { + const Keyframe& a = keyframes[i]; + const Keyframe& b = keyframes[i + 1]; + if (time >= a.time && time <= b.time) { + float span = b.time - a.time; + float t = (span > 0.0f) ? (time - a.time) / span : 0.0f; + ctx.SetPosition(lerpVec3(a.position, b.position, t)); + ctx.SetRotation(lerpVec3(a.rotation, b.rotation, t)); + ctx.SetScale(lerpVec3(a.scale, b.scale, t)); + ctx.MarkDirty(); + return; + } + } +} + +void drawTimeline(float& time, float length, int& selection) { + ImVec2 size = ImVec2(ImGui::GetContentRegionAvail().x, 70.0f); + ImVec2 start = ImGui::GetCursorScreenPos(); + ImGui::InvisibleButton("Timeline", size); + + ImDrawList* draw = ImGui::GetWindowDrawList(); + ImU32 bg = ImGui::GetColorU32(ImGuiCol_FrameBg); + ImU32 border = ImGui::GetColorU32(ImGuiCol_Border); + ImU32 accent = ImGui::GetColorU32(ImGuiCol_CheckMark); + ImU32 keyColor = ImGui::GetColorU32(ImGuiCol_SliderGrab); + + draw->AddRectFilled(start, ImVec2(start.x + size.x, start.y + size.y), bg, 6.0f); + draw->AddRect(start, ImVec2(start.x + size.x, start.y + size.y), border, 6.0f); + + float clamped = clampFloat(time, 0.0f, length); + float playheadX = start.x + (length > 0.0f ? (clamped / length) * size.x : 0.0f); + draw->AddLine(ImVec2(playheadX, start.y), ImVec2(playheadX, start.y + size.y), accent, 2.0f); + + for (size_t i = 0; i < keyframes.size(); ++i) { + float keyX = start.x + (length > 0.0f ? (keyframes[i].time / length) * size.x : 0.0f); + ImVec2 center(keyX, start.y + size.y * 0.5f); + float radius = (selection == static_cast(i)) ? 6.0f : 4.5f; + draw->AddCircleFilled(center, radius, keyColor); + ImRect hit(ImVec2(center.x - 7.0f, center.y - 7.0f), ImVec2(center.x + 7.0f, center.y + 7.0f)); + if (ImGui::IsMouseHoveringRect(hit.Min, hit.Max) && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + selection = static_cast(i); + time = keyframes[i].time; + } + } + + if (ImGui::IsItemActive() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + float mouseX = ImGui::GetIO().MousePos.x; + float t = (mouseX - start.x) / size.x; + time = clampFloat(t * length, 0.0f, length); + } +} + +void drawKeyframeTable() { + if (keyframes.empty()) { + ImGui::TextDisabled("No keyframes yet."); + return; + } + + if (ImGui::BeginTable("KeyframeTable", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit)) { + ImGui::TableSetupColumn("Time"); + ImGui::TableSetupColumn("Position"); + ImGui::TableSetupColumn("Rotation"); + ImGui::TableSetupColumn("Scale"); + ImGui::TableHeadersRow(); + + for (size_t i = 0; i < keyframes.size(); ++i) { + const auto& key = keyframes[i]; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + bool selected = selectedKey == static_cast(i); + std::string label = std::to_string(key.time); + if (ImGui::Selectable(label.c_str(), selected, ImGuiSelectableFlags_SpanAllColumns)) { + selectedKey = static_cast(i); + currentTime = key.time; + } + ImGui::TableNextColumn(); + ImGui::Text("%.2f, %.2f, %.2f", key.position.x, key.position.y, key.position.z); + ImGui::TableNextColumn(); + ImGui::Text("%.2f, %.2f, %.2f", key.rotation.x, key.rotation.y, key.rotation.z); + ImGui::TableNextColumn(); + ImGui::Text("%.2f, %.2f, %.2f", key.scale.x, key.scale.y, key.scale.z); + } + ImGui::EndTable(); + } +} +} // namespace + +extern "C" void RenderEditorWindow(ScriptContext& ctx) { + ImGui::TextUnformatted("Simple Animation"); + ImGui::Separator(); + + SceneObject* selectedObj = ctx.object; + SceneObject* targetObj = resolveTarget(ctx); + + ImGui::TextDisabled("Select a GameObject to animate:"); + ImGui::BeginDisabled(); + ImGui::InputText("##TargetName", targetName, sizeof(targetName)); + ImGui::EndDisabled(); + ImGui::SameLine(); + if (ImGui::Button("Use Selected") && selectedObj) { + targetId = selectedObj->id; + syncTargetLabel(selectedObj); + } + ImGui::SameLine(); + if (ImGui::Button("Clear")) { + targetId = -1; + targetName[0] = '\0'; + targetObj = nullptr; + } + + ImGui::Spacing(); + if (ImGui::BeginTabBar("AnimModeTabs")) { + if (ImGui::BeginTabItem("Pose Mode")) { + ImGui::TextDisabled("Pose Editor"); + ImGui::Separator(); + + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f); + if (ImGui::Button("Key")) { + if (targetObj) captureKeyframe(*targetObj, currentTime); + } + ImGui::SameLine(); + if (ImGui::Button("Delete") && selectedKey >= 0) { + deleteKeyframe(selectedKey); + } + ImGui::PopStyleVar(); + + ImGui::Spacing(); + drawTimeline(currentTime, clipLength, selectedKey); + ImGui::SliderFloat("Time", ¤tTime, 0.0f, clipLength, "%.2fs"); + + ImGui::Spacing(); + drawKeyframeTable(); + + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Config Mode")) { + ImGui::TextDisabled("Playback"); + ImGui::Separator(); + ImGui::Checkbox("Loop", &loop); + ImGui::Checkbox("Apply On Scrub", &applyOnScrub); + ImGui::SliderFloat("Length", &clipLength, 0.1f, 20.0f, "%.2fs"); + ImGui::SliderFloat("Speed", &playSpeed, 0.1f, 4.0f, "%.2fx"); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::TextDisabled("Transport"); + if (ImGui::Button(isPlaying ? "Pause" : "Play")) { + isPlaying = !isPlaying; + } + ImGui::SameLine(); + if (ImGui::Button("Stop")) { + isPlaying = false; + currentTime = 0.0f; + } + + if (targetObj) { + ImGui::SameLine(); + ImGui::TextDisabled("Target: %s", targetObj->name.c_str()); + } else { + ImGui::TextDisabled("No target selected."); + } + + if (isPlaying && clipLength > 0.0f) { + currentTime += ImGui::GetIO().DeltaTime * playSpeed; + if (currentTime > clipLength) { + if (loop) currentTime = std::fmod(currentTime, clipLength); + else { + currentTime = clipLength; + isPlaying = false; + } + } + } + + if (targetObj && (isPlaying || applyOnScrub)) { + applyPoseAtTime(ctx, *targetObj, currentTime); + } +} + +extern "C" void ExitRenderEditorWindow(ScriptContext& ctx) { + (void)ctx; +} diff --git a/Scripts/Managed/ModuCPP.cs b/Scripts/Managed/ModuCPP.cs new file mode 100644 index 0000000..971496e --- /dev/null +++ b/Scripts/Managed/ModuCPP.cs @@ -0,0 +1,297 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace ModuCPP { + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void ScriptTickDelegate(IntPtr ctx, float deltaTime); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void ScriptInspectorDelegate(IntPtr ctx); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void SetNativeApiDelegate(IntPtr apiPtr); + + [StructLayout(LayoutKind.Sequential)] + public struct Vec3 { + public float X; + public float Y; + public float Z; + + public Vec3(float x, float y, float z) { + X = x; + Y = y; + Z = z; + } + + public static Vec3 operator +(Vec3 a, Vec3 b) => new Vec3(a.X + b.X, a.Y + b.Y, a.Z + b.Z); + public static Vec3 operator -(Vec3 a, Vec3 b) => new Vec3(a.X - b.X, a.Y - b.Y, a.Z - b.Z); + public static Vec3 operator *(Vec3 a, float s) => new Vec3(a.X * s, a.Y * s, a.Z * s); + } + + public enum ConsoleMessageType { + Info = 0, + Warning = 1, + Error = 2, + Success = 3 + } + + [StructLayout(LayoutKind.Sequential)] + public struct NativeApi { + public uint Version; + public IntPtr GetObjectId; + public IntPtr GetPosition; + public IntPtr SetPosition; + public IntPtr GetRotation; + public IntPtr SetRotation; + public IntPtr GetScale; + public IntPtr SetScale; + public IntPtr HasRigidbody; + public IntPtr EnsureRigidbody; + public IntPtr SetRigidbodyVelocity; + public IntPtr GetRigidbodyVelocity; + public IntPtr AddRigidbodyForce; + public IntPtr AddRigidbodyImpulse; + public IntPtr GetSettingFloat; + public IntPtr GetSettingBool; + public IntPtr GetSettingString; + public IntPtr SetSettingFloat; + public IntPtr SetSettingBool; + public IntPtr SetSettingString; + public IntPtr AddConsoleMessage; + } + + internal unsafe static class Native { + public static NativeApi Api; + public static GetObjectIdFn GetObjectId; + public static GetPositionFn GetPosition; + public static SetPositionFn SetPosition; + public static GetRotationFn GetRotation; + public static SetRotationFn SetRotation; + public static GetScaleFn GetScale; + public static SetScaleFn SetScale; + public static HasRigidbodyFn HasRigidbody; + public static EnsureRigidbodyFn EnsureRigidbody; + public static SetRigidbodyVelocityFn SetRigidbodyVelocity; + public static GetRigidbodyVelocityFn GetRigidbodyVelocity; + public static AddRigidbodyForceFn AddRigidbodyForce; + public static AddRigidbodyImpulseFn AddRigidbodyImpulse; + public static GetSettingFloatFn GetSettingFloat; + public static GetSettingBoolFn GetSettingBool; + public static GetSettingStringFn GetSettingString; + public static SetSettingFloatFn SetSettingFloat; + public static SetSettingBoolFn SetSettingBool; + public static SetSettingStringFn SetSettingString; + public static AddConsoleMessageFn AddConsoleMessage; + + public static void BindDelegates() { + GetObjectId = Marshal.GetDelegateForFunctionPointer(Api.GetObjectId); + GetPosition = Marshal.GetDelegateForFunctionPointer(Api.GetPosition); + SetPosition = Marshal.GetDelegateForFunctionPointer(Api.SetPosition); + GetRotation = Marshal.GetDelegateForFunctionPointer(Api.GetRotation); + SetRotation = Marshal.GetDelegateForFunctionPointer(Api.SetRotation); + GetScale = Marshal.GetDelegateForFunctionPointer(Api.GetScale); + SetScale = Marshal.GetDelegateForFunctionPointer(Api.SetScale); + HasRigidbody = Marshal.GetDelegateForFunctionPointer(Api.HasRigidbody); + EnsureRigidbody = Marshal.GetDelegateForFunctionPointer(Api.EnsureRigidbody); + SetRigidbodyVelocity = Marshal.GetDelegateForFunctionPointer(Api.SetRigidbodyVelocity); + GetRigidbodyVelocity = Marshal.GetDelegateForFunctionPointer(Api.GetRigidbodyVelocity); + AddRigidbodyForce = Marshal.GetDelegateForFunctionPointer(Api.AddRigidbodyForce); + AddRigidbodyImpulse = Marshal.GetDelegateForFunctionPointer(Api.AddRigidbodyImpulse); + GetSettingFloat = Marshal.GetDelegateForFunctionPointer(Api.GetSettingFloat); + GetSettingBool = Marshal.GetDelegateForFunctionPointer(Api.GetSettingBool); + GetSettingString = Marshal.GetDelegateForFunctionPointer(Api.GetSettingString); + SetSettingFloat = Marshal.GetDelegateForFunctionPointer(Api.SetSettingFloat); + SetSettingBool = Marshal.GetDelegateForFunctionPointer(Api.SetSettingBool); + SetSettingString = Marshal.GetDelegateForFunctionPointer(Api.SetSettingString); + AddConsoleMessage = Marshal.GetDelegateForFunctionPointer(Api.AddConsoleMessage); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate int GetObjectIdFn(IntPtr ctx); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void GetPositionFn(IntPtr ctx, float* x, float* y, float* z); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void SetPositionFn(IntPtr ctx, float x, float y, float z); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void GetRotationFn(IntPtr ctx, float* x, float* y, float* z); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void SetRotationFn(IntPtr ctx, float x, float y, float z); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void GetScaleFn(IntPtr ctx, float* x, float* y, float* z); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void SetScaleFn(IntPtr ctx, float x, float y, float z); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate int HasRigidbodyFn(IntPtr ctx); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate int EnsureRigidbodyFn(IntPtr ctx, int useGravity, int kinematic); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate int SetRigidbodyVelocityFn(IntPtr ctx, float x, float y, float z); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate int GetRigidbodyVelocityFn(IntPtr ctx, float* x, float* y, float* z); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate int AddRigidbodyForceFn(IntPtr ctx, float x, float y, float z); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate int AddRigidbodyImpulseFn(IntPtr ctx, float x, float y, float z); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate float GetSettingFloatFn(IntPtr ctx, byte* key, float fallback); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate int GetSettingBoolFn(IntPtr ctx, byte* key, int fallback); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void GetSettingStringFn(IntPtr ctx, byte* key, byte* fallback, byte* outBuffer, int outBufferSize); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void SetSettingFloatFn(IntPtr ctx, byte* key, float value); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void SetSettingBoolFn(IntPtr ctx, byte* key, int value); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void SetSettingStringFn(IntPtr ctx, byte* key, byte* value); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void AddConsoleMessageFn(IntPtr ctx, byte* message, int type); + } + + public static unsafe class Host { + public static void SetNativeApi(IntPtr apiPtr) { + Native.Api = Marshal.PtrToStructure(apiPtr); + Native.BindDelegates(); + } + } + + public readonly unsafe struct Context { + private readonly IntPtr handle; + + public Context(IntPtr ctx) { + handle = ctx; + } + + public int ObjectId => Native.GetObjectId(handle); + + public Vec3 Position { + get { + float x = 0f, y = 0f, z = 0f; + Native.GetPosition(handle, &x, &y, &z); + return new Vec3(x, y, z); + } + set { + Native.SetPosition(handle, value.X, value.Y, value.Z); + } + } + + public Vec3 Rotation { + get { + float x = 0f, y = 0f, z = 0f; + Native.GetRotation(handle, &x, &y, &z); + return new Vec3(x, y, z); + } + set { + Native.SetRotation(handle, value.X, value.Y, value.Z); + } + } + + public Vec3 Scale { + get { + float x = 0f, y = 0f, z = 0f; + Native.GetScale(handle, &x, &y, &z); + return new Vec3(x, y, z); + } + set { + Native.SetScale(handle, value.X, value.Y, value.Z); + } + } + + public bool HasRigidbody => Native.HasRigidbody(handle) != 0; + + public bool EnsureRigidbody(bool useGravity = true, bool kinematic = false) { + return Native.EnsureRigidbody(handle, useGravity ? 1 : 0, kinematic ? 1 : 0) != 0; + } + + public Vec3 RigidbodyVelocity { + get { + float x = 0f, y = 0f, z = 0f; + if (Native.GetRigidbodyVelocity(handle, &x, &y, &z) == 0) { + return new Vec3(0f, 0f, 0f); + } + return new Vec3(x, y, z); + } + set { + Native.SetRigidbodyVelocity(handle, value.X, value.Y, value.Z); + } + } + + public void AddRigidbodyForce(Vec3 force) { + Native.AddRigidbodyForce(handle, force.X, force.Y, force.Z); + } + + public void AddRigidbodyImpulse(Vec3 impulse) { + Native.AddRigidbodyImpulse(handle, impulse.X, impulse.Y, impulse.Z); + } + + public float GetSettingFloat(string key, float fallback = 0f) { + byte[] keyBytes = Encoding.UTF8.GetBytes((key ?? string.Empty) + "\0"); + fixed (byte* keyPtr = keyBytes) { + return Native.GetSettingFloat(handle, keyPtr, fallback); + } + } + + public bool GetSettingBool(string key, bool fallback = false) { + byte[] keyBytes = Encoding.UTF8.GetBytes((key ?? string.Empty) + "\0"); + fixed (byte* keyPtr = keyBytes) { + int value = Native.GetSettingBool(handle, keyPtr, fallback ? 1 : 0); + return value != 0; + } + } + + public string GetSettingString(string key, string fallback = "") { + const int bufferSize = 256; + byte[] keyBytes = Encoding.UTF8.GetBytes((key ?? string.Empty) + "\0"); + byte[] fallbackBytes = Encoding.UTF8.GetBytes((fallback ?? string.Empty) + "\0"); + byte* buffer = stackalloc byte[bufferSize]; + fixed (byte* keyPtr = keyBytes) + fixed (byte* fallbackPtr = fallbackBytes) { + Native.GetSettingString(handle, keyPtr, fallbackPtr, buffer, bufferSize); + } + return FromUtf8(buffer); + } + + public void SetSettingFloat(string key, float value) { + byte[] keyBytes = Encoding.UTF8.GetBytes((key ?? string.Empty) + "\0"); + fixed (byte* keyPtr = keyBytes) { + Native.SetSettingFloat(handle, keyPtr, value); + } + } + + public void SetSettingBool(string key, bool value) { + byte[] keyBytes = Encoding.UTF8.GetBytes((key ?? string.Empty) + "\0"); + fixed (byte* keyPtr = keyBytes) { + Native.SetSettingBool(handle, keyPtr, value ? 1 : 0); + } + } + + public void SetSettingString(string key, string value) { + byte[] keyBytes = Encoding.UTF8.GetBytes((key ?? string.Empty) + "\0"); + byte[] valueBytes = Encoding.UTF8.GetBytes((value ?? string.Empty) + "\0"); + fixed (byte* keyPtr = keyBytes) + fixed (byte* valuePtr = valueBytes) { + Native.SetSettingString(handle, keyPtr, valuePtr); + } + } + + public void AddConsoleMessage(string message, ConsoleMessageType type = ConsoleMessageType.Info) { + byte[] msgBytes = Encoding.UTF8.GetBytes((message ?? string.Empty) + "\0"); + fixed (byte* msgPtr = msgBytes) { + Native.AddConsoleMessage(handle, msgPtr, (int)type); + } + } + + private static string FromUtf8(byte* ptr) { + if (ptr == null) return string.Empty; + int length = 0; + while (ptr[length] != 0) { + length++; + } + if (length == 0) return string.Empty; + byte[] bytes = new byte[length]; + Marshal.Copy((IntPtr)ptr, bytes, 0, length); + return Encoding.UTF8.GetString(bytes); + } + } +} diff --git a/Scripts/Managed/ModuCPP.csproj b/Scripts/Managed/ModuCPP.csproj new file mode 100644 index 0000000..eaa3ef9 --- /dev/null +++ b/Scripts/Managed/ModuCPP.csproj @@ -0,0 +1,10 @@ + + + netstandard2.0 + true + enable + enable + latest + false + + diff --git a/Scripts/Managed/SampleInspector.cs b/Scripts/Managed/SampleInspector.cs new file mode 100644 index 0000000..ff7870a --- /dev/null +++ b/Scripts/Managed/SampleInspector.cs @@ -0,0 +1,68 @@ +using System; + +namespace ModuCPP { + public static class SampleInspector { + private static bool autoRotate = false; + private static Vec3 spinSpeed = new Vec3(0f, 45f, 0f); + private static Vec3 offset = new Vec3(0f, 1f, 0f); + private static string targetName = "MyTarget"; // Stored for parity; object lookup API not wired yet. + + private static void LoadSettings(Context context) { + autoRotate = context.GetSettingBool("autoRotate", autoRotate); + spinSpeed = new Vec3( + context.GetSettingFloat("spinSpeedX", spinSpeed.X), + context.GetSettingFloat("spinSpeedY", spinSpeed.Y), + context.GetSettingFloat("spinSpeedZ", spinSpeed.Z) + ); + offset = new Vec3( + context.GetSettingFloat("offsetX", offset.X), + context.GetSettingFloat("offsetY", offset.Y), + context.GetSettingFloat("offsetZ", offset.Z) + ); + targetName = context.GetSettingString("targetName", targetName); + } + + private static void SaveSettings(Context context) { + context.SetSettingBool("autoRotate", autoRotate); + context.SetSettingFloat("spinSpeedX", spinSpeed.X); + context.SetSettingFloat("spinSpeedY", spinSpeed.Y); + context.SetSettingFloat("spinSpeedZ", spinSpeed.Z); + context.SetSettingFloat("offsetX", offset.X); + context.SetSettingFloat("offsetY", offset.Y); + context.SetSettingFloat("offsetZ", offset.Z); + context.SetSettingString("targetName", targetName); + } + + private static void ApplyAutoRotate(Context context, float deltaTime) { + if (!autoRotate) return; + context.Rotation = context.Rotation + (spinSpeed * deltaTime); + } + + public static void Script_Begin(IntPtr ctx, float deltaTime) { + var context = new Context(ctx); + LoadSettings(context); + SaveSettings(context); + context.EnsureRigidbody(useGravity: true, kinematic: false); + context.AddConsoleMessage("Managed script begin (C#)", ConsoleMessageType.Info); + } + + public static void Script_OnInspector(IntPtr ctx) { + var context = new Context(ctx); + LoadSettings(context); + SaveSettings(context); + context.AddConsoleMessage("Managed inspector hook (no UI yet)", ConsoleMessageType.Info); + } + + public static void Script_Spec(IntPtr ctx, float deltaTime) { + ApplyAutoRotate(new Context(ctx), deltaTime); + } + + public static void Script_TestEditor(IntPtr ctx, float deltaTime) { + ApplyAutoRotate(new Context(ctx), deltaTime); + } + + public static void Script_TickUpdate(IntPtr ctx, float deltaTime) { + ApplyAutoRotate(new Context(ctx), deltaTime); + } + } +} diff --git a/Scripts/Managed/SampleInspectorManaged.cs b/Scripts/Managed/SampleInspectorManaged.cs new file mode 100644 index 0000000..fbe2889 --- /dev/null +++ b/Scripts/Managed/SampleInspectorManaged.cs @@ -0,0 +1,68 @@ +using System; + +namespace ModuCPP { + public static class SampleInspectorManaged { + private static bool autoRotate = false; + private static Vec3 spinSpeed = new Vec3(0f, 45f, 0f); + private static Vec3 offset = new Vec3(0f, 1f, 0f); + private static string targetName = "MyTarget"; // Stored for parity; object lookup API not wired yet. + + private static void LoadSettings(Context context) { + autoRotate = context.GetSettingBool("autoRotate", autoRotate); + spinSpeed = new Vec3( + context.GetSettingFloat("spinSpeedX", spinSpeed.X), + context.GetSettingFloat("spinSpeedY", spinSpeed.Y), + context.GetSettingFloat("spinSpeedZ", spinSpeed.Z) + ); + offset = new Vec3( + context.GetSettingFloat("offsetX", offset.X), + context.GetSettingFloat("offsetY", offset.Y), + context.GetSettingFloat("offsetZ", offset.Z) + ); + targetName = context.GetSettingString("targetName", targetName); + } + + private static void SaveSettings(Context context) { + context.SetSettingBool("autoRotate", autoRotate); + context.SetSettingFloat("spinSpeedX", spinSpeed.X); + context.SetSettingFloat("spinSpeedY", spinSpeed.Y); + context.SetSettingFloat("spinSpeedZ", spinSpeed.Z); + context.SetSettingFloat("offsetX", offset.X); + context.SetSettingFloat("offsetY", offset.Y); + context.SetSettingFloat("offsetZ", offset.Z); + context.SetSettingString("targetName", targetName); + } + + private static void ApplyAutoRotate(Context context, float deltaTime) { + if (!autoRotate) return; + context.Rotation = context.Rotation + (spinSpeed * deltaTime); + } + + public static void Script_Begin(IntPtr ctx, float deltaTime) { + var context = new Context(ctx); + LoadSettings(context); + SaveSettings(context); + context.EnsureRigidbody(useGravity: true, kinematic: false); + context.AddConsoleMessage("Managed script begin (C#)", ConsoleMessageType.Info); + } + + public static void Script_OnInspector(IntPtr ctx) { + var context = new Context(ctx); + LoadSettings(context); + SaveSettings(context); + context.AddConsoleMessage("Managed inspector hook (no UI yet)", ConsoleMessageType.Info); + } + + public static void Script_Spec(IntPtr ctx, float deltaTime) { + ApplyAutoRotate(new Context(ctx), deltaTime); + } + + public static void Script_TestEditor(IntPtr ctx, float deltaTime) { + ApplyAutoRotate(new Context(ctx), deltaTime); + } + + public static void Script_TickUpdate(IntPtr ctx, float deltaTime) { + ApplyAutoRotate(new Context(ctx), deltaTime); + } + } +} diff --git a/Scripts/Managed/bin/Debug/net10.0/ModuCPP.deps.json b/Scripts/Managed/bin/Debug/net10.0/ModuCPP.deps.json new file mode 100644 index 0000000..839fee7 --- /dev/null +++ b/Scripts/Managed/bin/Debug/net10.0/ModuCPP.deps.json @@ -0,0 +1,23 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v10.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v10.0": { + "ModuCPP/1.0.0": { + "runtime": { + "ModuCPP.dll": {} + } + } + } + }, + "libraries": { + "ModuCPP/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + } + } +} \ No newline at end of file diff --git a/Scripts/Managed/bin/Debug/net10.0/ModuCPP.dll b/Scripts/Managed/bin/Debug/net10.0/ModuCPP.dll new file mode 100644 index 0000000..37dc751 Binary files /dev/null and b/Scripts/Managed/bin/Debug/net10.0/ModuCPP.dll differ diff --git a/Scripts/Managed/bin/Debug/net10.0/ModuCPP.pdb b/Scripts/Managed/bin/Debug/net10.0/ModuCPP.pdb new file mode 100644 index 0000000..ad1bcb6 Binary files /dev/null and b/Scripts/Managed/bin/Debug/net10.0/ModuCPP.pdb differ diff --git a/Scripts/Managed/bin/Debug/net10.0/ModuCPP.runtimeconfig.json b/Scripts/Managed/bin/Debug/net10.0/ModuCPP.runtimeconfig.json new file mode 100644 index 0000000..c121312 --- /dev/null +++ b/Scripts/Managed/bin/Debug/net10.0/ModuCPP.runtimeconfig.json @@ -0,0 +1,14 @@ +{ + "runtimeOptions": { + "tfm": "net10.0", + "framework": { + "name": "Microsoft.NETCore.App", + "version": "10.0.0" + }, + "configProperties": { + "System.Runtime.InteropServices.EnableComHosting": false, + "System.Runtime.InteropServices.BuiltInComInterop": false, + "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false + } + } +} \ No newline at end of file diff --git a/Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.deps.json b/Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.deps.json new file mode 100644 index 0000000..b71df7c --- /dev/null +++ b/Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.deps.json @@ -0,0 +1,24 @@ +{ + "runtimeTarget": { + "name": ".NETStandard,Version=v2.0/", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETStandard,Version=v2.0": {}, + ".NETStandard,Version=v2.0/": { + "ModuCPP/1.0.0": { + "runtime": { + "ModuCPP.dll": {} + } + } + } + }, + "libraries": { + "ModuCPP/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + } + } +} \ No newline at end of file diff --git a/Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.dll b/Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.dll new file mode 100644 index 0000000..7da4679 Binary files /dev/null and b/Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.dll differ diff --git a/Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.pdb b/Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.pdb new file mode 100644 index 0000000..466b412 Binary files /dev/null and b/Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.pdb differ diff --git a/Scripts/Managed/obj/Debug/net10.0/.NETCoreApp,Version=v10.0.AssemblyAttributes.cs b/Scripts/Managed/obj/Debug/net10.0/.NETCoreApp,Version=v10.0.AssemblyAttributes.cs new file mode 100644 index 0000000..925b135 --- /dev/null +++ b/Scripts/Managed/obj/Debug/net10.0/.NETCoreApp,Version=v10.0.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v10.0", FrameworkDisplayName = ".NET 10.0")] diff --git a/Scripts/Managed/obj/Debug/net10.0/ModuCPP.AssemblyInfo.cs b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.AssemblyInfo.cs new file mode 100644 index 0000000..1152aad --- /dev/null +++ b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.AssemblyInfo.cs @@ -0,0 +1,22 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: System.Reflection.AssemblyCompanyAttribute("ModuCPP")] +[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] +[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+2061d588e7a10416f073bb34ad8bda8e068f291b")] +[assembly: System.Reflection.AssemblyProductAttribute("ModuCPP")] +[assembly: System.Reflection.AssemblyTitleAttribute("ModuCPP")] +[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] + +// Generated by the MSBuild WriteCodeFragment class. + diff --git a/Scripts/Managed/obj/Debug/net10.0/ModuCPP.AssemblyInfoInputs.cache b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.AssemblyInfoInputs.cache new file mode 100644 index 0000000..c38ab4c --- /dev/null +++ b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.AssemblyInfoInputs.cache @@ -0,0 +1 @@ +78499018a8a6914630a79de98a62c3a139d45e8a04deb724bf7e5060d9670375 diff --git a/Scripts/Managed/obj/Debug/net10.0/ModuCPP.GeneratedMSBuildEditorConfig.editorconfig b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.GeneratedMSBuildEditorConfig.editorconfig new file mode 100644 index 0000000..10be183 --- /dev/null +++ b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.GeneratedMSBuildEditorConfig.editorconfig @@ -0,0 +1,17 @@ +is_global = true +build_property.TargetFramework = net10.0 +build_property.TargetFrameworkIdentifier = .NETCoreApp +build_property.TargetFrameworkVersion = v10.0 +build_property.TargetPlatformMinVersion = +build_property.UsingMicrosoftNETSdkWeb = +build_property.ProjectTypeGuids = +build_property.InvariantGlobalization = +build_property.PlatformNeutralAssembly = +build_property.EnforceExtendedAnalyzerRules = +build_property._SupportedPlatformList = Linux,macOS,Windows +build_property.RootNamespace = ModuCPP +build_property.ProjectDir = /home/anemunt/Git-base/Modularity/Scripts/Managed/ +build_property.EnableComHosting = false +build_property.EnableGeneratedComInterfaceComImportInterop = +build_property.EffectiveAnalysisLevelStyle = 10.0 +build_property.EnableCodeStyleSeverity = diff --git a/Scripts/Managed/obj/Debug/net10.0/ModuCPP.GlobalUsings.g.cs b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.GlobalUsings.g.cs new file mode 100644 index 0000000..d12bcbc --- /dev/null +++ b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.GlobalUsings.g.cs @@ -0,0 +1,8 @@ +// +global using System; +global using System.Collections.Generic; +global using System.IO; +global using System.Linq; +global using System.Net.Http; +global using System.Threading; +global using System.Threading.Tasks; diff --git a/Scripts/Managed/obj/Debug/net10.0/ModuCPP.assets.cache b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.assets.cache new file mode 100644 index 0000000..1900c4e Binary files /dev/null and b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.assets.cache differ diff --git a/Scripts/Managed/obj/Debug/net10.0/ModuCPP.csproj.CoreCompileInputs.cache b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.csproj.CoreCompileInputs.cache new file mode 100644 index 0000000..d9e5ba5 --- /dev/null +++ b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.csproj.CoreCompileInputs.cache @@ -0,0 +1 @@ +0579f849781bddafc2e55261290008c5a7fb6bddd064d155e3e9c2dd44aec502 diff --git a/Scripts/Managed/obj/Debug/net10.0/ModuCPP.csproj.FileListAbsolute.txt b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.csproj.FileListAbsolute.txt new file mode 100644 index 0000000..b215bfb --- /dev/null +++ b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.csproj.FileListAbsolute.txt @@ -0,0 +1,14 @@ +/home/anemunt/Git-base/Modularity/Scripts/Managed/bin/Debug/net10.0/ModuCPP.deps.json +/home/anemunt/Git-base/Modularity/Scripts/Managed/bin/Debug/net10.0/ModuCPP.runtimeconfig.json +/home/anemunt/Git-base/Modularity/Scripts/Managed/bin/Debug/net10.0/ModuCPP.dll +/home/anemunt/Git-base/Modularity/Scripts/Managed/bin/Debug/net10.0/ModuCPP.pdb +/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/net10.0/ModuCPP.GeneratedMSBuildEditorConfig.editorconfig +/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/net10.0/ModuCPP.AssemblyInfoInputs.cache +/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/net10.0/ModuCPP.AssemblyInfo.cs +/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/net10.0/ModuCPP.csproj.CoreCompileInputs.cache +/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/net10.0/ModuCPP.sourcelink.json +/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/net10.0/ModuCPP.dll +/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/net10.0/refint/ModuCPP.dll +/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/net10.0/ModuCPP.pdb +/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/net10.0/ModuCPP.genruntimeconfig.cache +/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/net10.0/ref/ModuCPP.dll diff --git a/Scripts/Managed/obj/Debug/net10.0/ModuCPP.dll b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.dll new file mode 100644 index 0000000..37dc751 Binary files /dev/null and b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.dll differ diff --git a/Scripts/Managed/obj/Debug/net10.0/ModuCPP.genruntimeconfig.cache b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.genruntimeconfig.cache new file mode 100644 index 0000000..d79c22e --- /dev/null +++ b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.genruntimeconfig.cache @@ -0,0 +1 @@ +b14c7a505f46d8314ef755360e8bbee5cc4a67ee7d033805e0a7f8e8d9b71b40 diff --git a/Scripts/Managed/obj/Debug/net10.0/ModuCPP.pdb b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.pdb new file mode 100644 index 0000000..ad1bcb6 Binary files /dev/null and b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.pdb differ diff --git a/Scripts/Managed/obj/Debug/net10.0/ModuCPP.sourcelink.json b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.sourcelink.json new file mode 100644 index 0000000..b84340a --- /dev/null +++ b/Scripts/Managed/obj/Debug/net10.0/ModuCPP.sourcelink.json @@ -0,0 +1 @@ +{"documents":{"/home/anemunt/Git-base/Modularity/src/ThirdParty/PhysX/*":"https://raw.githubusercontent.com/NVIDIA-Omniverse/PhysX/09ff24f3279b735e672ff27b155cbf49f6296f4d/*"}} \ No newline at end of file diff --git a/Scripts/Managed/obj/Debug/net10.0/ref/ModuCPP.dll b/Scripts/Managed/obj/Debug/net10.0/ref/ModuCPP.dll new file mode 100644 index 0000000..d39b56d Binary files /dev/null and b/Scripts/Managed/obj/Debug/net10.0/ref/ModuCPP.dll differ diff --git a/Scripts/Managed/obj/Debug/net10.0/refint/ModuCPP.dll b/Scripts/Managed/obj/Debug/net10.0/refint/ModuCPP.dll new file mode 100644 index 0000000..d39b56d Binary files /dev/null and b/Scripts/Managed/obj/Debug/net10.0/refint/ModuCPP.dll differ diff --git a/Scripts/Managed/obj/Debug/netstandard2.0/.NETStandard,Version=v2.0.AssemblyAttributes.cs b/Scripts/Managed/obj/Debug/netstandard2.0/.NETStandard,Version=v2.0.AssemblyAttributes.cs new file mode 100644 index 0000000..8bf3a42 --- /dev/null +++ b/Scripts/Managed/obj/Debug/netstandard2.0/.NETStandard,Version=v2.0.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")] diff --git a/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.AssemblyInfo.cs b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.AssemblyInfo.cs new file mode 100644 index 0000000..1152aad --- /dev/null +++ b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.AssemblyInfo.cs @@ -0,0 +1,22 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: System.Reflection.AssemblyCompanyAttribute("ModuCPP")] +[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] +[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+2061d588e7a10416f073bb34ad8bda8e068f291b")] +[assembly: System.Reflection.AssemblyProductAttribute("ModuCPP")] +[assembly: System.Reflection.AssemblyTitleAttribute("ModuCPP")] +[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] + +// Generated by the MSBuild WriteCodeFragment class. + diff --git a/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.AssemblyInfoInputs.cache b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.AssemblyInfoInputs.cache new file mode 100644 index 0000000..c38ab4c --- /dev/null +++ b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.AssemblyInfoInputs.cache @@ -0,0 +1 @@ +78499018a8a6914630a79de98a62c3a139d45e8a04deb724bf7e5060d9670375 diff --git a/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.GeneratedMSBuildEditorConfig.editorconfig b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.GeneratedMSBuildEditorConfig.editorconfig new file mode 100644 index 0000000..c5dcee9 --- /dev/null +++ b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.GeneratedMSBuildEditorConfig.editorconfig @@ -0,0 +1,8 @@ +is_global = true +build_property.RootNamespace = ModuCPP +build_property.ProjectDir = /home/anemunt/Git-base/Modularity/Scripts/Managed/ +build_property.EnableComHosting = +build_property.EnableGeneratedComInterfaceComImportInterop = +build_property.CsWinRTUseWindowsUIXamlProjections = false +build_property.EffectiveAnalysisLevelStyle = +build_property.EnableCodeStyleSeverity = diff --git a/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.GlobalUsings.g.cs b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.GlobalUsings.g.cs new file mode 100644 index 0000000..d12bcbc --- /dev/null +++ b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.GlobalUsings.g.cs @@ -0,0 +1,8 @@ +// +global using System; +global using System.Collections.Generic; +global using System.IO; +global using System.Linq; +global using System.Net.Http; +global using System.Threading; +global using System.Threading.Tasks; diff --git a/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.assets.cache b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.assets.cache new file mode 100644 index 0000000..a8e6a3e Binary files /dev/null and b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.assets.cache differ diff --git a/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.csproj.AssemblyReference.cache b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.csproj.AssemblyReference.cache new file mode 100644 index 0000000..97fa88d Binary files /dev/null and b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.csproj.AssemblyReference.cache differ diff --git a/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.csproj.CoreCompileInputs.cache b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.csproj.CoreCompileInputs.cache new file mode 100644 index 0000000..7e62e7b --- /dev/null +++ b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.csproj.CoreCompileInputs.cache @@ -0,0 +1 @@ +12a173d9ad34d74a13f6f07a58c9a75f8033484b726d3271d6b9bdffb23c227b diff --git a/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.csproj.FileListAbsolute.txt b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.csproj.FileListAbsolute.txt new file mode 100644 index 0000000..989787d --- /dev/null +++ b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.csproj.FileListAbsolute.txt @@ -0,0 +1,11 @@ +/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.csproj.AssemblyReference.cache +/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.GeneratedMSBuildEditorConfig.editorconfig +/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.AssemblyInfoInputs.cache +/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.AssemblyInfo.cs +/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.csproj.CoreCompileInputs.cache +/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.sourcelink.json +/home/anemunt/Git-base/Modularity/Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.deps.json +/home/anemunt/Git-base/Modularity/Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.dll +/home/anemunt/Git-base/Modularity/Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.pdb +/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.dll +/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.pdb diff --git a/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.dll b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.dll new file mode 100644 index 0000000..7da4679 Binary files /dev/null and b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.dll differ diff --git a/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.pdb b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.pdb new file mode 100644 index 0000000..466b412 Binary files /dev/null and b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.pdb differ diff --git a/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.sourcelink.json b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.sourcelink.json new file mode 100644 index 0000000..b84340a --- /dev/null +++ b/Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.sourcelink.json @@ -0,0 +1 @@ +{"documents":{"/home/anemunt/Git-base/Modularity/src/ThirdParty/PhysX/*":"https://raw.githubusercontent.com/NVIDIA-Omniverse/PhysX/09ff24f3279b735e672ff27b155cbf49f6296f4d/*"}} \ No newline at end of file diff --git a/Scripts/Managed/obj/ModuCPP.csproj.nuget.dgspec.json b/Scripts/Managed/obj/ModuCPP.csproj.nuget.dgspec.json new file mode 100644 index 0000000..0c8bb0a --- /dev/null +++ b/Scripts/Managed/obj/ModuCPP.csproj.nuget.dgspec.json @@ -0,0 +1,70 @@ +{ + "format": 1, + "restore": { + "/home/anemunt/Git-base/Modularity/Scripts/Managed/ModuCPP.csproj": {} + }, + "projects": { + "/home/anemunt/Git-base/Modularity/Scripts/Managed/ModuCPP.csproj": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "/home/anemunt/Git-base/Modularity/Scripts/Managed/ModuCPP.csproj", + "projectName": "ModuCPP", + "projectPath": "/home/anemunt/Git-base/Modularity/Scripts/Managed/ModuCPP.csproj", + "packagesPath": "/home/anemunt/.nuget/packages/", + "outputPath": "/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/", + "projectStyle": "PackageReference", + "configFilePaths": [ + "/home/anemunt/.nuget/NuGet/NuGet.Config" + ], + "originalTargetFrameworks": [ + "netstandard2.0" + ], + "sources": { + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "netstandard2.0": { + "targetAlias": "netstandard2.0", + "projectReferences": {} + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + }, + "restoreAuditProperties": { + "enableAudit": "true", + "auditLevel": "low", + "auditMode": "direct" + }, + "SdkAnalysisLevel": "10.0.100" + }, + "frameworks": { + "netstandard2.0": { + "targetAlias": "netstandard2.0", + "dependencies": { + "NETStandard.Library": { + "suppressParent": "All", + "target": "Package", + "version": "[2.0.3, )", + "autoReferenced": true + } + }, + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "runtimeIdentifierGraphPath": "/usr/share/dotnet/sdk/10.0.100/RuntimeIdentifierGraph.json" + } + } + } + } +} \ No newline at end of file diff --git a/Scripts/Managed/obj/ModuCPP.csproj.nuget.g.props b/Scripts/Managed/obj/ModuCPP.csproj.nuget.g.props new file mode 100644 index 0000000..7751212 --- /dev/null +++ b/Scripts/Managed/obj/ModuCPP.csproj.nuget.g.props @@ -0,0 +1,15 @@ + + + + True + NuGet + $(MSBuildThisFileDirectory)project.assets.json + /home/anemunt/.nuget/packages/ + /home/anemunt/.nuget/packages/ + PackageReference + 7.0.0 + + + + + \ No newline at end of file diff --git a/Scripts/Managed/obj/ModuCPP.csproj.nuget.g.targets b/Scripts/Managed/obj/ModuCPP.csproj.nuget.g.targets new file mode 100644 index 0000000..8284cdf --- /dev/null +++ b/Scripts/Managed/obj/ModuCPP.csproj.nuget.g.targets @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Scripts/Managed/obj/project.assets.json b/Scripts/Managed/obj/project.assets.json new file mode 100644 index 0000000..eb7e6e2 --- /dev/null +++ b/Scripts/Managed/obj/project.assets.json @@ -0,0 +1,247 @@ +{ + "version": 3, + "targets": { + ".NETStandard,Version=v2.0": { + "Microsoft.NETCore.Platforms/1.1.0": { + "type": "package", + "compile": { + "lib/netstandard1.0/_._": {} + }, + "runtime": { + "lib/netstandard1.0/_._": {} + } + }, + "NETStandard.Library/2.0.3": { + "type": "package", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0" + }, + "compile": { + "lib/netstandard1.0/_._": {} + }, + "runtime": { + "lib/netstandard1.0/_._": {} + }, + "build": { + "build/netstandard2.0/NETStandard.Library.targets": {} + } + } + } + }, + "libraries": { + "Microsoft.NETCore.Platforms/1.1.0": { + "sha512": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==", + "type": "package", + "path": "microsoft.netcore.platforms/1.1.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "ThirdPartyNotices.txt", + "dotnet_library_license.txt", + "lib/netstandard1.0/_._", + "microsoft.netcore.platforms.1.1.0.nupkg.sha512", + "microsoft.netcore.platforms.nuspec", + "runtime.json" + ] + }, + "NETStandard.Library/2.0.3": { + "sha512": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", + "type": "package", + "path": "netstandard.library/2.0.3", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "build/netstandard2.0/NETStandard.Library.targets", + "build/netstandard2.0/ref/Microsoft.Win32.Primitives.dll", + "build/netstandard2.0/ref/System.AppContext.dll", + "build/netstandard2.0/ref/System.Collections.Concurrent.dll", + "build/netstandard2.0/ref/System.Collections.NonGeneric.dll", + "build/netstandard2.0/ref/System.Collections.Specialized.dll", + "build/netstandard2.0/ref/System.Collections.dll", + "build/netstandard2.0/ref/System.ComponentModel.Composition.dll", + "build/netstandard2.0/ref/System.ComponentModel.EventBasedAsync.dll", + "build/netstandard2.0/ref/System.ComponentModel.Primitives.dll", + "build/netstandard2.0/ref/System.ComponentModel.TypeConverter.dll", + "build/netstandard2.0/ref/System.ComponentModel.dll", + "build/netstandard2.0/ref/System.Console.dll", + "build/netstandard2.0/ref/System.Core.dll", + "build/netstandard2.0/ref/System.Data.Common.dll", + "build/netstandard2.0/ref/System.Data.dll", + "build/netstandard2.0/ref/System.Diagnostics.Contracts.dll", + "build/netstandard2.0/ref/System.Diagnostics.Debug.dll", + "build/netstandard2.0/ref/System.Diagnostics.FileVersionInfo.dll", + "build/netstandard2.0/ref/System.Diagnostics.Process.dll", + "build/netstandard2.0/ref/System.Diagnostics.StackTrace.dll", + "build/netstandard2.0/ref/System.Diagnostics.TextWriterTraceListener.dll", + "build/netstandard2.0/ref/System.Diagnostics.Tools.dll", + "build/netstandard2.0/ref/System.Diagnostics.TraceSource.dll", + "build/netstandard2.0/ref/System.Diagnostics.Tracing.dll", + "build/netstandard2.0/ref/System.Drawing.Primitives.dll", + "build/netstandard2.0/ref/System.Drawing.dll", + "build/netstandard2.0/ref/System.Dynamic.Runtime.dll", + "build/netstandard2.0/ref/System.Globalization.Calendars.dll", + "build/netstandard2.0/ref/System.Globalization.Extensions.dll", + "build/netstandard2.0/ref/System.Globalization.dll", + "build/netstandard2.0/ref/System.IO.Compression.FileSystem.dll", + "build/netstandard2.0/ref/System.IO.Compression.ZipFile.dll", + "build/netstandard2.0/ref/System.IO.Compression.dll", + "build/netstandard2.0/ref/System.IO.FileSystem.DriveInfo.dll", + "build/netstandard2.0/ref/System.IO.FileSystem.Primitives.dll", + "build/netstandard2.0/ref/System.IO.FileSystem.Watcher.dll", + "build/netstandard2.0/ref/System.IO.FileSystem.dll", + "build/netstandard2.0/ref/System.IO.IsolatedStorage.dll", + "build/netstandard2.0/ref/System.IO.MemoryMappedFiles.dll", + "build/netstandard2.0/ref/System.IO.Pipes.dll", + "build/netstandard2.0/ref/System.IO.UnmanagedMemoryStream.dll", + "build/netstandard2.0/ref/System.IO.dll", + "build/netstandard2.0/ref/System.Linq.Expressions.dll", + "build/netstandard2.0/ref/System.Linq.Parallel.dll", + "build/netstandard2.0/ref/System.Linq.Queryable.dll", + "build/netstandard2.0/ref/System.Linq.dll", + "build/netstandard2.0/ref/System.Net.Http.dll", + "build/netstandard2.0/ref/System.Net.NameResolution.dll", + "build/netstandard2.0/ref/System.Net.NetworkInformation.dll", + "build/netstandard2.0/ref/System.Net.Ping.dll", + "build/netstandard2.0/ref/System.Net.Primitives.dll", + "build/netstandard2.0/ref/System.Net.Requests.dll", + "build/netstandard2.0/ref/System.Net.Security.dll", + "build/netstandard2.0/ref/System.Net.Sockets.dll", + "build/netstandard2.0/ref/System.Net.WebHeaderCollection.dll", + "build/netstandard2.0/ref/System.Net.WebSockets.Client.dll", + "build/netstandard2.0/ref/System.Net.WebSockets.dll", + "build/netstandard2.0/ref/System.Net.dll", + "build/netstandard2.0/ref/System.Numerics.dll", + "build/netstandard2.0/ref/System.ObjectModel.dll", + "build/netstandard2.0/ref/System.Reflection.Extensions.dll", + "build/netstandard2.0/ref/System.Reflection.Primitives.dll", + "build/netstandard2.0/ref/System.Reflection.dll", + "build/netstandard2.0/ref/System.Resources.Reader.dll", + "build/netstandard2.0/ref/System.Resources.ResourceManager.dll", + "build/netstandard2.0/ref/System.Resources.Writer.dll", + "build/netstandard2.0/ref/System.Runtime.CompilerServices.VisualC.dll", + "build/netstandard2.0/ref/System.Runtime.Extensions.dll", + "build/netstandard2.0/ref/System.Runtime.Handles.dll", + "build/netstandard2.0/ref/System.Runtime.InteropServices.RuntimeInformation.dll", + "build/netstandard2.0/ref/System.Runtime.InteropServices.dll", + "build/netstandard2.0/ref/System.Runtime.Numerics.dll", + "build/netstandard2.0/ref/System.Runtime.Serialization.Formatters.dll", + "build/netstandard2.0/ref/System.Runtime.Serialization.Json.dll", + "build/netstandard2.0/ref/System.Runtime.Serialization.Primitives.dll", + "build/netstandard2.0/ref/System.Runtime.Serialization.Xml.dll", + "build/netstandard2.0/ref/System.Runtime.Serialization.dll", + "build/netstandard2.0/ref/System.Runtime.dll", + "build/netstandard2.0/ref/System.Security.Claims.dll", + "build/netstandard2.0/ref/System.Security.Cryptography.Algorithms.dll", + "build/netstandard2.0/ref/System.Security.Cryptography.Csp.dll", + "build/netstandard2.0/ref/System.Security.Cryptography.Encoding.dll", + "build/netstandard2.0/ref/System.Security.Cryptography.Primitives.dll", + "build/netstandard2.0/ref/System.Security.Cryptography.X509Certificates.dll", + "build/netstandard2.0/ref/System.Security.Principal.dll", + "build/netstandard2.0/ref/System.Security.SecureString.dll", + "build/netstandard2.0/ref/System.ServiceModel.Web.dll", + "build/netstandard2.0/ref/System.Text.Encoding.Extensions.dll", + "build/netstandard2.0/ref/System.Text.Encoding.dll", + "build/netstandard2.0/ref/System.Text.RegularExpressions.dll", + "build/netstandard2.0/ref/System.Threading.Overlapped.dll", + "build/netstandard2.0/ref/System.Threading.Tasks.Parallel.dll", + "build/netstandard2.0/ref/System.Threading.Tasks.dll", + "build/netstandard2.0/ref/System.Threading.Thread.dll", + "build/netstandard2.0/ref/System.Threading.ThreadPool.dll", + "build/netstandard2.0/ref/System.Threading.Timer.dll", + "build/netstandard2.0/ref/System.Threading.dll", + "build/netstandard2.0/ref/System.Transactions.dll", + "build/netstandard2.0/ref/System.ValueTuple.dll", + "build/netstandard2.0/ref/System.Web.dll", + "build/netstandard2.0/ref/System.Windows.dll", + "build/netstandard2.0/ref/System.Xml.Linq.dll", + "build/netstandard2.0/ref/System.Xml.ReaderWriter.dll", + "build/netstandard2.0/ref/System.Xml.Serialization.dll", + "build/netstandard2.0/ref/System.Xml.XDocument.dll", + "build/netstandard2.0/ref/System.Xml.XPath.XDocument.dll", + "build/netstandard2.0/ref/System.Xml.XPath.dll", + "build/netstandard2.0/ref/System.Xml.XmlDocument.dll", + "build/netstandard2.0/ref/System.Xml.XmlSerializer.dll", + "build/netstandard2.0/ref/System.Xml.dll", + "build/netstandard2.0/ref/System.dll", + "build/netstandard2.0/ref/mscorlib.dll", + "build/netstandard2.0/ref/netstandard.dll", + "build/netstandard2.0/ref/netstandard.xml", + "lib/netstandard1.0/_._", + "netstandard.library.2.0.3.nupkg.sha512", + "netstandard.library.nuspec" + ] + } + }, + "projectFileDependencyGroups": { + ".NETStandard,Version=v2.0": [ + "NETStandard.Library >= 2.0.3" + ] + }, + "packageFolders": { + "/home/anemunt/.nuget/packages/": {} + }, + "project": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "/home/anemunt/Git-base/Modularity/Scripts/Managed/ModuCPP.csproj", + "projectName": "ModuCPP", + "projectPath": "/home/anemunt/Git-base/Modularity/Scripts/Managed/ModuCPP.csproj", + "packagesPath": "/home/anemunt/.nuget/packages/", + "outputPath": "/home/anemunt/Git-base/Modularity/Scripts/Managed/obj/", + "projectStyle": "PackageReference", + "configFilePaths": [ + "/home/anemunt/.nuget/NuGet/NuGet.Config" + ], + "originalTargetFrameworks": [ + "netstandard2.0" + ], + "sources": { + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "netstandard2.0": { + "targetAlias": "netstandard2.0", + "projectReferences": {} + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + }, + "restoreAuditProperties": { + "enableAudit": "true", + "auditLevel": "low", + "auditMode": "direct" + }, + "SdkAnalysisLevel": "10.0.100" + }, + "frameworks": { + "netstandard2.0": { + "targetAlias": "netstandard2.0", + "dependencies": { + "NETStandard.Library": { + "suppressParent": "All", + "target": "Package", + "version": "[2.0.3, )", + "autoReferenced": true + } + }, + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "runtimeIdentifierGraphPath": "/usr/share/dotnet/sdk/10.0.100/RuntimeIdentifierGraph.json" + } + } + } +} \ No newline at end of file diff --git a/Scripts/Managed/obj/project.nuget.cache b/Scripts/Managed/obj/project.nuget.cache new file mode 100644 index 0000000..82ab118 --- /dev/null +++ b/Scripts/Managed/obj/project.nuget.cache @@ -0,0 +1,11 @@ +{ + "version": 2, + "dgSpecHash": "iTrhv2TT9CE=", + "success": true, + "projectFilePath": "/home/anemunt/Git-base/Modularity/Scripts/Managed/ModuCPP.csproj", + "expectedPackageFiles": [ + "/home/anemunt/.nuget/packages/microsoft.netcore.platforms/1.1.0/microsoft.netcore.platforms.1.1.0.nupkg.sha512", + "/home/anemunt/.nuget/packages/netstandard.library/2.0.3/netstandard.library.2.0.3.nupkg.sha512" + ], + "logs": [] +} \ No newline at end of file diff --git a/Scripts/TopDownMovement2D.cpp b/Scripts/TopDownMovement2D.cpp new file mode 100644 index 0000000..84ca642 --- /dev/null +++ b/Scripts/TopDownMovement2D.cpp @@ -0,0 +1,74 @@ +#include "ScriptRuntime.h" +#include "SceneObject.h" +#include "ThirdParty/imgui/imgui.h" + +namespace { +float walkSpeed = 4.0f; +float runSpeed = 7.0f; +float acceleration = 18.0f; +float drag = 8.0f; +bool useRigidbody2D = true; +bool warnedMissingRb = false; +} // namespace + +extern "C" void Script_OnInspector(ScriptContext& ctx) { + ctx.AutoSetting("walkSpeed", walkSpeed); + ctx.AutoSetting("runSpeed", runSpeed); + ctx.AutoSetting("acceleration", acceleration); + ctx.AutoSetting("drag", drag); + ctx.AutoSetting("useRigidbody2D", useRigidbody2D); + + ImGui::TextUnformatted("Top Down Movement 2D"); + ImGui::Separator(); + ImGui::DragFloat("Walk Speed", &walkSpeed, 0.1f, 0.0f, 50.0f, "%.2f"); + ImGui::DragFloat("Run Speed", &runSpeed, 0.1f, 0.0f, 80.0f, "%.2f"); + ImGui::DragFloat("Acceleration", &acceleration, 0.1f, 0.0f, 200.0f, "%.2f"); + ImGui::DragFloat("Drag", &drag, 0.1f, 0.0f, 200.0f, "%.2f"); + ImGui::Checkbox("Use Rigidbody2D", &useRigidbody2D); +} + +void TickUpdate(ScriptContext& ctx, float dt) { + if (!ctx.object || dt <= 0.0f) return; + + glm::vec2 input(0.0f); + if (ImGui::IsKeyDown(ImGuiKey_W)) input.y += 1.0f; + if (ImGui::IsKeyDown(ImGuiKey_S)) input.y -= 1.0f; + if (ImGui::IsKeyDown(ImGuiKey_D)) input.x += 1.0f; + if (ImGui::IsKeyDown(ImGuiKey_A)) input.x -= 1.0f; + if (glm::length(input) > 1e-3f) input = glm::normalize(input); + + float speed = ctx.IsSprintDown() ? runSpeed : walkSpeed; + glm::vec2 targetVel = input * speed; + + if (useRigidbody2D) { + if (!ctx.HasRigidbody2D()) { + if (!warnedMissingRb) { + ctx.AddConsoleMessage("TopDownMovement2D: add Rigidbody2D to use velocity-based motion.", ConsoleMessageType::Warning); + warnedMissingRb = true; + } + return; + } + glm::vec2 vel(0.0f); + ctx.GetRigidbody2DVelocity(vel); + if (acceleration <= 0.0f) { + vel = targetVel; + } else { + glm::vec2 dv = targetVel - vel; + float maxDelta = acceleration * dt; + float len = glm::length(dv); + if (len > maxDelta && len > 1e-4f) { + dv *= (maxDelta / len); + } + vel += dv; + } + if (glm::length(input) < 1e-3f && drag > 0.0f) { + float damp = std::max(0.0f, 1.0f - drag * dt); + vel *= damp; + } + ctx.SetRigidbody2DVelocity(vel); + } else { + glm::vec2 pos = ctx.object->ui.position; + pos += targetVel * dt; + ctx.SetPosition2D(pos); + } +} diff --git a/TheSunset.ttf b/TheSunset.ttf new file mode 100644 index 0000000..c903940 Binary files /dev/null and b/TheSunset.ttf differ diff --git a/Thesunsethd-Regular (1).ttf b/Thesunsethd-Regular (1).ttf new file mode 100644 index 0000000..02235ac Binary files /dev/null and b/Thesunsethd-Regular (1).ttf differ diff --git a/build.sh b/build.sh index 22bfed3..98a600f 100755 --- a/build.sh +++ b/build.sh @@ -23,18 +23,56 @@ trap finish EXIT echo -e "================================\n Modularity - Native Linux Builder\n================================" +clean_build=0 +for arg in "$@"; do + if [ "$arg" = "--clean" ]; then + clean_build=1 + fi +done + git submodule update --init --recursive -if [ -d "build" ]; then - echo -e "[i]: Oh! We found an existing build directory.\nRemoving existing folder..." +if [ -d "build" ] && [ $clean_build -eq 1 ]; then + echo -e "[i]: Cleaning existing build directory..." rm -rf build/ echo -e "[i]: Build Has been Removed\nContinuing build" fi mkdir -p build cd build -cmake .. +cmake .. -DMONO_ROOT=/usr cmake --build . -- -j"$(nproc)" + +mkdir -p Packages/ThirdParty +find . -type f \( -name "*.a" -o -name "*.so" -o -name "*.dylib" -o -name "*.lib" \) \ + -not -path "./Packages/*" -exec cp -f {} Packages/ThirdParty/ \; + +mkdir -p Packages/Engine +find . -type f \( -name "libcore*" -o -name "core*.lib" -o -name "core*.dll" \) \ + -not -path "./Packages/*" -exec cp -f {} Packages/Engine/ \; + +cd .. + +player_cache_dir="build/player-cache" +if [ $clean_build -eq 1 ] && [ -d "$player_cache_dir" ]; then + echo -e "[i]: Cleaning player cache build directory..." + rm -rf "$player_cache_dir" +fi + +mkdir -p "$player_cache_dir" +cmake -S . -B "$player_cache_dir" -DMONO_ROOT=/usr -DCMAKE_BUILD_TYPE=Release -DMODULARITY_BUILD_EDITOR=OFF +cmake --build "$player_cache_dir" --target ModularityPlayer -- -j"$(nproc)" + +mkdir -p "$player_cache_dir/Packages/ThirdParty" +find "$player_cache_dir" -type f \( -name "*.a" -o -name "*.so" -o -name "*.dylib" -o -name "*.lib" \) \ + -not -path "$player_cache_dir/Packages/*" -exec cp -f {} "$player_cache_dir/Packages/ThirdParty/" \; + +mkdir -p "$player_cache_dir/Packages/Engine" +find "$player_cache_dir" -type f \( -name "libcore*" -o -name "core*.lib" -o -name "core*.dll" \) \ + -not -path "$player_cache_dir/Packages/*" -exec cp -f {} "$player_cache_dir/Packages/Engine/" \; + +cd build + cp -r ../Resources . cp Resources/imgui.ini . -ln -sf build/compile_commands.json compile_commands.json \ No newline at end of file +ln -sf build/compile_commands.json compile_commands.json diff --git a/docs/Scripting.md b/docs/Scripting.md index f732ee6..0fda9d5 100644 --- a/docs/Scripting.md +++ b/docs/Scripting.md @@ -12,6 +12,7 @@ Scripts in Modularity are native C++ code compiled into shared libraries and loa ## Table of contents - [Quickstart](#quickstart) +- [C# managed scripting (experimental)](#c-managed-scripting-experimental) - [Scripts.modu](#scriptsmodu) - [How compilation works](#how-compilation-works) - [Lifecycle hooks](#lifecycle-hooks) @@ -36,6 +37,29 @@ Scripts in Modularity are native C++ code compiled into shared libraries and loa - In the Inspector’s script component menu, choose **Compile**. 5. Implement a tick hook (`TickUpdate`) and observe behavior in play mode. +## C# managed scripting (experimental) +Modularity can host managed C# scripts via the .NET runtime. This is an early, minimal integration +intended for movement/transform tests and simple Rigidbody control. + +1. Build the managed project (this now happens automatically when you compile a C# script): + - `dotnet build Scripts/Managed/ModuCPP.csproj` +2. In the Inspector, add a Script component and set: + - `Language` = **C#** + - `Assembly Path` = `Scripts/Managed/bin/Debug/net10.0/ModuCPP.dll` (or point at `Scripts/Managed/SampleInspector.cs`) + - `Type` = `ModuCPP.SampleInspector` +3. Enter play mode. The sample script will auto-rotate the object. + +Notes: +- The `ModuCPP.runtimeconfig.json` produced by `dotnet build` must sit next to the DLL. +- The managed host currently expects the script assembly to also contain `ModuCPP.Host` + (use the provided `Scripts/Managed/ModuCPP.csproj` as the entry assembly). +- The managed API surface is tiny for now: position/rotation/scale, basic Rigidbody velocity/forces, + settings, and console logging. +- Requires a local .NET runtime (Windows/Linux). If the runtime is missing, the engine will fail to + initialize managed scripts and report the error in the inspector. +- Managed hooks should be exported as `Script_Begin`, `Script_TickUpdate`, etc. via + `[UnmanagedCallersOnly]` in the C# script class. + ## Scripts.modu Each project has a `Scripts.modu` file (auto-created if missing). It controls compilation. diff --git a/docs/mono-embedding.md b/docs/mono-embedding.md new file mode 100644 index 0000000..c59e41e --- /dev/null +++ b/docs/mono-embedding.md @@ -0,0 +1,17 @@ +# Mono Embedding Setup + +This project uses Mono embedding for managed (C#) scripts. + +Expected layout (vendored): +`src/ThirdParty/mono/` +- `include/mono-2.0/` +- `lib/` (or `lib64/`) with `mono-2.0-sgen` library +- `etc/mono/` (config files) +- `lib/mono/4.5/` (framework assemblies) + +You can override the runtime location at runtime with: +`MODU_MONO_ROOT=/path/to/mono` + +Build notes: +- The CMake cache variable `MONO_ROOT` controls where headers/libs are found. +- Managed scripts target `netstandard2.0` and are built with `dotnet build`. diff --git a/include/Shaders/Shader.h b/include/Shaders/Shader.h index 2c72fc2..92b941b 100644 --- a/include/Shaders/Shader.h +++ b/include/Shaders/Shader.h @@ -19,6 +19,7 @@ public: void setVec2(const std::string &name, const glm::vec2 &value) const; void setVec3(const std::string &name, const glm::vec3 &value) const; void setMat4(const std::string &name, const glm::mat4 &mat) const; + void setMat4Array(const std::string &name, const glm::mat4 *data, int count) const; private: std::string readShaderFile(const char* filePath); diff --git a/src/AudioSystem.cpp b/src/AudioSystem.cpp index 871cc64..33c5e63 100644 --- a/src/AudioSystem.cpp +++ b/src/AudioSystem.cpp @@ -2,10 +2,143 @@ #include "../include/ThirdParty/miniaudio.h" #include "AudioSystem.h" #include +#include +#include namespace { constexpr size_t kPreviewBuckets = 800; constexpr ma_uint32 kPreviewChunkFrames = 2048; +constexpr float kReverbSmoothing = 0.12f; +constexpr size_t kReverbCombCount = 4; +constexpr size_t kReverbAllpassCount = 2; +constexpr float kReverbPreDelayMaxSeconds = 0.2f; +constexpr float kReverbReflectionsMaxSeconds = 0.1f; + +float DbToLinear(float db) { + return std::pow(10.0f, db / 20.0f); +} + +struct ReverbNodeVTable { + ma_node_vtable vtable; +}; + +static void reverb_node_process(ma_node* pNode, const float** ppFramesIn, ma_uint32* pFrameCountIn, + float** ppFramesOut, ma_uint32* pFrameCountOut) { + if (!pNode || !ppFramesIn || !ppFramesOut) return; + auto* node = reinterpret_cast(pNode); + (void)pFrameCountIn; + const float* input = ppFramesIn[0]; + float* output = ppFramesOut[0]; + if (!input || !output) return; + + ma_uint32 frameCount = *pFrameCountOut; + int channels = node->channels; + float decayTime = std::max(0.1f, node->decayTime); + float diffusion = std::clamp(node->diffusion, 0.0f, 100.0f); + float density = std::clamp(node->density, 0.0f, 100.0f); + float preDelaySeconds = std::clamp(node->preDelaySeconds, 0.0f, kReverbPreDelayMaxSeconds); + float reflectionsDelaySeconds = std::clamp(node->reflectionsDelaySeconds, 0.0f, kReverbReflectionsMaxSeconds); + size_t preDelayFrames = static_cast(preDelaySeconds * static_cast(node->sampleRate)); + size_t reflectionsDelayFrames = static_cast(reflectionsDelaySeconds * static_cast(node->sampleRate)); + float wetGain = std::clamp(node->wetGain, 0.0f, 2.0f); + float reflectionsGain = std::clamp(node->reflectionsGain, 0.0f, 2.0f); + + float diffusionNorm = diffusion / 100.0f; + float densityNorm = density / 100.0f; + float allpassGain = 0.2f + 0.55f * diffusionNorm; + float densityScale = 0.6f + 0.4f * densityNorm; + float combGain = 1.0f / static_cast(node->combBuffers.size()); + std::array combFeedback{}; + for (size_t i = 0; i < node->combBuffers.size(); ++i) { + float delaySec = static_cast(node->combBuffers[i].size() / channels) / static_cast(node->sampleRate); + combFeedback[i] = std::pow(10.0f, (-3.0f * delaySec) / decayTime) * densityScale; + } + + float cutoffHz = std::clamp(node->hfReference * node->decayHFRatio, 500.0f, 20000.0f); + float lpAlpha = std::exp(-2.0f * PI * cutoffHz / static_cast(node->sampleRate)); + + for (ma_uint32 frame = 0; frame < frameCount; ++frame) { + size_t preReadIndex = node->preDelayMaxFrames > 0 + ? (node->preDelayIndex + node->preDelayMaxFrames - preDelayFrames) % node->preDelayMaxFrames + : 0; + size_t reflectionsReadIndex = node->reflectionsMaxFrames > 0 + ? (node->reflectionsIndex + node->reflectionsMaxFrames - reflectionsDelayFrames) % node->reflectionsMaxFrames + : 0; + + for (int ch = 0; ch < channels; ++ch) { + float inSample = input[frame * channels + ch]; + float preSample = inSample; + if (!node->preDelayBuffer.empty()) { + size_t writeBase = node->preDelayIndex * channels; + size_t readBase = preReadIndex * channels; + preSample = node->preDelayBuffer[readBase + ch]; + node->preDelayBuffer[writeBase + ch] = inSample; + } + + float reflectionsSample = 0.0f; + if (!node->reflectionsBuffer.empty()) { + size_t writeBase = node->reflectionsIndex * channels; + size_t readBase = reflectionsReadIndex * channels; + reflectionsSample = node->reflectionsBuffer[readBase + ch]; + node->reflectionsBuffer[writeBase + ch] = preSample; + } + + float combSum = 0.0f; + for (size_t i = 0; i < node->combBuffers.size(); ++i) { + auto& buffer = node->combBuffers[i]; + size_t idx = node->combIndex[i]; + size_t base = idx * channels + ch; + float y = buffer[base]; + buffer[base] = preSample + y * combFeedback[i]; + combSum += y; + } + + combSum *= combGain; + float apOut = combSum; + for (size_t i = 0; i < node->allpassBuffers.size(); ++i) { + auto& buffer = node->allpassBuffers[i]; + size_t idx = node->allpassIndex[i]; + size_t base = idx * channels + ch; + float buf = buffer[base]; + float y = -allpassGain * apOut + buf; + buffer[base] = apOut + buf * allpassGain; + apOut = y; + } + + float wetSample = apOut * wetGain + reflectionsSample * reflectionsGain; + float lp = node->lpState.empty() ? wetSample : (lpAlpha * node->lpState[ch] + (1.0f - lpAlpha) * wetSample); + if (!node->lpState.empty()) node->lpState[ch] = lp; + output[frame * channels + ch] = lp; + } + + if (!node->preDelayBuffer.empty()) { + node->preDelayIndex = (node->preDelayIndex + 1) % node->preDelayMaxFrames; + } + if (!node->reflectionsBuffer.empty()) { + node->reflectionsIndex = (node->reflectionsIndex + 1) % node->reflectionsMaxFrames; + } + for (size_t i = 0; i < node->combIndex.size(); ++i) { + node->combIndex[i] = (node->combIndex[i] + 1) % (node->combBuffers[i].size() / channels); + } + for (size_t i = 0; i < node->allpassIndex.size(); ++i) { + node->allpassIndex[i] = (node->allpassIndex[i] + 1) % (node->allpassBuffers[i].size() / channels); + } + } +} + +static ma_result reverb_node_get_required_input_frames(ma_node* pNode, ma_uint32 outputFrameCount, ma_uint32* pInputFrameCount) { + (void)pNode; + if (pInputFrameCount) *pInputFrameCount = outputFrameCount; + return MA_SUCCESS; +} + +static ma_node_vtable g_reverb_node_vtable = { + reverb_node_process, + reverb_node_get_required_input_frames, + 1, + 1, + 0 +}; } bool AudioSystem::init() { @@ -15,6 +148,65 @@ bool AudioSystem::init() { std::cerr << "AudioSystem: failed to init miniaudio (" << res << ")\n"; return false; } + ma_uint32 channels = ma_engine_get_channels(&engine); + ma_uint32 sampleRate = ma_engine_get_sample_rate(&engine); + ma_splitter_node_config splitterConfig = ma_splitter_node_config_init(channels); + res = ma_splitter_node_init(ma_engine_get_node_graph(&engine), &splitterConfig, nullptr, &reverbSplitter); + if (res == MA_SUCCESS) { + ma_node_config nodeConfig = ma_node_config_init(); + nodeConfig.vtable = &g_reverb_node_vtable; + nodeConfig.pInputChannels = reinterpret_cast(&channels); + nodeConfig.pOutputChannels = reinterpret_cast(&channels); + res = ma_node_init(ma_engine_get_node_graph(&engine), &nodeConfig, nullptr, reinterpret_cast(&reverbNode)); + if (res == MA_SUCCESS) { + reverbNode.channels = static_cast(channels); + reverbNode.sampleRate = static_cast(sampleRate); + reverbNode.preDelayMaxFrames = static_cast(kReverbPreDelayMaxSeconds * sampleRate); + reverbNode.reflectionsMaxFrames = static_cast(kReverbReflectionsMaxSeconds * sampleRate); + reverbNode.preDelayBuffer.assign(reverbNode.preDelayMaxFrames * channels, 0.0f); + reverbNode.reflectionsBuffer.assign(reverbNode.reflectionsMaxFrames * channels, 0.0f); + reverbNode.lpState.assign(channels, 0.0f); + + const float combDelayMs[kReverbCombCount] = { 29.7f, 37.1f, 41.1f, 43.7f }; + reverbNode.combBuffers.resize(kReverbCombCount); + reverbNode.combIndex.assign(kReverbCombCount, 0); + for (size_t i = 0; i < kReverbCombCount; ++i) { + size_t frames = static_cast((combDelayMs[i] / 1000.0f) * sampleRate); + frames = std::max(1, frames); + reverbNode.combBuffers[i].assign(frames * channels, 0.0f); + } + + const float allpassDelayMs[kReverbAllpassCount] = { 5.0f, 1.7f }; + reverbNode.allpassBuffers.resize(kReverbAllpassCount); + reverbNode.allpassIndex.assign(kReverbAllpassCount, 0); + for (size_t i = 0; i < kReverbAllpassCount; ++i) { + size_t frames = static_cast((allpassDelayMs[i] / 1000.0f) * sampleRate); + frames = std::max(1, frames); + reverbNode.allpassBuffers[i].assign(frames * channels, 0.0f); + } + + ma_node_attach_output_bus(reinterpret_cast(&reverbSplitter), 0, ma_engine_get_endpoint(&engine), 0); + ma_node_attach_output_bus(reinterpret_cast(&reverbSplitter), 1, reinterpret_cast(&reverbNode), 0); + ma_node_attach_output_bus(reinterpret_cast(&reverbNode), 0, ma_engine_get_endpoint(&engine), 0); + ma_sound_group_config groupConfig = ma_sound_group_config_init_2(&engine); + groupConfig.pInitialAttachment = reinterpret_cast(&reverbSplitter); + groupConfig.initialAttachmentInputBusIndex = 0; + res = ma_sound_group_init_ex(&engine, &groupConfig, &reverbGroup); + if (res == MA_SUCCESS) { + reverbReady = true; + ma_sound_group_set_spatialization_enabled(&reverbGroup, MA_FALSE); + ma_sound_group_set_attenuation_model(&reverbGroup, ma_attenuation_model_none); + ma_sound_group_start(&reverbGroup); + ma_node_set_output_bus_volume(reinterpret_cast(&reverbSplitter), 0, 1.0f); + } else { + ma_node_uninit(reinterpret_cast(&reverbNode), nullptr); + ma_splitter_node_uninit(&reverbSplitter, nullptr); + } + } else { + ma_splitter_node_uninit(&reverbSplitter, nullptr); + } + } + initialized = true; return true; } @@ -22,6 +214,7 @@ bool AudioSystem::init() { void AudioSystem::shutdown() { stopPreview(); destroyActiveSounds(); + shutdownReverbGraph(); if (initialized) { ma_engine_uninit(&engine); initialized = false; @@ -81,7 +274,7 @@ bool AudioSystem::ensureSoundFor(const SceneObject& obj) { &engine, obj.audioSource.clipPath.c_str(), MA_SOUND_FLAG_STREAM, - nullptr, + reverbReady ? &reverbGroup : nullptr, nullptr, &snd->sound ); @@ -104,6 +297,26 @@ void AudioSystem::refreshSoundParams(const SceneObject& obj, ActiveSound& snd) { ma_sound_set_looping(&snd.sound, obj.audioSource.loop ? MA_TRUE : MA_FALSE); ma_sound_set_volume(&snd.sound, obj.audioSource.volume); ma_sound_set_spatialization_enabled(&snd.sound, obj.audioSource.spatial ? MA_TRUE : MA_FALSE); + if (obj.audioSource.spatial) { + switch (obj.audioSource.rolloffMode) { + case AudioRolloffMode::Linear: + ma_sound_set_attenuation_model(&snd.sound, ma_attenuation_model_linear); + break; + case AudioRolloffMode::Exponential: + ma_sound_set_attenuation_model(&snd.sound, ma_attenuation_model_exponential); + break; + case AudioRolloffMode::Custom: + ma_sound_set_attenuation_model(&snd.sound, ma_attenuation_model_none); + break; + case AudioRolloffMode::Logarithmic: + default: + ma_sound_set_attenuation_model(&snd.sound, ma_attenuation_model_inverse); + break; + } + ma_sound_set_rolloff(&snd.sound, std::max(0.01f, obj.audioSource.rolloff)); + } else { + ma_sound_set_attenuation_model(&snd.sound, ma_attenuation_model_none); + } ma_sound_set_min_distance(&snd.sound, minDist); ma_sound_set_max_distance(&snd.sound, maxDist); ma_sound_set_position(&snd.sound, obj.position.x, obj.position.y, obj.position.z); @@ -120,6 +333,7 @@ void AudioSystem::update(const std::vector& objects, const Camera& ma_engine_listener_set_position(&engine, 0, listenerCamera.position.x, listenerCamera.position.y, listenerCamera.position.z); ma_engine_listener_set_direction(&engine, 0, listenerCamera.front.x, listenerCamera.front.y, listenerCamera.front.z); ma_engine_listener_set_world_up(&engine, 0, listenerCamera.up.x, listenerCamera.up.y, listenerCamera.up.z); + updateReverb(objects, listenerCamera.position); if (!playing) { destroyActiveSounds(); @@ -144,6 +358,10 @@ void AudioSystem::update(const std::vector& objects, const Camera& if (ensureSoundFor(obj)) { refreshSoundParams(obj, *activeSounds[obj.id]); + if (obj.audioSource.spatial && obj.audioSource.rolloffMode == AudioRolloffMode::Custom) { + float attenuation = computeCustomAttenuation(obj, listenerCamera.position); + ma_sound_set_volume(&activeSounds[obj.id]->sound, obj.audioSource.volume * attenuation); + } } } @@ -253,6 +471,152 @@ bool AudioSystem::setObjectVolume(const SceneObject& obj, float volume) { return true; } +float AudioSystem::computeCustomAttenuation(const SceneObject& obj, const glm::vec3& listenerPos) const { + float minDist = std::max(0.1f, obj.audioSource.minDistance); + float maxDist = std::max(obj.audioSource.maxDistance, minDist + 0.5f); + float dist = glm::length(listenerPos - obj.position); + if (dist <= minDist) return 1.0f; + if (dist >= maxDist) return std::clamp(obj.audioSource.customEndGain, 0.0f, 1.0f); + + float range = maxDist - minDist; + float midRatio = std::clamp(obj.audioSource.customMidDistance, 0.0f, 1.0f); + float midDist = minDist + range * midRatio; + float midGain = std::clamp(obj.audioSource.customMidGain, 0.0f, 1.0f); + float endGain = std::clamp(obj.audioSource.customEndGain, 0.0f, 1.0f); + + if (dist <= midDist) { + float t = (dist - minDist) / std::max(0.001f, midDist - minDist); + return std::clamp(1.0f + (midGain - 1.0f) * t, 0.0f, 1.0f); + } + + float t = (dist - midDist) / std::max(0.001f, maxDist - midDist); + return std::clamp(midGain + (endGain - midGain) * t, 0.0f, 1.0f); +} + +void AudioSystem::updateReverb(const std::vector& objects, const glm::vec3& listenerPos) { + if (!reverbReady) return; + float blend = 0.0f; + ReverbSettings target = getReverbTarget(objects, listenerPos, blend); + applyReverbSettings(target, blend); +} + +AudioSystem::ReverbSettings AudioSystem::getReverbTarget(const std::vector& objects, const glm::vec3& listenerPos, float& outBlend) const { + ReverbSettings target{}; + float bestBlend = 0.0f; + + for (const auto& obj : objects) { + if (!obj.enabled || !obj.hasReverbZone || !obj.reverbZone.enabled) continue; + const auto& zone = obj.reverbZone; + float blend = 0.0f; + + if (zone.shape == ReverbZoneShape::Sphere) { + float minDist = std::max(0.0f, zone.minDistance); + float maxDist = std::max(zone.maxDistance, minDist + 0.01f); + float radius = std::max(0.01f, zone.radius); + float dist = glm::length(listenerPos - obj.position); + if (dist > radius) continue; + maxDist = std::min(maxDist, radius); + if (dist >= maxDist) continue; + if (dist <= minDist) { + blend = 1.0f; + } else { + blend = std::clamp((maxDist - dist) / (maxDist - minDist), 0.0f, 1.0f); + } + } else { + glm::vec3 halfSize = glm::max(zone.boxSize * 0.5f, glm::vec3(0.01f)); + glm::vec3 delta = glm::abs(listenerPos - obj.position); + if (delta.x > halfSize.x || delta.y > halfSize.y || delta.z > halfSize.z) continue; + float edgeDistance = std::min({halfSize.x - delta.x, halfSize.y - delta.y, halfSize.z - delta.z}); + if (zone.blendDistance <= 0.001f) { + blend = 1.0f; + } else { + blend = std::clamp(edgeDistance / zone.blendDistance, 0.0f, 1.0f); + } + } + + if (blend > bestBlend) { + bestBlend = blend; + target.room = zone.room; + target.roomHF = zone.roomHF; + target.roomLF = zone.roomLF; + target.decayTime = zone.decayTime; + target.decayHFRatio = zone.decayHFRatio; + target.reflections = zone.reflections; + target.reflectionsDelay = zone.reflectionsDelay; + target.reverb = zone.reverb; + target.reverbDelay = zone.reverbDelay; + target.hfReference = zone.hfReference; + target.lfReference = zone.lfReference; + target.roomRolloffFactor = zone.roomRolloffFactor; + target.diffusion = zone.diffusion; + target.density = zone.density; + } + } + + outBlend = bestBlend; + return target; +} + +void AudioSystem::applyReverbSettings(const ReverbSettings& target, float blend) { + ReverbSettings mixed{}; + mixed.room = target.room; + mixed.roomHF = target.roomHF; + mixed.roomLF = target.roomLF; + mixed.decayTime = std::max(0.1f, target.decayTime); + mixed.decayHFRatio = std::clamp(target.decayHFRatio, 0.1f, 2.0f); + mixed.reflections = target.reflections; + mixed.reflectionsDelay = std::clamp(target.reflectionsDelay, 0.0f, kReverbReflectionsMaxSeconds); + mixed.reverb = target.reverb; + mixed.reverbDelay = std::clamp(target.reverbDelay, 0.0f, kReverbPreDelayMaxSeconds); + mixed.hfReference = std::clamp(target.hfReference, 1000.0f, 20000.0f); + mixed.lfReference = std::clamp(target.lfReference, 20.0f, 1000.0f); + mixed.roomRolloffFactor = std::max(0.0f, target.roomRolloffFactor); + mixed.diffusion = std::clamp(target.diffusion, 0.0f, 100.0f); + mixed.density = std::clamp(target.density, 0.0f, 100.0f); + + currentReverb.room = currentReverb.room + (mixed.room - currentReverb.room) * kReverbSmoothing; + currentReverb.roomHF = currentReverb.roomHF + (mixed.roomHF - currentReverb.roomHF) * kReverbSmoothing; + currentReverb.roomLF = currentReverb.roomLF + (mixed.roomLF - currentReverb.roomLF) * kReverbSmoothing; + currentReverb.decayTime = currentReverb.decayTime + (mixed.decayTime - currentReverb.decayTime) * kReverbSmoothing; + currentReverb.decayHFRatio = currentReverb.decayHFRatio + (mixed.decayHFRatio - currentReverb.decayHFRatio) * kReverbSmoothing; + currentReverb.reflections = currentReverb.reflections + (mixed.reflections - currentReverb.reflections) * kReverbSmoothing; + currentReverb.reflectionsDelay = currentReverb.reflectionsDelay + (mixed.reflectionsDelay - currentReverb.reflectionsDelay) * kReverbSmoothing; + currentReverb.reverb = currentReverb.reverb + (mixed.reverb - currentReverb.reverb) * kReverbSmoothing; + currentReverb.reverbDelay = currentReverb.reverbDelay + (mixed.reverbDelay - currentReverb.reverbDelay) * kReverbSmoothing; + currentReverb.hfReference = currentReverb.hfReference + (mixed.hfReference - currentReverb.hfReference) * kReverbSmoothing; + currentReverb.lfReference = currentReverb.lfReference + (mixed.lfReference - currentReverb.lfReference) * kReverbSmoothing; + currentReverb.roomRolloffFactor = currentReverb.roomRolloffFactor + (mixed.roomRolloffFactor - currentReverb.roomRolloffFactor) * kReverbSmoothing; + currentReverb.diffusion = currentReverb.diffusion + (mixed.diffusion - currentReverb.diffusion) * kReverbSmoothing; + currentReverb.density = currentReverb.density + (mixed.density - currentReverb.density) * kReverbSmoothing; + + constexpr float kDbSoftening = 0.5f; + constexpr float kWetScale = 0.25f; + float reflectionsGain = DbToLinear((currentReverb.reflections + currentReverb.room) * kDbSoftening) * (blend * kWetScale); + float reverbGain = DbToLinear((currentReverb.reverb + currentReverb.room) * kDbSoftening) * (blend * kWetScale); + float dry = std::clamp(1.0f - blend * (currentReverb.roomRolloffFactor * 0.05f), 0.2f, 1.0f); + + ma_node_set_output_bus_volume(reinterpret_cast(&reverbSplitter), 0, dry); + reverbNode.wetGain = std::clamp(reverbGain, 0.0f, 1.0f); + reverbNode.reflectionsGain = std::clamp(reflectionsGain, 0.0f, 1.0f); + reverbNode.decayTime = currentReverb.decayTime; + reverbNode.decayHFRatio = currentReverb.decayHFRatio; + reverbNode.diffusion = currentReverb.diffusion; + reverbNode.density = currentReverb.density; + reverbNode.hfReference = currentReverb.hfReference; + reverbNode.preDelaySeconds = currentReverb.reverbDelay; + reverbNode.reflectionsDelaySeconds = currentReverb.reflectionsDelay; +} + +void AudioSystem::shutdownReverbGraph() { + if (reverbReady) { + ma_sound_group_uninit(&reverbGroup); + ma_node_uninit(reinterpret_cast(&reverbNode), nullptr); + ma_splitter_node_uninit(&reverbSplitter, nullptr); + reverbReady = false; + } + currentReverb = ReverbSettings{}; +} + AudioClipPreview AudioSystem::loadPreview(const std::string& path) { AudioClipPreview preview; preview.path = path; diff --git a/src/AudioSystem.h b/src/AudioSystem.h index deccdec..b2aab7c 100644 --- a/src/AudioSystem.h +++ b/src/AudioSystem.h @@ -42,7 +42,51 @@ public: bool setObjectLoop(const SceneObject& obj, bool loop); bool setObjectVolume(const SceneObject& obj, float volume); + struct SimpleReverbNode { + ma_node_base baseNode; + int channels = 0; + int sampleRate = 0; + std::vector> combBuffers; + std::vector combIndex; + std::vector> allpassBuffers; + std::vector allpassIndex; + std::vector preDelayBuffer; + size_t preDelayIndex = 0; + std::vector reflectionsBuffer; + size_t reflectionsIndex = 0; + std::vector lpState; + float wetGain = 0.0f; + float reflectionsGain = 0.0f; + float decayTime = 1.5f; + float decayHFRatio = 0.5f; + float diffusion = 100.0f; + float density = 100.0f; + float hfReference = 5000.0f; + float preDelaySeconds = 0.01f; + float reflectionsDelaySeconds = 0.01f; + size_t preDelayMaxFrames = 0; + size_t reflectionsMaxFrames = 0; + }; + private: + struct ReverbSettings { + float room = -10000.0f; + float roomHF = -10000.0f; + float roomLF = -10000.0f; + float decayTime = 1.5f; + float decayHFRatio = 0.5f; + float reflections = -10000.0f; + float reflectionsDelay = 0.01f; + float reverb = -10000.0f; + float reverbDelay = 0.01f; + float hfReference = 5000.0f; + float lfReference = 250.0f; + float roomRolloffFactor = 0.0f; + float diffusion = 100.0f; + float density = 100.0f; + float dry = 1.0f; + }; + struct ActiveSound { ma_sound sound; std::string clipPath; @@ -56,6 +100,12 @@ private: std::unordered_map previewCache; std::unordered_set missingClips; + SimpleReverbNode reverbNode{}; + ma_splitter_node reverbSplitter{}; + ma_sound_group reverbGroup{}; + bool reverbReady = false; + ReverbSettings currentReverb{}; + ma_sound previewSound{}; bool previewActive = false; std::string previewPath; @@ -63,5 +113,10 @@ private: void destroyActiveSounds(); bool ensureSoundFor(const SceneObject& obj); void refreshSoundParams(const SceneObject& obj, ActiveSound& snd); + float computeCustomAttenuation(const SceneObject& obj, const glm::vec3& listenerPos) const; AudioClipPreview loadPreview(const std::string& path); + void updateReverb(const std::vector& objects, const glm::vec3& listenerPos); + ReverbSettings getReverbTarget(const std::vector& objects, const glm::vec3& listenerPos, float& outBlend) const; + void applyReverbSettings(const ReverbSettings& target, float blend); + void shutdownReverbGraph(); }; diff --git a/src/EditorUI.cpp b/src/EditorUI.cpp index 3d0515d..ca7e872 100644 --- a/src/EditorUI.cpp +++ b/src/EditorUI.cpp @@ -95,9 +95,10 @@ FileCategory FileBrowser::getFileCategory(const fs::directory_entry& entry) cons if (ext == ".modu" || ext == ".scene") return FileCategory::Scene; // Model files - if (ext == ".fbx" || ext == ".obj" || ext == ".gltf" || ext == ".glb" || - ext == ".dae" || ext == ".blend" || ext == ".3ds" || ext == ".ply" || - ext == ".stl" || ext == ".x" || ext == ".md5mesh" || ext == ".rmesh") { + if (ext == ".fbx" || ext == ".obj" || ext == ".gltf" || ext == ".glb" || + ext == ".dae" || ext == ".blend" || ext == ".3ds" || ext == ".b3d" || + ext == ".ply" || ext == ".stl" || ext == ".x" || ext == ".md5mesh" || + ext == ".rmesh") { return FileCategory::Model; } @@ -189,6 +190,60 @@ bool FileBrowser::matchesFilter(const fs::directory_entry& entry) const { void applyModernTheme() { ImGuiStyle& style = ImGui::GetStyle(); ImVec4* colors = style.Colors; + ImGuiIO& io = ImGui::GetIO(); + const float fontSizeBase = 18.0f; + const float fontSizeOffset = -2.5f; + const float fontSize = std::max(1.0f, fontSizeBase + fontSizeOffset); + ImFont* editorFont = nullptr; + fs::path primaryFontPath; + const fs::path fontCandidates[] = { + fs::path("Resources") / "Fonts" / "TheSunset.ttf", + fs::path("Resources") / "Fonts" / "Thesunsethd-Regular (1).ttf", + fs::path("TheSunset.ttf"), + fs::path("Thesunsethd-Regular (1).ttf") + }; + for (const auto& fontPath : fontCandidates) { + if (!fs::exists(fontPath)) { + continue; + } + const std::string fontPathStr = fontPath.string(); + editorFont = io.Fonts->AddFontFromFileTTF(fontPathStr.c_str(), fontSize); + if (editorFont) { + primaryFontPath = fontPath; + io.FontDefault = editorFont; + break; + } + } + if (!editorFont) { + std::cerr << "[WARN] Failed to load editor font (TheSunset) from Resources/Fonts." + << std::endl; + } else { + const fs::path fallbackCandidates[] = { + fs::path("Resources") / "Fonts" / "TheSunset.ttf", + fs::path("TheSunset.ttf") + }; + if (primaryFontPath.filename() != "TheSunset.ttf") { + for (const auto& fallbackPath : fallbackCandidates) { + if (!fs::exists(fallbackPath)) { + continue; + } + const std::string fallbackPathStr = fallbackPath.string(); + ImFontConfig mergeConfig; + mergeConfig.MergeMode = true; + ImFont* fallbackFont = io.Fonts->AddFontFromFileTTF( + fallbackPathStr.c_str(), + fontSize, + &mergeConfig, + io.Fonts->GetGlyphRangesDefault() + ); + if (!fallbackFont) { + std::cerr << "[WARN] Failed to merge fallback font: " + << fallbackPathStr << std::endl; + } + break; + } + } + } ImVec4 slate = ImVec4(0.10f, 0.11f, 0.16f, 1.00f); ImVec4 panel = ImVec4(0.14f, 0.15f, 0.21f, 1.00f); @@ -253,26 +308,113 @@ void applyModernTheme() { colors[ImGuiCol_NavHighlight] = accent; colors[ImGuiCol_TableHeaderBg] = ImVec4(0.18f, 0.20f, 0.28f, 1.00f); colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.05f, 0.06f, 0.08f, 0.70f); + applyEditorLayoutPreset(style); +} - style.WindowRounding = 10.0f; +void applyEditorLayoutPreset(ImGuiStyle& style) { + style.WindowPadding = ImVec2(3.0f, 3.0f); + style.FramePadding = ImVec2(4.0f, 4.0f); + style.ItemSpacing = ImVec2(10.0f, 5.0f); + style.ItemInnerSpacing = ImVec2(2.0f, 2.0f); + style.CellPadding = ImVec2(4.0f, 2.0f); + style.TouchExtraPadding = ImVec2(0.0f, 0.0f); + style.IndentSpacing = 11.0f; + style.GrabMinSize = 8.0f; + + style.WindowBorderSize = 0.0f; + style.ChildBorderSize = 1.0f; + style.PopupBorderSize = 1.0f; + style.FrameBorderSize = 0.0f; + + style.WindowRounding = 12.0f; style.ChildRounding = 12.0f; - style.FrameRounding = 10.0f; + style.FrameRounding = 12.0f; style.PopupRounding = 12.0f; + style.GrabRounding = 12.0f; + + style.ScrollbarSize = 11.0f; style.ScrollbarRounding = 10.0f; - style.GrabRounding = 8.0f; + style.ScrollbarPadding = 1.0f; + + style.TabBorderSize = 1.0f; + style.TabBarBorderSize = 1.0f; + style.TabBarOverlineSize = 1.0f; + style.TabMinWidthBase = 1.0f; + style.TabMinWidthShrink = 80.0f; + style.TabCloseButtonMinWidthSelected = -1.0f; + style.TabCloseButtonMinWidthUnselected = 0.0f; style.TabRounding = 10.0f; - style.WindowPadding = ImVec2(12.0f, 12.0f); - style.FramePadding = ImVec2(10.0f, 6.0f); - style.ItemSpacing = ImVec2(10.0f, 8.0f); - style.ItemInnerSpacing = ImVec2(8.0f, 6.0f); - style.IndentSpacing = 18.0f; + style.TableAngledHeadersAngle = 35.0f; + style.TableAngledHeadersTextAlign = ImVec2(0.50f, 0.00f); + + style.TreeLinesFlags = ImGuiTreeNodeFlags_DrawLinesNone; + style.TreeLinesSize = 1.0f; + style.TreeLinesRounding = 0.0f; + + style.WindowTitleAlign = ImVec2(0.50f, 0.50f); + style.WindowBorderHoverPadding = 6.0f; + style.WindowMenuButtonPosition = ImGuiDir_None; + + style.ColorButtonPosition = ImGuiDir_Right; + style.ButtonTextAlign = ImVec2(0.50f, 0.50f); + style.SelectableTextAlign = ImVec2(0.00f, 0.00f); + style.SeparatorTextBorderSize = 2.0f; + style.SeparatorTextAlign = ImVec2(0.50f, 0.50f); + style.SeparatorTextPadding = ImVec2(4.0f, 0.0f); + style.LogSliderDeadzone = 4.0f; + style.ImageBorderSize = 0.0f; + + style.DockingNodeHasCloseButton = true; + style.DockingSeparatorSize = 0.0f; + + style.DisplayWindowPadding = ImVec2(19.0f, 19.0f); + style.DisplaySafeAreaPadding = ImVec2(0.0f, 0.0f); +} + +void applyPixelStyle(ImGuiStyle& style) { + applyEditorLayoutPreset(style); + style.WindowRounding = 0.0f; + style.ChildRounding = 0.0f; + style.FrameRounding = 0.0f; + style.PopupRounding = 0.0f; + style.ScrollbarRounding = 0.0f; + style.GrabRounding = 0.0f; + style.TabRounding = 0.0f; + + style.WindowPadding = ImVec2(8.0f, 6.0f); + style.FramePadding = ImVec2(6.0f, 4.0f); + style.ItemSpacing = ImVec2(6.0f, 4.0f); + style.ItemInnerSpacing = ImVec2(6.0f, 4.0f); + style.IndentSpacing = 14.0f; style.WindowBorderSize = 1.0f; style.FrameBorderSize = 1.0f; style.PopupBorderSize = 1.0f; style.TabBorderSize = 1.0f; } + +void applySuperRoundStyle(ImGuiStyle& style) { + applyEditorLayoutPreset(style); + style.WindowRounding = 18.0f; + style.ChildRounding = 16.0f; + style.FrameRounding = 16.0f; + style.PopupRounding = 16.0f; + style.ScrollbarRounding = 16.0f; + style.GrabRounding = 14.0f; + style.TabRounding = 16.0f; + + style.WindowPadding = ImVec2(14.0f, 10.0f); + style.FramePadding = ImVec2(12.0f, 8.0f); + style.ItemSpacing = ImVec2(10.0f, 8.0f); + style.ItemInnerSpacing = ImVec2(8.0f, 6.0f); + style.IndentSpacing = 18.0f; + + style.WindowBorderSize = 0.0f; + style.FrameBorderSize = 0.0f; + style.PopupBorderSize = 0.0f; + style.TabBorderSize = 0.0f; +} #pragma endregion #pragma region Dockspace diff --git a/src/EditorUI.h b/src/EditorUI.h index 200f624..600b8b5 100644 --- a/src/EditorUI.h +++ b/src/EditorUI.h @@ -69,6 +69,9 @@ public: // Apply the modern dark theme to ImGui void applyModernTheme(); +void applyEditorLayoutPreset(ImGuiStyle& style); +void applyPixelStyle(ImGuiStyle& style); +void applySuperRoundStyle(ImGuiStyle& style); // Setup ImGui dockspace for the editor void setupDockspace(const std::function& menuBarContent = nullptr); diff --git a/src/EditorWindows/AnimationWindow.cpp b/src/EditorWindows/AnimationWindow.cpp new file mode 100644 index 0000000..080d619 --- /dev/null +++ b/src/EditorWindows/AnimationWindow.cpp @@ -0,0 +1,554 @@ +#include "Engine.h" +#include "ThirdParty/imgui/imgui.h" +#include +#include +#include + +void Engine::renderAnimationWindow() { + if (!showAnimationWindow) return; + + auto clampFloat = [](float value, float minValue, float maxValue) { + return std::max(minValue, std::min(value, maxValue)); + }; + + auto lerpVec3 = [](const glm::vec3& a, const glm::vec3& b, float t) { + return a + (b - a) * t; + }; + + auto applyInterpolation = [](float t, AnimationInterpolation interpolation) { + t = std::max(0.0f, std::min(1.0f, t)); + switch (interpolation) { + case AnimationInterpolation::SmoothStep: + return t * t * (3.0f - 2.0f * t); + case AnimationInterpolation::EaseIn: + return t * t; + case AnimationInterpolation::EaseOut: { + float inv = 1.0f - t; + return 1.0f - inv * inv; + } + case AnimationInterpolation::EaseInOut: + return (t < 0.5f) ? (2.0f * t * t) : (1.0f - 2.0f * (1.0f - t) * (1.0f - t)); + case AnimationInterpolation::Linear: + default: + return t; + } + }; + + const char* interpLabels[] = { "Linear", "SmoothStep", "Ease In", "Ease Out", "Ease In Out" }; + const char* curveModeLabels[] = { "Preset", "Bezier" }; + + auto getInterpLabel = [&](AnimationInterpolation interpolation) { + int idx = static_cast(interpolation); + if (idx < 0 || idx >= static_cast(IM_ARRAYSIZE(interpLabels))) return "Linear"; + return interpLabels[idx]; + }; + + auto cubicBezier = [](float p0, float p1, float p2, float p3, float t) { + float inv = 1.0f - t; + return (inv * inv * inv * p0) + + (3.0f * inv * inv * t * p1) + + (3.0f * inv * t * t * p2) + + (t * t * t * p3); + }; + + auto cubicBezierDerivative = [](float p0, float p1, float p2, float p3, float t) { + float inv = 1.0f - t; + return (3.0f * inv * inv * (p1 - p0)) + + (6.0f * inv * t * (p2 - p1)) + + (3.0f * t * t * (p3 - p2)); + }; + + auto applyBezier = [&](float t, const glm::vec2& outCtrl, const glm::vec2& inCtrl) { + t = std::max(0.0f, std::min(1.0f, t)); + float u = t; + for (int i = 0; i < 6; ++i) { + float x = cubicBezier(0.0f, outCtrl.x, inCtrl.x, 1.0f, u); + float dx = cubicBezierDerivative(0.0f, outCtrl.x, inCtrl.x, 1.0f, u); + if (std::abs(dx) < 0.0001f) break; + u -= (x - t) / dx; + u = std::max(0.0f, std::min(1.0f, u)); + } + float xCheck = cubicBezier(0.0f, outCtrl.x, inCtrl.x, 1.0f, u); + if (std::abs(xCheck - t) > 0.001f) { + float lo = 0.0f; + float hi = 1.0f; + for (int i = 0; i < 12; ++i) { + float mid = (lo + hi) * 0.5f; + float x = cubicBezier(0.0f, outCtrl.x, inCtrl.x, 1.0f, mid); + if (x < t) lo = mid; + else hi = mid; + } + u = (lo + hi) * 0.5f; + } + return cubicBezier(0.0f, outCtrl.y, inCtrl.y, 1.0f, u); + }; + + auto captureKeyframe = [&](SceneObject& obj) { + auto& anim = obj.animation; + float clamped = clampFloat(animationCurrentTime, 0.0f, anim.clipLength); + auto it = std::find_if(anim.keyframes.begin(), anim.keyframes.end(), + [&](const AnimationKeyframe& k) { return std::abs(k.time - clamped) < 0.0001f; }); + if (it == anim.keyframes.end()) { + AnimationKeyframe key; + key.time = clamped; + key.position = obj.position; + key.rotation = obj.rotation; + key.scale = obj.scale; + key.interpolation = AnimationInterpolation::SmoothStep; + key.curveMode = AnimationCurveMode::Preset; + anim.keyframes.push_back(key); + } else { + it->position = obj.position; + it->rotation = obj.rotation; + it->scale = obj.scale; + } + std::sort(anim.keyframes.begin(), anim.keyframes.end(), + [](const AnimationKeyframe& a, const AnimationKeyframe& b) { return a.time < b.time; }); + projectManager.currentProject.hasUnsavedChanges = true; + }; + + auto deleteKeyframe = [&](SceneObject& obj) { + auto& anim = obj.animation; + if (animationSelectedKey < 0 || animationSelectedKey >= static_cast(anim.keyframes.size())) return; + anim.keyframes.erase(anim.keyframes.begin() + animationSelectedKey); + if (animationSelectedKey >= static_cast(anim.keyframes.size())) { + animationSelectedKey = static_cast(anim.keyframes.size()) - 1; + } + projectManager.currentProject.hasUnsavedChanges = true; + }; + + auto applyPoseAtTime = [&](SceneObject& obj, float time) { + auto& anim = obj.animation; + if (anim.keyframes.empty()) return; + + if (time <= anim.keyframes.front().time) { + obj.position = anim.keyframes.front().position; + obj.rotation = NormalizeEulerDegrees(anim.keyframes.front().rotation); + obj.scale = anim.keyframes.front().scale; + syncLocalTransform(obj); + projectManager.currentProject.hasUnsavedChanges = true; + return; + } + if (time >= anim.keyframes.back().time) { + obj.position = anim.keyframes.back().position; + obj.rotation = NormalizeEulerDegrees(anim.keyframes.back().rotation); + obj.scale = anim.keyframes.back().scale; + syncLocalTransform(obj); + projectManager.currentProject.hasUnsavedChanges = true; + return; + } + + for (size_t i = 0; i + 1 < anim.keyframes.size(); ++i) { + const auto& a = anim.keyframes[i]; + const auto& b = anim.keyframes[i + 1]; + if (time >= a.time && time <= b.time) { + float span = b.time - a.time; + float t = (span > 0.0f) ? (time - a.time) / span : 0.0f; + if (a.curveMode == AnimationCurveMode::Bezier) { + t = applyBezier(t, a.bezierOut, b.bezierIn); + } else { + t = applyInterpolation(t, a.interpolation); + } + obj.position = lerpVec3(a.position, b.position, t); + obj.rotation = NormalizeEulerDegrees(lerpVec3(a.rotation, b.rotation, t)); + obj.scale = lerpVec3(a.scale, b.scale, t); + syncLocalTransform(obj); + projectManager.currentProject.hasUnsavedChanges = true; + return; + } + } + }; + + auto drawTimeline = [&](AnimationComponent& anim) { + ImVec2 size = ImVec2(ImGui::GetContentRegionAvail().x, 70.0f); + ImVec2 start = ImGui::GetCursorScreenPos(); + ImGui::InvisibleButton("AnimationTimeline", size); + + ImDrawList* draw = ImGui::GetWindowDrawList(); + ImU32 bg = ImGui::GetColorU32(ImGuiCol_FrameBg); + ImU32 border = ImGui::GetColorU32(ImGuiCol_Border); + ImU32 accent = ImGui::GetColorU32(ImGuiCol_CheckMark); + ImU32 keyColor = ImGui::GetColorU32(ImGuiCol_SliderGrab); + + draw->AddRectFilled(start, ImVec2(start.x + size.x, start.y + size.y), bg, 6.0f); + draw->AddRect(start, ImVec2(start.x + size.x, start.y + size.y), border, 6.0f); + + float clamped = clampFloat(animationCurrentTime, 0.0f, anim.clipLength); + float playheadX = start.x + (anim.clipLength > 0.0f ? (clamped / anim.clipLength) * size.x : 0.0f); + draw->AddLine(ImVec2(playheadX, start.y), ImVec2(playheadX, start.y + size.y), accent, 2.0f); + + for (size_t i = 0; i < anim.keyframes.size(); ++i) { + float keyX = start.x + + (anim.clipLength > 0.0f ? (anim.keyframes[i].time / anim.clipLength) * size.x : 0.0f); + ImVec2 center(keyX, start.y + size.y * 0.5f); + float radius = (animationSelectedKey == static_cast(i)) ? 6.0f : 4.5f; + draw->AddCircleFilled(center, radius, keyColor); + + ImRect hit(ImVec2(center.x - 7.0f, center.y - 7.0f), ImVec2(center.x + 7.0f, center.y + 7.0f)); + if (ImGui::IsMouseHoveringRect(hit.Min, hit.Max) && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + animationSelectedKey = static_cast(i); + animationCurrentTime = anim.keyframes[i].time; + } + } + + if (ImGui::IsItemActive() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + float mouseX = ImGui::GetIO().MousePos.x; + float t = (mouseX - start.x) / size.x; + animationCurrentTime = clampFloat(t * anim.clipLength, 0.0f, anim.clipLength); + } + }; + + auto* selectedObj = getSelectedObject(); + std::vector animTargets; + animTargets.reserve(sceneObjects.size()); + for (auto& obj : sceneObjects) { + if (obj.hasAnimation) animTargets.push_back(&obj); + } + + auto resolveTarget = [&]() -> SceneObject* { + if (animationTargetId < 0) return nullptr; + SceneObject* obj = findObjectById(animationTargetId); + if (!obj || !obj->hasAnimation) return nullptr; + return obj; + }; + + SceneObject* targetObj = resolveTarget(); + if (!targetObj && !animTargets.empty()) { + animationTargetId = animTargets.front()->id; + animationSelectedKey = -1; + animationLastAppliedTime = -1.0f; + targetObj = resolveTarget(); + } + + ImGui::Begin("Animation", &showAnimationWindow, ImGuiWindowFlags_NoCollapse); + + if (!ImGui::BeginTable("AnimatorLayout", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV)) { + ImGui::End(); + return; + } + + ImGui::TableSetupColumn("Targets", ImGuiTableColumnFlags_WidthFixed, 220.0f); + ImGui::TableSetupColumn("Editor", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableNextRow(); + + ImGui::TableSetColumnIndex(0); + ImGui::BeginChild("AnimatorTargets", ImVec2(0, 0), true); + ImGui::TextDisabled("Targets"); + ImGui::Spacing(); + ImGui::BeginDisabled(!selectedObj); + if (ImGui::Button("Add Animation to Selected", ImVec2(-1, 0))) { + if (selectedObj && !selectedObj->hasAnimation) { + selectedObj->hasAnimation = true; + selectedObj->animation = AnimationComponent{}; + projectManager.currentProject.hasUnsavedChanges = true; + animationTargetId = selectedObj->id; + animationSelectedKey = -1; + animationLastAppliedTime = -1.0f; + animTargets.push_back(selectedObj); + } else if (selectedObj) { + animationTargetId = selectedObj->id; + } + } + ImGui::EndDisabled(); + ImGui::Spacing(); + + if (animTargets.empty()) { + ImGui::TextDisabled("No Animation components yet."); + } else { + for (auto* obj : animTargets) { + bool selected = (targetObj && obj->id == targetObj->id); + if (ImGui::Selectable(obj->name.c_str(), selected)) { + animationTargetId = obj->id; + animationSelectedKey = -1; + animationLastAppliedTime = -1.0f; + animationCurrentTime = 0.0f; + targetObj = obj; + } + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("Keyframes: %zu", obj->animation.keyframes.size()); + ImGui::Text("Length: %.2fs", obj->animation.clipLength); + ImGui::EndTooltip(); + } + } + } + ImGui::EndChild(); + + ImGui::TableSetColumnIndex(1); + ImGui::BeginChild("AnimatorEditor", ImVec2(0, 0), true); + + if (!targetObj) { + ImGui::TextDisabled("Select or add an Animation component to edit."); + ImGui::EndChild(); + ImGui::EndTable(); + ImGui::End(); + return; + } + + auto& anim = targetObj->animation; + animationCurrentTime = clampFloat(animationCurrentTime, 0.0f, anim.clipLength); + + ImGui::Text("Animator"); + ImGui::SameLine(); + ImGui::TextDisabled("Target: %s", targetObj->name.c_str()); + + ImGui::Spacing(); + ImGui::Separator(); + + if (ImGui::BeginTabBar("AnimatorTabs")) { + if (ImGui::BeginTabItem("Pose")) { + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f); + ImGui::BeginDisabled(!anim.enabled); + if (ImGui::Button("Key")) { + captureKeyframe(*targetObj); + } + ImGui::EndDisabled(); + ImGui::SameLine(); + ImGui::BeginDisabled(animationSelectedKey < 0); + if (ImGui::Button("Delete")) { + deleteKeyframe(*targetObj); + } + ImGui::EndDisabled(); + ImGui::SameLine(); + ImGui::BeginDisabled(anim.keyframes.empty()); + if (ImGui::Button("Sort")) { + std::sort(anim.keyframes.begin(), anim.keyframes.end(), + [](const AnimationKeyframe& a, const AnimationKeyframe& b) { return a.time < b.time; }); + projectManager.currentProject.hasUnsavedChanges = true; + } + ImGui::EndDisabled(); + ImGui::PopStyleVar(); + + ImGui::Spacing(); + drawTimeline(anim); + ImGui::SliderFloat("Time", &animationCurrentTime, 0.0f, anim.clipLength, "%.2fs"); + + if (animationSelectedKey >= 0 && animationSelectedKey < static_cast(anim.keyframes.size())) { + auto& key = anim.keyframes[animationSelectedKey]; + ImGui::Separator(); + ImGui::TextDisabled("Blend"); + int modeIndex = static_cast(key.curveMode); + ImGui::SetNextItemWidth(200.0f); + if (ImGui::Combo("Mode", &modeIndex, curveModeLabels, IM_ARRAYSIZE(curveModeLabels))) { + key.curveMode = static_cast(modeIndex); + projectManager.currentProject.hasUnsavedChanges = true; + } + ImGui::SameLine(); + if (ImGui::Button("Apply Mode To All")) { + for (auto& k : anim.keyframes) { + k.curveMode = key.curveMode; + } + projectManager.currentProject.hasUnsavedChanges = true; + } + + if (key.curveMode == AnimationCurveMode::Preset) { + int interpIndex = static_cast(key.interpolation); + ImGui::SetNextItemWidth(200.0f); + if (ImGui::Combo("Preset", &interpIndex, interpLabels, IM_ARRAYSIZE(interpLabels))) { + key.interpolation = static_cast(interpIndex); + projectManager.currentProject.hasUnsavedChanges = true; + } + ImGui::SameLine(); + if (ImGui::Button("Apply Preset To All")) { + for (auto& k : anim.keyframes) { + k.interpolation = key.interpolation; + } + projectManager.currentProject.hasUnsavedChanges = true; + } + } else { + ImGui::TextDisabled("Out Handle (to next)"); + ImGui::SetNextItemWidth(160.0f); + if (ImGui::SliderFloat2("Out", &key.bezierOut.x, 0.0f, 1.0f, "%.2f")) { + key.bezierOut.x = clampFloat(key.bezierOut.x, 0.0f, 1.0f); + key.bezierOut.y = clampFloat(key.bezierOut.y, 0.0f, 1.0f); + projectManager.currentProject.hasUnsavedChanges = true; + } + ImGui::TextDisabled("In Handle (from prev)"); + ImGui::SetNextItemWidth(160.0f); + if (ImGui::SliderFloat2("In", &key.bezierIn.x, 0.0f, 1.0f, "%.2f")) { + key.bezierIn.x = clampFloat(key.bezierIn.x, 0.0f, 1.0f); + key.bezierIn.y = clampFloat(key.bezierIn.y, 0.0f, 1.0f); + projectManager.currentProject.hasUnsavedChanges = true; + } + + int nextIndex = animationSelectedKey + 1; + if (nextIndex < static_cast(anim.keyframes.size())) { + static int activeHandle = -1; + auto& nextKey = anim.keyframes[nextIndex]; + ImVec2 previewSize(260.0f, 110.0f); + ImGui::TextDisabled("Curve Editor"); + ImGui::BeginChild("BezierPreview", previewSize, true, ImGuiWindowFlags_NoScrollbar); + ImDrawList* draw = ImGui::GetWindowDrawList(); + ImVec2 p0 = ImGui::GetCursorScreenPos(); + ImVec2 p1(p0.x + previewSize.x, p0.y + previewSize.y); + ImU32 grid = ImGui::GetColorU32(ImGuiCol_Border); + ImU32 handleColor = ImGui::GetColorU32(ImGuiCol_SliderGrab); + ImU32 lineColor = ImGui::GetColorU32(ImGuiCol_CheckMark); + draw->AddRect(p0, p1, grid); + + auto toScreen = [&](const glm::vec2& v) { + return ImVec2(p0.x + v.x * previewSize.x, p0.y + (1.0f - v.y) * previewSize.y); + }; + + ImVec2 outHandle = toScreen(key.bezierOut); + ImVec2 inHandle = toScreen(nextKey.bezierIn); + ImVec2 start = toScreen(glm::vec2(0.0f, 0.0f)); + ImVec2 end = toScreen(glm::vec2(1.0f, 1.0f)); + draw->AddLine(start, outHandle, grid, 1.0f); + draw->AddLine(end, inHandle, grid, 1.0f); + draw->AddCircleFilled(outHandle, 5.0f, handleColor); + draw->AddCircleFilled(inHandle, 5.0f, handleColor); + + const int samples = 32; + ImVec2 last = start; + for (int i = 0; i <= samples; ++i) { + float t = static_cast(i) / samples; + float y = applyBezier(t, key.bezierOut, nextKey.bezierIn); + ImVec2 cur(p0.x + t * previewSize.x, p0.y + (1.0f - y) * previewSize.y); + if (i > 0) { + draw->AddLine(last, cur, lineColor, 2.0f); + } + last = cur; + } + + ImRect outRect(ImVec2(outHandle.x - 7.0f, outHandle.y - 7.0f), + ImVec2(outHandle.x + 7.0f, outHandle.y + 7.0f)); + ImRect inRect(ImVec2(inHandle.x - 7.0f, inHandle.y - 7.0f), + ImVec2(inHandle.x + 7.0f, inHandle.y + 7.0f)); + + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + if (ImGui::IsMouseHoveringRect(outRect.Min, outRect.Max)) activeHandle = 0; + else if (ImGui::IsMouseHoveringRect(inRect.Min, inRect.Max)) activeHandle = 1; + else activeHandle = -1; + } + if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { + activeHandle = -1; + } + + if (activeHandle >= 0 && ImGui::IsMouseDown(ImGuiMouseButton_Left)) { + ImVec2 mouse = ImGui::GetIO().MousePos; + float x = (mouse.x - p0.x) / previewSize.x; + float y = 1.0f - (mouse.y - p0.y) / previewSize.y; + glm::vec2 clamped(clampFloat(x, 0.0f, 1.0f), clampFloat(y, 0.0f, 1.0f)); + if (activeHandle == 0) { + key.bezierOut = clamped; + } else { + nextKey.bezierIn = clamped; + } + projectManager.currentProject.hasUnsavedChanges = true; + } + + ImGui::EndChild(); + } + } + } + + ImGui::Spacing(); + if (anim.keyframes.empty()) { + ImGui::TextDisabled("No keyframes yet."); + } else if (ImGui::BeginTable("AnimationKeyframeTable", 5, + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit)) { + ImGui::TableSetupColumn("Time"); + ImGui::TableSetupColumn("Blend"); + ImGui::TableSetupColumn("Position"); + ImGui::TableSetupColumn("Rotation"); + ImGui::TableSetupColumn("Scale"); + ImGui::TableHeadersRow(); + + for (size_t i = 0; i < anim.keyframes.size(); ++i) { + const auto& key = anim.keyframes[i]; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + bool selected = animationSelectedKey == static_cast(i); + char label[32]; + std::snprintf(label, sizeof(label), "%.2f", key.time); + if (ImGui::Selectable(label, selected, ImGuiSelectableFlags_SpanAllColumns)) { + animationSelectedKey = static_cast(i); + animationCurrentTime = key.time; + } + ImGui::TableNextColumn(); + if (key.curveMode == AnimationCurveMode::Bezier) { + ImGui::TextUnformatted("Bezier"); + } else { + ImGui::TextUnformatted(getInterpLabel(key.interpolation)); + } + ImGui::TableNextColumn(); + ImGui::Text("%.2f, %.2f, %.2f", key.position.x, key.position.y, key.position.z); + ImGui::TableNextColumn(); + ImGui::Text("%.2f, %.2f, %.2f", key.rotation.x, key.rotation.y, key.rotation.z); + ImGui::TableNextColumn(); + ImGui::Text("%.2f, %.2f, %.2f", key.scale.x, key.scale.y, key.scale.z); + } + ImGui::EndTable(); + } + + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Config")) { + if (ImGui::DragFloat("Clip Length", &anim.clipLength, 0.05f, 0.1f, 120.0f, "%.2f")) { + anim.clipLength = std::max(0.1f, anim.clipLength); + projectManager.currentProject.hasUnsavedChanges = true; + animationCurrentTime = clampFloat(animationCurrentTime, 0.0f, anim.clipLength); + } + if (ImGui::DragFloat("Play Speed", &anim.playSpeed, 0.05f, 0.05f, 8.0f, "%.2f")) { + anim.playSpeed = std::max(0.05f, anim.playSpeed); + projectManager.currentProject.hasUnsavedChanges = true; + } + if (ImGui::Checkbox("Loop", &anim.loop)) { + projectManager.currentProject.hasUnsavedChanges = true; + } + if (ImGui::Checkbox("Apply On Scrub", &anim.applyOnScrub)) { + projectManager.currentProject.hasUnsavedChanges = true; + } + ImGui::Spacing(); + if (ImGui::Button("Clear Keyframes")) { + anim.keyframes.clear(); + animationSelectedKey = -1; + projectManager.currentProject.hasUnsavedChanges = true; + } + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::TextDisabled("Transport"); + ImGui::BeginDisabled(!anim.enabled); + if (ImGui::Button(animationIsPlaying ? "Pause" : "Play")) { + animationIsPlaying = !animationIsPlaying; + animationLastAppliedTime = -1.0f; + } + ImGui::EndDisabled(); + ImGui::SameLine(); + if (ImGui::Button("Stop")) { + animationIsPlaying = false; + animationCurrentTime = 0.0f; + animationLastAppliedTime = -1.0f; + } + ImGui::SameLine(); + ImGui::TextDisabled("Time: %.2fs / %.2fs", animationCurrentTime, anim.clipLength); + + if (animationIsPlaying && anim.clipLength > 0.0f) { + animationCurrentTime += ImGui::GetIO().DeltaTime * anim.playSpeed; + if (animationCurrentTime > anim.clipLength) { + if (anim.loop) { + animationCurrentTime = std::fmod(animationCurrentTime, anim.clipLength); + } else { + animationCurrentTime = anim.clipLength; + animationIsPlaying = false; + } + } + } + + if (anim.enabled && (animationIsPlaying || anim.applyOnScrub)) { + if (animationIsPlaying || std::abs(animationCurrentTime - animationLastAppliedTime) > 0.0001f) { + applyPoseAtTime(*targetObj, animationCurrentTime); + animationLastAppliedTime = animationCurrentTime; + } + } + + ImGui::EndChild(); + ImGui::EndTable(); + ImGui::End(); +} diff --git a/src/EditorWindows/BuildSettingsWindow.cpp b/src/EditorWindows/BuildSettingsWindow.cpp new file mode 100644 index 0000000..4f6dda2 --- /dev/null +++ b/src/EditorWindows/BuildSettingsWindow.cpp @@ -0,0 +1,253 @@ +#include "Engine.h" + +void Engine::renderBuildSettingsWindow() { + if (!showBuildSettings) return; + + ImGui::SetNextWindowSize(ImVec2(760, 520), ImGuiCond_FirstUseEver); + if (!ImGui::Begin("Build Settings", &showBuildSettings)) { + ImGui::End(); + return; + } + + if (!projectManager.currentProject.isLoaded) { + ImGui::TextDisabled("No project loaded."); + ImGui::End(); + return; + } + + bool changed = false; + + ImGui::BeginChild("BuildScenesList", ImVec2(0, 150), true); + ImGui::Text("Scenes In Build"); + ImGui::Separator(); + for (int i = 0; i < static_cast(buildSettings.scenes.size()); ++i) { + BuildSceneEntry& entry = buildSettings.scenes[i]; + ImGui::PushID(i); + bool enabled = entry.enabled; + if (ImGui::Checkbox("##enabled", &enabled)) { + entry.enabled = enabled; + changed = true; + } + ImGui::SameLine(); + bool selected = (buildSettingsSelectedIndex == i); + if (ImGui::Selectable(entry.name.c_str(), selected, ImGuiSelectableFlags_SpanAllColumns)) { + buildSettingsSelectedIndex = i; + } + float rightX = ImGui::GetWindowContentRegionMax().x; + ImGui::SameLine(rightX - 24.0f); + ImGui::TextDisabled("%d", i); + ImGui::PopID(); + } + ImGui::EndChild(); + + float buttonSpacing = ImGui::GetStyle().ItemSpacing.x; + float addWidth = 150.0f; + float removeWidth = 130.0f; + float totalButtons = addWidth + removeWidth + buttonSpacing; + float buttonStart = ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - totalButtons; + if (buttonStart > ImGui::GetCursorPosX()) { + ImGui::SetCursorPosX(buttonStart); + } + if (ImGui::Button("Remove Selected", ImVec2(removeWidth, 0.0f))) { + if (buildSettingsSelectedIndex >= 0 && + buildSettingsSelectedIndex < static_cast(buildSettings.scenes.size())) { + buildSettings.scenes.erase(buildSettings.scenes.begin() + buildSettingsSelectedIndex); + if (buildSettingsSelectedIndex >= static_cast(buildSettings.scenes.size())) { + buildSettingsSelectedIndex = static_cast(buildSettings.scenes.size()) - 1; + } + changed = true; + } + } + ImGui::SameLine(); + if (ImGui::Button("Add Open Scenes", ImVec2(addWidth, 0.0f))) { + if (addSceneToBuildSettings(projectManager.currentProject.currentSceneName, true)) { + changed = true; + } + } + + ImGui::Spacing(); + ImGui::Text("Platform"); + ImGui::Separator(); + + ImGui::BeginChild("BuildPlatforms", ImVec2(220, 0), true); + ImGui::Selectable("Windows & Linux Standalone", true); + ImGui::BeginDisabled(true); + ImGui::Selectable("Android", false); + ImGui::Selectable("Android | Meta Quest", false); + ImGui::EndDisabled(); + ImGui::EndChild(); + + ImGui::SameLine(); + ImGui::BeginChild("BuildPlatformSettings", ImVec2(0, 0), true); + ImGui::Text("Target Platform"); + const char* targets[] = {"Windows", "Linux"}; + int targetIndex = (buildSettings.platform == BuildPlatform::Linux) ? 1 : 0; + if (ImGui::Combo("##target-platform", &targetIndex, targets, 2)) { + buildSettings.platform = (targetIndex == 1) ? BuildPlatform::Linux : BuildPlatform::Windows; + changed = true; + } + + ImGui::Text("Architecture"); + const char* arches[] = {"x86_64", "x86"}; + int archIndex = (buildSettings.architecture == "x86") ? 1 : 0; + if (ImGui::Combo("##architecture", &archIndex, arches, 2)) { + buildSettings.architecture = arches[archIndex]; + changed = true; + } + + ImGui::Spacing(); + if (ImGui::Checkbox("Server Build", &buildSettings.serverBuild)) changed = true; + if (ImGui::Checkbox("Development Build", &buildSettings.developmentBuild)) changed = true; + if (ImGui::Checkbox("Autoconnect Profiler", &buildSettings.autoConnectProfiler)) changed = true; + if (ImGui::Checkbox("Deep Profiling Support", &buildSettings.deepProfiling)) changed = true; + if (ImGui::Checkbox("Script Debugging", &buildSettings.scriptDebugging)) changed = true; + if (ImGui::Checkbox("Scripts Only Build", &buildSettings.scriptsOnlyBuild)) changed = true; + + ImGui::Spacing(); + ImGui::Text("Compression Method"); + const char* compressionOptions[] = {"Default", "None", "LZ4", "LZ4HC"}; + int compressionIndex = 0; + for (int i = 0; i < 4; ++i) { + if (buildSettings.compressionMethod == compressionOptions[i]) { + compressionIndex = i; + break; + } + } + if (ImGui::Combo("##compression", &compressionIndex, compressionOptions, 4)) { + buildSettings.compressionMethod = compressionOptions[compressionIndex]; + changed = true; + } + ImGui::TextDisabled("Android support will unlock after OpenGLES is available."); + ImGui::EndChild(); + + ImGui::Separator(); + float buildWidth = 90.0f; + float buildRunWidth = 120.0f; + float buildTotal = buildWidth + buildRunWidth + buttonSpacing; + float buildStart = ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - buildTotal; + if (buildStart > ImGui::GetCursorPosX()) { + ImGui::SetCursorPosX(buildStart); + } + if (ImGui::Button("Export Game", ImVec2(buildWidth, 0.0f))) { + exportRunAfter = false; + if (exportOutputPath[0] == '\0') { + fs::path defaultOut = projectManager.currentProject.projectPath / "Builds"; + std::snprintf(exportOutputPath, sizeof(exportOutputPath), "%s", defaultOut.string().c_str()); + } + showExportDialog = true; + } + ImGui::SameLine(); + if (ImGui::Button("Export & Run", ImVec2(buildRunWidth, 0.0f))) { + exportRunAfter = true; + if (exportOutputPath[0] == '\0') { + fs::path defaultOut = projectManager.currentProject.projectPath / "Builds"; + std::snprintf(exportOutputPath, sizeof(exportOutputPath), "%s", defaultOut.string().c_str()); + } + showExportDialog = true; + } + + if (changed) { + saveBuildSettings(); + } + + if (showExportDialog) { + ImGui::SetNextWindowSize(ImVec2(720, 460), ImGuiCond_Appearing); + ImGui::OpenPopup("Export Game"); + showExportDialog = false; + } + + bool exportPopupOpen = true; + ImGuiWindowFlags popupFlags = ImGuiWindowFlags_NoDocking; + bool exportActive = false; + bool exportDone = false; + bool exportSuccess = false; + float exportProgress = 0.0f; + std::string exportStatus; + std::string exportLog; + fs::path exportDir; + { + std::lock_guard lock(exportMutex); + exportActive = exportJob.active; + exportDone = exportJob.done; + exportSuccess = exportJob.success; + exportProgress = exportJob.progress; + exportStatus = exportJob.status; + exportLog = exportJob.log; + exportDir = exportJob.outputDir; + } + bool allowClose = !exportActive; + if (ImGui::BeginPopupModal("Export Game", allowClose ? &exportPopupOpen : nullptr, popupFlags)) { + ImGui::Text("Output Folder"); + ImGui::SetNextItemWidth(-1); + ImGui::BeginDisabled(exportActive); + ImGui::InputText("##ExportOutput", exportOutputPath, sizeof(exportOutputPath)); + ImGui::EndDisabled(); + + if (!exportActive) { + if (ImGui::Button("Use Selected Folder")) { + if (!fileBrowser.selectedFile.empty()) { + fs::path selected = fileBrowser.selectedFile; + fs::path folder = fs::is_directory(selected) ? selected : selected.parent_path(); + std::snprintf(exportOutputPath, sizeof(exportOutputPath), "%s", folder.string().c_str()); + } + } + ImGui::SameLine(); + if (ImGui::Button("Use Project Folder")) { + fs::path folder = projectManager.currentProject.projectPath / "Builds"; + std::snprintf(exportOutputPath, sizeof(exportOutputPath), "%s", folder.string().c_str()); + } + } + + ImGui::Spacing(); + if (exportActive || exportDone) { + const char* statusLabel = exportStatus.empty() ? "Working..." : exportStatus.c_str(); + float barValue = exportActive ? exportProgress : 1.0f; + if (barValue <= 0.0f) barValue = 0.02f; + ImGui::ProgressBar(barValue, ImVec2(-1, 0), statusLabel); + if (exportActive) { + ImGui::TextDisabled("Build can take a while (PhysX/assimp). Output updates after each step finishes."); + } + ImGui::BeginChild("ExportLog", ImVec2(0, 180), true); + if (exportLog.empty()) { + ImGui::TextUnformatted("Waiting for build output..."); + } else { + ImGui::TextUnformatted(exportLog.c_str()); + } + ImGui::EndChild(); + } + + ImGui::Separator(); + if (!exportActive && !exportDone) { + if (ImGui::Button("Start Export", ImVec2(120, 0))) { + if (!exportOutputPath[0]) { + addConsoleMessage("Please choose an export folder.", ConsoleMessageType::Warning); + } else { + startExportBuild(fs::path(exportOutputPath), exportRunAfter); + } + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(100, 0))) { + ImGui::CloseCurrentPopup(); + } + } else if (!exportActive && exportDone) { + if (exportSuccess && !exportDir.empty()) { + ImGui::TextDisabled("Exported to: %s", exportDir.string().c_str()); + } + if (ImGui::Button("Close", ImVec2(100, 0))) { + ImGui::CloseCurrentPopup(); + std::lock_guard lock(exportMutex); + exportJob = ExportJobState{}; + } + } else { + if (ImGui::Button("Cancel Export", ImVec2(140, 0))) { + exportCancelRequested = true; + std::lock_guard lock(exportMutex); + exportJob.status = "Cancelling..."; + } + } + + ImGui::EndPopup(); + } + + ImGui::End(); +} diff --git a/src/EditorWindows/FileBrowserWindow.cpp b/src/EditorWindows/FileBrowserWindow.cpp index f8636d9..d494097 100644 --- a/src/EditorWindows/FileBrowserWindow.cpp +++ b/src/EditorWindows/FileBrowserWindow.cpp @@ -501,6 +501,7 @@ void Engine::renderFileBrowserPanel() { static fs::path pendingDeletePath; static fs::path pendingRenamePath; static char renameName[256] = ""; + bool settingsDirty = false; auto openEntry = [&](const fs::directory_entry& entry) { if (entry.is_directory()) { @@ -536,6 +537,10 @@ void Engine::renderFileBrowserPanel() { logToConsole("Loaded scene: " + sceneName); return; } + if (fileBrowser.getFileCategory(entry) == FileCategory::Script) { + openScriptInEditor(entry.path()); + return; + } openPathInShell(entry.path()); }; @@ -623,6 +628,15 @@ void Engine::renderFileBrowserPanel() { } return false; }; + + auto normalizePath = [](const fs::path& path) { + std::error_code ec; + fs::path canonical = fs::weakly_canonical(path, ec); + if (!ec) { + return canonical; + } + return path.lexically_normal(); + }; // Get colors for categories auto getCategoryColor = [](FileCategory cat) -> ImU32 { @@ -742,16 +756,24 @@ void Engine::renderFileBrowserPanel() { ImGui::TextDisabled("Size"); ImGui::SameLine(); ImGui::SetNextItemWidth(90); - ImGui::SliderFloat("##IconScale", &fileBrowserIconScale, 0.6f, 2.0f, "%.1fx"); + if (ImGui::SliderFloat("##IconScale", &fileBrowserIconScale, 0.6f, 2.0f, "%.1fx")) { + settingsDirty = true; + } if (ImGui::IsItemHovered()) ImGui::SetTooltip("Icon Size: %.1fx", fileBrowserIconScale); ImGui::SameLine(); } if (ImGui::Button(isGridMode ? "Grid" : "List", ImVec2(54, 0))) { fileBrowser.viewMode = isGridMode ? FileBrowserViewMode::List : FileBrowserViewMode::Grid; + settingsDirty = true; } if (ImGui::IsItemHovered()) ImGui::SetTooltip(isGridMode ? "Switch to List View" : "Switch to Grid View"); ImGui::SameLine(); + if (ImGui::Button(showFileBrowserSidebar ? "Side" : "Side", ImVec2(52, 0))) { + showFileBrowserSidebar = !showFileBrowserSidebar; + settingsDirty = true; + } + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Toggle sidebar"); ImGui::EndChild(); ImGui::PopStyleVar(2); @@ -766,9 +788,162 @@ void Engine::renderFileBrowserPanel() { contentBg.z = std::min(contentBg.z + 0.01f, 1.0f); ImGui::PushStyleColor(ImGuiCol_ChildBg, contentBg); ImGui::BeginChild("FileContent", ImVec2(0, 0), true); - + if (showFileBrowserSidebar) { + float minSidebarWidth = 160.0f; + float maxSidebarWidth = std::max(minSidebarWidth, ImGui::GetContentRegionAvail().x * 0.5f); + fileBrowserSidebarWidth = std::clamp(fileBrowserSidebarWidth, minSidebarWidth, maxSidebarWidth); + + ImGui::BeginChild("FileSidebar", ImVec2(fileBrowserSidebarWidth, 0), true); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.0f, 4.0f)); + ImGui::TextDisabled("Favorites"); + ImGui::SameLine(); + if (ImGui::SmallButton("+")) { + fs::path current = normalizePath(fileBrowser.currentPath); + bool exists = false; + for (const auto& fav : fileBrowserFavorites) { + if (normalizePath(fav) == current) { + exists = true; + break; + } + } + if (!exists) { + fileBrowserFavorites.push_back(current); + settingsDirty = true; + } + } + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Add current folder"); + + fs::path baseRoot = fileBrowser.projectRoot.empty() + ? projectManager.currentProject.projectPath + : fileBrowser.projectRoot; + fs::path normalizedCurrent = normalizePath(fileBrowser.currentPath); + + for (size_t i = 0; i < fileBrowserFavorites.size(); ++i) { + fs::path fav = fileBrowserFavorites[i]; + std::string label; + std::error_code ec; + fs::path rel = fs::relative(fav, baseRoot, ec); + std::string relStr = rel.generic_string(); + if (!ec && !rel.empty() && relStr.find("..") != 0) { + label = relStr; + if (label.empty() || label == ".") { + label = "Project"; + } + } else { + label = fav.filename().string(); + if (label.empty()) { + label = fav.string(); + } + } + + bool exists = fs::exists(fav); + ImGui::PushID(static_cast(i)); + if (!exists) { + ImGui::BeginDisabled(); + } + if (ImGui::Selectable(label.c_str(), normalizePath(fav) == normalizedCurrent)) { + if (exists) { + fileBrowser.navigateTo(fav); + } + } + if (!exists) { + ImGui::EndDisabled(); + } + if (ImGui::BeginPopupContextItem("FavContext")) { + if (ImGui::MenuItem("Remove")) { + fileBrowserFavorites.erase(fileBrowserFavorites.begin() + static_cast(i)); + settingsDirty = true; + ImGui::EndPopup(); + ImGui::PopID(); + break; + } + if (exists && ImGui::MenuItem("Open in File Explorer")) { + openPathInFileManager(fav); + } + ImGui::EndPopup(); + } + ImGui::PopID(); + } + + ImGui::Separator(); + ImGui::TextDisabled("Folders"); + ImGui::BeginChild("FolderTree", ImVec2(0, 0), false); + + auto drawFolderTree = [&](auto&& self, const fs::path& path) -> void { + if (!fs::exists(path)) { + return; + } + std::string name = path.filename().string(); + if (name.empty()) { + name = "Project"; + } + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | + ImGuiTreeNodeFlags_OpenOnDoubleClick | + ImGuiTreeNodeFlags_SpanFullWidth; + if (fileBrowser.currentPath == path) { + flags |= ImGuiTreeNodeFlags_Selected; + } + ImGui::PushID(path.string().c_str()); + bool open = ImGui::TreeNodeEx(name.c_str(), flags); + if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) { + fileBrowser.navigateTo(path); + } + if (open) { + std::vector dirs; + std::error_code ec; + for (const auto& entry : fs::directory_iterator(path, ec)) { + if (ec) { + break; + } + if (!entry.is_directory()) { + continue; + } + std::string dirName = entry.path().filename().string(); + if (!fileBrowser.showHiddenFiles && !dirName.empty() && dirName[0] == '.') { + continue; + } + dirs.push_back(entry.path()); + } + std::sort(dirs.begin(), dirs.end(), [](const fs::path& a, const fs::path& b) { + return a.filename().string() < b.filename().string(); + }); + for (const auto& dir : dirs) { + self(self, dir); + } + ImGui::TreePop(); + } + ImGui::PopID(); + }; + + if (!baseRoot.empty()) { + drawFolderTree(drawFolderTree, baseRoot); + } + + ImGui::EndChild(); + ImGui::PopStyleVar(); + ImGui::EndChild(); + + ImGui::SameLine(); + float splitterHeight = ImGui::GetContentRegionAvail().y; + if (splitterHeight < 1.0f) { + splitterHeight = 1.0f; + } + ImGui::InvisibleButton("SidebarSplitter", ImVec2(4.0f, splitterHeight)); + if (ImGui::IsItemHovered() || ImGui::IsItemActive()) { + ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW); + } + if (ImGui::IsItemActive()) { + fileBrowserSidebarWidth += ImGui::GetIO().MouseDelta.x; + fileBrowserSidebarWidth = std::clamp(fileBrowserSidebarWidth, minSidebarWidth, maxSidebarWidth); + settingsDirty = true; + } + ImGui::SameLine(); + } + + ImGui::BeginChild("FileMain", ImVec2(0, 0), false); + ImDrawList* drawList = ImGui::GetWindowDrawList(); - + if (fileBrowser.viewMode == FileBrowserViewMode::Grid) { float baseIconSize = 64.0f; float iconSize = baseIconSize * fileBrowserIconScale; @@ -776,10 +951,10 @@ void Engine::renderFileBrowserPanel() { float textHeight = 32.0f; // Space for filename text float cellWidth = iconSize + padding * 2; float cellHeight = iconSize + padding * 2 + textHeight; - + float windowWidth = ImGui::GetContentRegionAvail().x; int columns = std::max(1, (int)((windowWidth + padding) / (cellWidth + padding))); - + // Use a table for consistent grid layout if (ImGui::BeginTable("FileGrid", columns, ImGuiTableFlags_NoPadInnerX)) { for (int i = 0; i < (int)fileBrowser.entries.size(); i++) { @@ -787,44 +962,44 @@ void Engine::renderFileBrowserPanel() { std::string filename = entry.path().filename().string(); FileCategory category = fileBrowser.getFileCategory(entry); bool isSelected = fileBrowser.selectedFile == entry.path(); - + ImGui::TableNextColumn(); ImGui::PushID(i); - + // Cell content area ImVec2 cellStart = ImGui::GetCursorScreenPos(); ImVec2 cellEnd(cellStart.x + cellWidth, cellStart.y + cellHeight); - + // Invisible button for the entire cell if (ImGui::InvisibleButton("##cell", ImVec2(cellWidth, cellHeight))) { fileBrowser.selectedFile = entry.path(); } bool hovered = ImGui::IsItemHovered(); bool doubleClicked = hovered && ImGui::IsMouseDoubleClicked(0); - + // Draw background ImU32 bgColor = isSelected ? IM_COL32(70, 110, 160, 200) : (hovered ? IM_COL32(60, 65, 75, 180) : IM_COL32(0, 0, 0, 0)); if (bgColor != IM_COL32(0, 0, 0, 0)) { drawList->AddRectFilled(cellStart, cellEnd, bgColor, 6.0f); } - + // Draw border on selection if (isSelected) { drawList->AddRect(cellStart, cellEnd, IM_COL32(100, 150, 220, 255), 6.0f, 0, 2.0f); } - + // Draw icon centered in cell ImVec2 iconPos( cellStart.x + (cellWidth - iconSize) * 0.5f, cellStart.y + padding ); FileIcons::DrawIcon(drawList, category, iconPos, iconSize, getCategoryColor(category)); - + // Draw filename below icon (centered, with wrapping) std::string displayName = filename; float maxTextWidth = cellWidth - 4; - + // Truncate if too long ImVec2 textSize = ImGui::CalcTextSize(displayName.c_str()); if (textSize.x > maxTextWidth) { @@ -837,21 +1012,21 @@ void Engine::renderFileBrowserPanel() { displayName += "..."; textSize = ImGui::CalcTextSize(displayName.c_str()); } - + ImVec2 textPos( cellStart.x + (cellWidth - textSize.x) * 0.5f, cellStart.y + padding + iconSize + 4 ); - + // Text with subtle shadow for readability drawList->AddText(ImVec2(textPos.x + 1, textPos.y + 1), IM_COL32(0, 0, 0, 100), displayName.c_str()); drawList->AddText(textPos, IM_COL32(230, 230, 230, 255), displayName.c_str()); - + // Handle double click if (doubleClicked) { openEntry(entry); } - + // Context menu if (ImGui::BeginPopupContextItem("FileContextMenu")) { if (ImGui::MenuItem("Open")) { @@ -960,33 +1135,33 @@ void Engine::renderFileBrowserPanel() { ImGui::Text("%s", filename.c_str()); ImGui::EndDragDropSource(); } - + ImGui::PopID(); } ImGui::EndTable(); } - + } else { // List View ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 2)); - + for (int i = 0; i < (int)fileBrowser.entries.size(); i++) { const auto& entry = fileBrowser.entries[i]; std::string filename = entry.path().filename().string(); FileCategory category = fileBrowser.getFileCategory(entry); bool isSelected = fileBrowser.selectedFile == entry.path(); - + ImGui::PushID(i); - + // Selectable row if (ImGui::Selectable("##row", isSelected, ImGuiSelectableFlags_AllowDoubleClick, ImVec2(0, 20))) { fileBrowser.selectedFile = entry.path(); - + if (ImGui::IsMouseDoubleClicked(0)) { openEntry(entry); } } - + // Context menu if (ImGui::BeginPopupContextItem("FileContextMenu")) { if (ImGui::MenuItem("Open")) { @@ -1098,15 +1273,15 @@ void Engine::renderFileBrowserPanel() { ImGui::Text("%s", filename.c_str()); ImGui::EndDragDropSource(); } - + // Draw icon inline ImGui::SameLine(4); ImVec2 iconPos = ImGui::GetCursorScreenPos(); iconPos.y -= 2; FileIcons::DrawIcon(drawList, category, iconPos, 16, getCategoryColor(category)); - + ImGui::SameLine(26); - + // Color-coded filename ImVec4 textColor; switch (category) { @@ -1118,10 +1293,10 @@ void Engine::renderFileBrowserPanel() { default: textColor = ImVec4(0.85f, 0.85f, 0.85f, 1.0f); break; } ImGui::TextColored(textColor, "%s", filename.c_str()); - + ImGui::PopID(); } - + ImGui::PopStyleVar(); } @@ -1168,9 +1343,14 @@ void Engine::renderFileBrowserPanel() { ImGui::EndPopup(); } + ImGui::EndChild(); ImGui::EndChild(); ImGui::PopStyleColor(); + if (settingsDirty) { + saveEditorUserSettings(); + } + if (triggerDeletePopup) { ImGui::OpenPopup("Confirm Delete"); triggerDeletePopup = false; diff --git a/src/EditorWindows/ProjectManagerWindow.cpp b/src/EditorWindows/ProjectManagerWindow.cpp index 101a463..a687a79 100644 --- a/src/EditorWindows/ProjectManagerWindow.cpp +++ b/src/EditorWindows/ProjectManagerWindow.cpp @@ -103,6 +103,34 @@ bool Spinner(const char* label, float radius, int thickness, const ImU32& color) return true; } +bool ProgressCircle(const char* label, float radius, float thickness, float value, + const ImU32& color, const ImU32& bgColor) { + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + + ImVec2 pos = window->DC.CursorPos; + ImVec2 size((radius) * 2, (radius + style.FramePadding.y) * 2); + const ImRect bb(pos, ImVec2(pos.x + size.x, pos.y + size.y)); + ItemSize(bb, style.FramePadding.y); + if (!ItemAdd(bb, id)) + return false; + + ImVec2 centre = ImVec2(pos.x + radius, pos.y + radius + style.FramePadding.y); + float startAngle = -IM_PI * 0.5f; + float endAngle = startAngle + IM_PI * 2.0f * ImClamp(value, 0.0f, 1.0f); + + window->DrawList->AddCircle(centre, radius, bgColor, 32, thickness); + window->DrawList->PathClear(); + window->DrawList->PathArcTo(centre, radius, startAngle, endAngle, 32); + window->DrawList->PathStroke(color, false, thickness); + return true; +} + } // namespace ImGui #pragma endregion @@ -188,7 +216,7 @@ void Engine::renderLauncher() { ImGui::SetWindowFontScale(1.4f); ImGui::TextColored(ImVec4(0.95f, 0.96f, 0.98f, 1.0f), "Modularity"); ImGui::SetWindowFontScale(1.0f); - ImGui::TextColored(ImVec4(0.70f, 0.73f, 0.80f, 1.0f), "Modularity | Beta V1.0"); + ImGui::TextColored(ImVec4(0.70f, 0.73f, 0.80f, 1.0f), "Modularity | Beta V6.3"); ImGui::EndChild(); @@ -352,7 +380,7 @@ void Engine::renderLauncher() { ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - ImGui::TextDisabled("Modularity Engine - Beta V1.0"); + ImGui::TextDisabled("Modularity Engine - Beta V6.3"); ImGui::EndChild(); } @@ -365,7 +393,7 @@ void Engine::renderLauncher() { if (projectManager.showOpenProjectDialog) renderOpenProjectDialog(); - if (projectLoadInProgress) { + if (projectLoadInProgress || sceneLoadInProgress) { float elapsed = static_cast(glfwGetTime() - projectLoadStartTime); if (elapsed > 0.15f) { ImGuiIO& io = ImGui::GetIO(); @@ -396,16 +424,32 @@ void Engine::renderLauncher() { ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings); - ImGui::TextColored(ImVec4(0.88f, 0.90f, 0.96f, 1.0f), "Loading project..."); + const char* headline = sceneLoadInProgress ? "Loading scene..." : "Loading project..."; + ImGui::TextColored(ImVec4(0.88f, 0.90f, 0.96f, 1.0f), "%s", headline); ImGui::Spacing(); - ImGui::TextDisabled("%s", projectLoadPath.c_str()); + if (sceneLoadInProgress && !sceneLoadStatus.empty()) { + ImGui::TextDisabled("%s", sceneLoadStatus.c_str()); + } else if (!projectLoadPath.empty()) { + ImGui::TextDisabled("%s", projectLoadPath.c_str()); + } ImGui::Spacing(); - ImGui::Spinner("##project_load_spinner", 16.0f, 4, ImGui::GetColorU32(ImGuiCol_ButtonHovered)); - ImGui::SameLine(); - ImGui::BufferingBar("##project_load_bar", std::fmod(elapsed * 0.25f, 1.0f), - ImVec2(ImGui::GetContentRegionAvail().x - 40.0f, 8.0f), - ImGui::GetColorU32(ImGuiCol_Button), - ImGui::GetColorU32(ImGuiCol_ButtonHovered)); + if (sceneLoadInProgress) { + ImGui::ProgressCircle("##project_load_circle", 16.0f, 4.0f, sceneLoadProgress, + ImGui::GetColorU32(ImGuiCol_ButtonHovered), + ImGui::GetColorU32(ImGuiCol_Button)); + ImGui::SameLine(); + ImGui::BufferingBar("##project_load_bar", sceneLoadProgress, + ImVec2(ImGui::GetContentRegionAvail().x - 40.0f, 8.0f), + ImGui::GetColorU32(ImGuiCol_Button), + ImGui::GetColorU32(ImGuiCol_ButtonHovered)); + } else { + ImGui::Spinner("##project_load_spinner", 16.0f, 4, ImGui::GetColorU32(ImGuiCol_ButtonHovered)); + ImGui::SameLine(); + ImGui::BufferingBar("##project_load_bar", std::fmod(elapsed * 0.25f, 1.0f), + ImVec2(ImGui::GetContentRegionAvail().x - 40.0f, 8.0f), + ImGui::GetColorU32(ImGuiCol_Button), + ImGui::GetColorU32(ImGuiCol_ButtonHovered)); + } ImGui::End(); ImGui::PopStyleColor(); diff --git a/src/EditorWindows/SceneWindows.cpp b/src/EditorWindows/SceneWindows.cpp index 0c82c0b..fa2cabb 100644 --- a/src/EditorWindows/SceneWindows.cpp +++ b/src/EditorWindows/SceneWindows.cpp @@ -20,28 +20,80 @@ #pragma region Hierarchy Helpers namespace { - ImU32 GetHierarchyTypeColor(ObjectType type) { - switch (type) { - case ObjectType::Camera: return IM_COL32(110, 175, 235, 220); - case ObjectType::DirectionalLight: - case ObjectType::PointLight: - case ObjectType::SpotLight: - case ObjectType::AreaLight: return IM_COL32(255, 200, 90, 220); - case ObjectType::PostFXNode: return IM_COL32(200, 140, 230, 220); - case ObjectType::OBJMesh: - case ObjectType::Model: - case ObjectType::Sprite: return IM_COL32(120, 200, 150, 220); - case ObjectType::Mirror: return IM_COL32(180, 200, 210, 220); - case ObjectType::Plane: return IM_COL32(170, 180, 190, 220); - case ObjectType::Torus: return IM_COL32(155, 215, 180, 220); - case ObjectType::Canvas: return IM_COL32(120, 180, 255, 220); - case ObjectType::UIImage: - case ObjectType::UISlider: - case ObjectType::UIButton: - case ObjectType::UIText: - case ObjectType::Sprite2D: return IM_COL32(160, 210, 255, 220); - default: return IM_COL32(140, 190, 235, 220); + ImU32 GetHierarchyTypeColor(const SceneObject& obj) { + if (obj.hasCamera) return IM_COL32(110, 175, 235, 220); + if (obj.hasLight) return IM_COL32(255, 200, 90, 220); + if (obj.hasPostFX) return IM_COL32(200, 140, 230, 220); + if (obj.hasUI) return IM_COL32(160, 210, 255, 220); + if (obj.hasRenderer) { + switch (obj.renderType) { + case RenderType::OBJMesh: + case RenderType::Model: + case RenderType::Sprite: + return IM_COL32(120, 200, 150, 220); + case RenderType::Mirror: + return IM_COL32(180, 200, 210, 220); + case RenderType::Plane: + return IM_COL32(170, 180, 190, 220); + case RenderType::Torus: + return IM_COL32(155, 215, 180, 220); + case RenderType::Cube: + case RenderType::Sphere: + case RenderType::Capsule: + case RenderType::None: + default: + break; + } } + return IM_COL32(130, 150, 170, 220); + } + + void UpdateLegacyTypeFromComponents(SceneObject& target) { + if (target.hasRenderer) { + switch (target.renderType) { + case RenderType::Cube: target.type = ObjectType::Cube; break; + case RenderType::Sphere: target.type = ObjectType::Sphere; break; + case RenderType::Capsule: target.type = ObjectType::Capsule; break; + case RenderType::OBJMesh: target.type = ObjectType::OBJMesh; break; + case RenderType::Model: target.type = ObjectType::Model; break; + case RenderType::Mirror: target.type = ObjectType::Mirror; break; + case RenderType::Plane: target.type = ObjectType::Plane; break; + case RenderType::Torus: target.type = ObjectType::Torus; break; + case RenderType::Sprite: target.type = ObjectType::Sprite; break; + case RenderType::None: break; + } + return; + } + if (target.hasUI) { + switch (target.ui.type) { + case UIElementType::Canvas: target.type = ObjectType::Canvas; break; + case UIElementType::Image: target.type = ObjectType::UIImage; break; + case UIElementType::Slider: target.type = ObjectType::UISlider; break; + case UIElementType::Button: target.type = ObjectType::UIButton; break; + case UIElementType::Text: target.type = ObjectType::UIText; break; + case UIElementType::Sprite2D: target.type = ObjectType::Sprite2D; break; + case UIElementType::None: break; + } + return; + } + if (target.hasLight) { + switch (target.light.type) { + case LightType::Directional: target.type = ObjectType::DirectionalLight; break; + case LightType::Point: target.type = ObjectType::PointLight; break; + case LightType::Spot: target.type = ObjectType::SpotLight; break; + case LightType::Area: target.type = ObjectType::AreaLight; break; + } + return; + } + if (target.hasCamera) { + target.type = ObjectType::Camera; + return; + } + if (target.hasPostFX) { + target.type = ObjectType::PostFXNode; + return; + } + target.type = ObjectType::Empty; } void DrawFileOutlineIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) { @@ -101,6 +153,79 @@ namespace { drawList->AddLine(ImVec2(connectorX, rowTop), ImVec2(connectorX, vertEnd), lineColor, 1.0f); drawList->AddLine(ImVec2(connectorX, rowMid), ImVec2(itemMin.x + 6.0f, rowMid), lineColor, 1.0f); } + + void ApplyReverbPreset(ReverbZoneComponent& zone, ReverbPreset preset) { + zone.preset = preset; + switch (preset) { + case ReverbPreset::Room: + zone.room = -1000.0f; + zone.roomHF = -500.0f; + zone.roomLF = 0.0f; + zone.decayTime = 1.2f; + zone.decayHFRatio = 0.8f; + zone.reflections = -2600.0f; + zone.reflectionsDelay = 0.01f; + zone.reverb = 100.0f; + zone.reverbDelay = 0.012f; + zone.hfReference = 5000.0f; + zone.lfReference = 250.0f; + zone.roomRolloffFactor = 0.0f; + zone.diffusion = 85.0f; + zone.density = 90.0f; + break; + case ReverbPreset::LivingRoom: + zone.room = -1200.0f; + zone.roomHF = -800.0f; + zone.roomLF = 0.0f; + zone.decayTime = 1.5f; + zone.decayHFRatio = 0.7f; + zone.reflections = -2400.0f; + zone.reflectionsDelay = 0.02f; + zone.reverb = 150.0f; + zone.reverbDelay = 0.015f; + zone.hfReference = 5000.0f; + zone.lfReference = 250.0f; + zone.roomRolloffFactor = 0.0f; + zone.diffusion = 90.0f; + zone.density = 95.0f; + break; + case ReverbPreset::Hall: + zone.room = -1000.0f; + zone.roomHF = -200.0f; + zone.roomLF = 0.0f; + zone.decayTime = 3.2f; + zone.decayHFRatio = 0.7f; + zone.reflections = -1500.0f; + zone.reflectionsDelay = 0.03f; + zone.reverb = 500.0f; + zone.reverbDelay = 0.02f; + zone.hfReference = 5000.0f; + zone.lfReference = 250.0f; + zone.roomRolloffFactor = 0.0f; + zone.diffusion = 95.0f; + zone.density = 100.0f; + break; + case ReverbPreset::Forest: + zone.room = -1500.0f; + zone.roomHF = -1800.0f; + zone.roomLF = 0.0f; + zone.decayTime = 1.1f; + zone.decayHFRatio = 0.3f; + zone.reflections = -3000.0f; + zone.reflectionsDelay = 0.02f; + zone.reverb = -100.0f; + zone.reverbDelay = 0.01f; + zone.hfReference = 2500.0f; + zone.lfReference = 150.0f; + zone.roomRolloffFactor = 0.0f; + zone.diffusion = 50.0f; + zone.density = 60.0f; + break; + case ReverbPreset::Custom: + default: + break; + } + } } #pragma endregion @@ -185,8 +310,14 @@ void Engine::renderHierarchyPanel() { std::vector rootIndices; rootIndices.reserve(sceneObjects.size()); + std::unordered_set knownIds; + knownIds.reserve(sceneObjects.size()); + for (const auto& obj : sceneObjects) { + knownIds.insert(obj.id); + } for (size_t i = 0; i < sceneObjects.size(); i++) { - if (sceneObjects[i].parentId == -1) { + int parentId = sceneObjects[i].parentId; + if (parentId == -1 || knownIds.find(parentId) == knownIds.end()) { rootIndices.push_back(i); } } @@ -204,7 +335,7 @@ void Engine::renderHierarchyPanel() { auto createUIWithCanvas = [&](ObjectType type, const std::string& baseName) { int canvasId = -1; for (const auto& obj : sceneObjects) { - if (obj.type == ObjectType::Canvas) { + if (obj.hasUI && obj.ui.type == UIElementType::Canvas) { canvasId = obj.id; break; } @@ -220,8 +351,17 @@ void Engine::renderHierarchyPanel() { setParent(sceneObjects.back().id, canvasId); } }; + auto createReverbZoneObject = [&]() { + addObject(ObjectType::Empty, "Reverb Zone"); + if (!sceneObjects.empty()) { + sceneObjects.back().hasReverbZone = true; + sceneObjects.back().reverbZone = ReverbZoneComponent{}; + sceneObjects.back().reverbZone.boxSize = glm::max(sceneObjects.back().scale, glm::vec3(1.0f)); + } + }; if (ImGui::BeginMenu("Create")) { + if (ImGui::MenuItem("Empty")) addObject(ObjectType::Empty, "Empty"); // ── Primitives ───────────────────────────── if (ImGui::BeginMenu("Primitives")) { @@ -261,6 +401,7 @@ void Engine::renderHierarchyPanel() { if (ImGui::BeginMenu("Effects")) { if (ImGui::MenuItem("Post FX Node")) addObject(ObjectType::PostFXNode, "Post FX"); + if (ImGui::MenuItem("Audio Reverb Zone")) createReverbZoneObject(); ImGui::EndMenu(); } if (ImGui::BeginMenu("2D/UI")) @@ -314,7 +455,7 @@ void Engine::renderObjectNode(SceneObject& obj, const std::string& filter, float iconSize = std::max(8.0f, lineHeight - 6.0f); float labelStart = itemMin.x + ImGui::GetTreeNodeToLabelSpacing(); ImVec2 iconPos(labelStart, itemMin.y + (lineHeight - iconSize) * 0.5f); - ImU32 iconColor = GetHierarchyTypeColor(obj.type); + ImU32 iconColor = GetHierarchyTypeColor(obj); if (obj.parentId == -1) { DrawCubeOutlineIcon(ImGui::GetWindowDrawList(), iconPos, iconSize, iconColor); } else { @@ -390,7 +531,7 @@ void Engine::renderObjectNode(SceneObject& obj, const std::string& filter, deleteSelected(); } ImGui::Separator(); - if (obj.type == ObjectType::Canvas && ImGui::BeginMenu("Create UI Child")) { + if (obj.hasUI && obj.ui.type == UIElementType::Canvas && ImGui::BeginMenu("Create UI Child")) { auto createChild = [&](ObjectType type, const std::string& baseName) { addObject(type, baseName); if (!sceneObjects.empty()) { @@ -964,13 +1105,8 @@ void Engine::renderInspectorPanel() { SceneObject& obj = *it; ImGui::PushID(obj.id); // Scope per-object widgets to avoid ID collisions - auto isUIObjectType = [](ObjectType type) { - return type == ObjectType::Canvas || - type == ObjectType::UIImage || - type == ObjectType::UISlider || - type == ObjectType::UIButton || - type == ObjectType::UIText || - type == ObjectType::Sprite2D; + auto isUIObject = [](const SceneObject& target) { + return target.hasUI && target.ui.type != UIElementType::None; }; if (selectedObjectIds.size() > 1) { @@ -995,29 +1131,41 @@ void Engine::renderInspectorPanel() { ImGui::Text("Type:"); ImGui::SameLine(); - const char* typeLabel = "Unknown"; - switch (obj.type) { - case ObjectType::Cube: typeLabel = "Cube"; break; - case ObjectType::Sphere: typeLabel = "Sphere"; break; - case ObjectType::Capsule: typeLabel = "Capsule"; break; - case ObjectType::OBJMesh: typeLabel = "OBJ Mesh"; break; - case ObjectType::Model: typeLabel = "Model"; break; - case ObjectType::Sprite: typeLabel = "Sprite"; break; - case ObjectType::Sprite2D: typeLabel = "Sprite2D"; break; - case ObjectType::Canvas: typeLabel = "Canvas"; break; - case ObjectType::UIImage: typeLabel = "UI Image"; break; - case ObjectType::UISlider: typeLabel = "UI Slider"; break; - case ObjectType::UIButton: typeLabel = "UI Button"; break; - case ObjectType::UIText: typeLabel = "UI Text"; break; - case ObjectType::Camera: typeLabel = "Camera"; break; - case ObjectType::DirectionalLight: typeLabel = "Directional Light"; break; - case ObjectType::PointLight: typeLabel = "Point Light"; break; - case ObjectType::SpotLight: typeLabel = "Spot Light"; break; - case ObjectType::AreaLight: typeLabel = "Area Light"; break; - case ObjectType::PostFXNode: typeLabel = "Post FX Node"; break; - case ObjectType::Mirror: typeLabel = "Mirror"; break; - case ObjectType::Plane: typeLabel = "Plane"; break; - case ObjectType::Torus: typeLabel = "Torus"; break; + const char* typeLabel = "Empty"; + if (obj.hasRenderer) { + switch (obj.renderType) { + case RenderType::Cube: typeLabel = "Cube"; break; + case RenderType::Sphere: typeLabel = "Sphere"; break; + case RenderType::Capsule: typeLabel = "Capsule"; break; + case RenderType::OBJMesh: typeLabel = "OBJ Mesh"; break; + case RenderType::Model: typeLabel = "Model"; break; + case RenderType::Sprite: typeLabel = "Sprite"; break; + case RenderType::Mirror: typeLabel = "Mirror"; break; + case RenderType::Plane: typeLabel = "Plane"; break; + case RenderType::Torus: typeLabel = "Torus"; break; + case RenderType::None: break; + } + } else if (obj.hasUI) { + switch (obj.ui.type) { + case UIElementType::Canvas: typeLabel = "Canvas"; break; + case UIElementType::Image: typeLabel = "UI Image"; break; + case UIElementType::Slider: typeLabel = "UI Slider"; break; + case UIElementType::Button: typeLabel = "UI Button"; break; + case UIElementType::Text: typeLabel = "UI Text"; break; + case UIElementType::Sprite2D: typeLabel = "Sprite2D"; break; + case UIElementType::None: break; + } + } else if (obj.hasLight) { + switch (obj.light.type) { + case LightType::Directional: typeLabel = "Directional Light"; break; + case LightType::Point: typeLabel = "Point Light"; break; + case LightType::Spot: typeLabel = "Spot Light"; break; + case LightType::Area: typeLabel = "Area Light"; break; + } + } else if (obj.hasCamera) { + typeLabel = "Camera"; + } else if (obj.hasPostFX) { + typeLabel = "Post FX Node"; } ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "%s", typeLabel); @@ -1049,22 +1197,12 @@ void Engine::renderInspectorPanel() { obj.tag = tagBuf; projectManager.currentProject.hasUnsavedChanges = true; } - } - ImGui::PopStyleColor(); - - ImGui::Spacing(); - - ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.4f, 0.5f, 0.3f, 1.0f)); - - if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::PushID("Transform"); - ImGui::Indent(10.0f); - - if (obj.type == ObjectType::PostFXNode) { + ImGui::Spacing(); + if (obj.hasPostFX) { ImGui::TextDisabled("Transform is ignored for post-processing nodes."); } - if (isUIObjectType(obj.type)) { + if (isUIObject(obj)) { ImGui::TextDisabled("UI objects use the UI section for positioning."); } @@ -1076,8 +1214,6 @@ void Engine::renderInspectorPanel() { } ImGui::PopItemWidth(); - ImGui::Spacing(); - ImGui::Text("Rotation"); ImGui::PushItemWidth(-1); if (ImGui::DragFloat3("##Rotation", &obj.rotation.x, 1.0f, -360.0f, 360.0f)) { @@ -1087,8 +1223,6 @@ void Engine::renderInspectorPanel() { } ImGui::PopItemWidth(); - ImGui::Spacing(); - ImGui::Text("Scale"); ImGui::PushItemWidth(-1); if (ImGui::DragFloat3("##Scale", &obj.scale.x, 0.05f, 0.01f, 100.0f)) { @@ -1097,8 +1231,6 @@ void Engine::renderInspectorPanel() { } ImGui::PopItemWidth(); - ImGui::Spacing(); - if (ImGui::Button("Reset Transform", ImVec2(-1, 0))) { obj.position = glm::vec3(0.0f); obj.rotation = glm::vec3(0.0f); @@ -1106,17 +1238,23 @@ void Engine::renderInspectorPanel() { syncLocalTransform(obj); projectManager.currentProject.hasUnsavedChanges = true; } - - ImGui::Unindent(10.0f); - ImGui::PopID(); } ImGui::PopStyleColor(); - if (isUIObjectType(obj.type)) { + ImGui::Spacing(); + + if (isUIObject(obj)) { ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.25f, 0.45f, 0.65f, 1.0f)); - if (ImGui::CollapsingHeader("UI", ImGuiTreeNodeFlags_DefaultOpen)) { + bool changed = false; + bool removeUi = false; + auto header = drawComponentHeader("UI", "UI", nullptr, true, [&]() { + if (ImGui::MenuItem("Remove")) { + removeUi = true; + } + }); + if (header.open) { ImGui::PushID("UI"); ImGui::Indent(10.0f); @@ -1124,23 +1262,30 @@ void Engine::renderInspectorPanel() { int anchor = static_cast(obj.ui.anchor); if (ImGui::Combo("Anchor", &anchor, anchors, IM_ARRAYSIZE(anchors))) { obj.ui.anchor = static_cast(anchor); - projectManager.currentProject.hasUnsavedChanges = true; + changed = true; } if (ImGui::DragFloat2("Position (px)", &obj.ui.position.x, 1.0f)) { - projectManager.currentProject.hasUnsavedChanges = true; + changed = true; + } + + if (ImGui::DragFloat("Rotation (deg)", &obj.ui.rotation, 0.5f, -360.0f, 360.0f)) { + glm::vec3 rot(0.0f, 0.0f, obj.ui.rotation); + rot = NormalizeEulerDegrees(rot); + obj.ui.rotation = rot.z; + changed = true; } glm::vec2 minSize(8.0f, 8.0f); if (ImGui::DragFloat2("Size (px)", &obj.ui.size.x, 1.0f, minSize.x, 4096.0f)) { obj.ui.size.x = std::max(minSize.x, obj.ui.size.x); obj.ui.size.y = std::max(minSize.y, obj.ui.size.y); - projectManager.currentProject.hasUnsavedChanges = true; + changed = true; } - if (obj.type == ObjectType::UIButton || obj.type == ObjectType::UISlider) { + if (obj.ui.type == UIElementType::Button || obj.ui.type == UIElementType::Slider) { if (ImGui::Checkbox("Interactable", &obj.ui.interactable)) { - projectManager.currentProject.hasUnsavedChanges = true; + changed = true; } const auto& presets = getUIStylePresets(); @@ -1153,7 +1298,7 @@ void Engine::renderInspectorPanel() { bool selected = (i == presetIndex); if (ImGui::Selectable(presets[i].name.c_str(), selected)) { obj.ui.stylePreset = presets[i].name; - projectManager.currentProject.hasUnsavedChanges = true; + changed = true; } if (selected) ImGui::SetItemDefaultFocus(); } @@ -1162,34 +1307,36 @@ void Engine::renderInspectorPanel() { } } - if (obj.type == ObjectType::UIButton || obj.type == ObjectType::UISlider || obj.type == ObjectType::UIImage || obj.type == ObjectType::UIText || obj.type == ObjectType::Sprite2D) { + if (obj.ui.type == UIElementType::Button || obj.ui.type == UIElementType::Slider || + obj.ui.type == UIElementType::Image || obj.ui.type == UIElementType::Text || + obj.ui.type == UIElementType::Sprite2D) { char labelBuf[128] = {}; std::snprintf(labelBuf, sizeof(labelBuf), "%s", obj.ui.label.c_str()); - if (ImGui::InputText(obj.type == ObjectType::UIText ? "Text" : "Label", labelBuf, sizeof(labelBuf))) { + if (ImGui::InputText(obj.ui.type == UIElementType::Text ? "Text" : "Label", labelBuf, sizeof(labelBuf))) { obj.ui.label = labelBuf; - projectManager.currentProject.hasUnsavedChanges = true; + changed = true; } } - if (obj.type == ObjectType::UIText) { + if (obj.ui.type == UIElementType::Text) { if (ImGui::DragFloat("Text Size", &obj.ui.textScale, 0.05f, 0.1f, 10.0f, "%.2f")) { obj.ui.textScale = std::max(0.1f, obj.ui.textScale); - projectManager.currentProject.hasUnsavedChanges = true; + changed = true; } } - if (obj.type == ObjectType::UIImage || obj.type == ObjectType::Sprite2D) { + if (obj.ui.type == UIElementType::Image || obj.ui.type == UIElementType::Sprite2D) { ImGui::TextUnformatted("Texture"); ImGui::SetNextItemWidth(-160); char texBuf[512] = {}; std::snprintf(texBuf, sizeof(texBuf), "%s", obj.albedoTexturePath.c_str()); if (ImGui::InputText("##UITexture", texBuf, sizeof(texBuf))) { obj.albedoTexturePath = texBuf; - projectManager.currentProject.hasUnsavedChanges = true; + changed = true; } ImGui::SameLine(); if (ImGui::SmallButton("Clear##UITexture")) { obj.albedoTexturePath.clear(); - projectManager.currentProject.hasUnsavedChanges = true; + changed = true; } ImGui::SameLine(); bool canUseTex = !fileBrowser.selectedFile.empty() && fs::exists(fileBrowser.selectedFile) && @@ -1197,44 +1344,44 @@ void Engine::renderInspectorPanel() { ImGui::BeginDisabled(!canUseTex); if (ImGui::SmallButton("Use Selection##UITexture")) { obj.albedoTexturePath = fileBrowser.selectedFile.string(); - projectManager.currentProject.hasUnsavedChanges = true; + changed = true; } ImGui::EndDisabled(); } - if (obj.type == ObjectType::UISlider) { + if (obj.ui.type == UIElementType::Slider) { const char* sliderStyles[] = { "ImGui", "Fill", "Circle" }; int sliderStyle = static_cast(obj.ui.sliderStyle); if (ImGui::Combo("Style", &sliderStyle, sliderStyles, IM_ARRAYSIZE(sliderStyles))) { obj.ui.sliderStyle = static_cast(sliderStyle); - projectManager.currentProject.hasUnsavedChanges = true; + changed = true; } if (ImGui::DragFloat("Min", &obj.ui.sliderMin, 0.1f)) { - projectManager.currentProject.hasUnsavedChanges = true; + changed = true; } if (ImGui::DragFloat("Max", &obj.ui.sliderMax, 0.1f)) { - projectManager.currentProject.hasUnsavedChanges = true; + changed = true; } if (obj.ui.sliderMax < obj.ui.sliderMin) { std::swap(obj.ui.sliderMin, obj.ui.sliderMax); } if (ImGui::SliderFloat("Value", &obj.ui.sliderValue, obj.ui.sliderMin, obj.ui.sliderMax)) { - projectManager.currentProject.hasUnsavedChanges = true; + changed = true; } } ImVec4 uiColor(obj.ui.color.r, obj.ui.color.g, obj.ui.color.b, obj.ui.color.a); if (ImGui::ColorEdit4("Tint", &uiColor.x)) { obj.ui.color = glm::vec4(uiColor.x, uiColor.y, uiColor.z, uiColor.w); - projectManager.currentProject.hasUnsavedChanges = true; + changed = true; } - if (obj.type == ObjectType::UIButton) { + if (obj.ui.type == UIElementType::Button) { const char* buttonStyles[] = { "ImGui", "Outline" }; int buttonStyle = static_cast(obj.ui.buttonStyle); if (ImGui::Combo("Style", &buttonStyle, buttonStyles, IM_ARRAYSIZE(buttonStyles))) { obj.ui.buttonStyle = static_cast(buttonStyle); - projectManager.currentProject.hasUnsavedChanges = true; + changed = true; } ImGui::TextDisabled("Last Pressed: %s", obj.ui.buttonPressed ? "yes" : "no"); } @@ -1242,6 +1389,15 @@ void Engine::renderInspectorPanel() { ImGui::Unindent(10.0f); ImGui::PopID(); } + if (removeUi) { + obj.hasUI = false; + obj.ui.type = UIElementType::None; + UpdateLegacyTypeFromComponents(obj); + changed = true; + } + if (changed) { + projectManager.currentProject.hasUnsavedChanges = true; + } ImGui::PopStyleColor(); } @@ -1389,7 +1545,7 @@ void Engine::renderInspectorPanel() { ImGui::PushID("Rigidbody3D"); ImGui::Indent(10.0f); ImGui::TextDisabled("Collider required for physics."); - if (isUIObjectType(obj.type)) { + if (isUIObject(obj)) { ImGui::TextDisabled("Rigidbody3D is for 3D objects (use Rigidbody2D for UI/canvas)."); } @@ -1450,7 +1606,7 @@ void Engine::renderInspectorPanel() { if (header.open) { ImGui::PushID("Rigidbody2D"); ImGui::Indent(10.0f); - if (!isUIObjectType(obj.type)) { + if (!isUIObject(obj)) { ImGui::TextDisabled("Rigidbody2D is for UI/canvas objects only."); } if (ImGui::Checkbox("Use Gravity", &obj.rigidbody2D.useGravity)) { @@ -1480,6 +1636,176 @@ void Engine::renderInspectorPanel() { ImGui::PopStyleColor(); } + if (obj.hasCollider2D) { + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.5f, 0.65f, 1.0f)); + bool removeCollider2D = false; + bool changed = false; + auto header = drawComponentHeader("Collider2D", "Collider2D", &obj.collider2D.enabled, true, [&]() { + if (ImGui::MenuItem("Remove")) { + removeCollider2D = true; + } + }); + if (header.enabledChanged) { + changed = true; + } + if (header.open) { + ImGui::PushID("Collider2D"); + ImGui::Indent(10.0f); + if (!isUIObject(obj)) { + ImGui::TextDisabled("Collider2D is for UI/canvas objects only."); + } + const char* colliderTypes[] = { "Box", "Polygon", "Edge" }; + int colliderType = static_cast(obj.collider2D.type); + if (ImGui::Combo("Type", &colliderType, colliderTypes, IM_ARRAYSIZE(colliderTypes))) { + obj.collider2D.type = static_cast(colliderType); + if (obj.collider2D.type == Collider2DType::Polygon) { + obj.collider2D.closed = true; + } else if (obj.collider2D.type == Collider2DType::Edge) { + obj.collider2D.closed = false; + } + changed = true; + } + + auto ensureHexagon = [&](Collider2DComponent& col, const glm::vec2& size) { + if (!col.points.empty()) return; + float radius = 0.5f * std::min(size.x, size.y); + col.points.clear(); + for (int i = 0; i < 6; ++i) { + float ang = static_cast(i) * (2.0f * PI / 6.0f); + col.points.emplace_back(std::cos(ang) * radius, std::sin(ang) * radius); + } + }; + auto ensureEdge = [&](Collider2DComponent& col, const glm::vec2& size) { + if (col.points.size() >= 2) return; + float half = size.x * 0.5f; + col.points = { glm::vec2(-half, 0.0f), glm::vec2(half, 0.0f) }; + }; + + if (obj.collider2D.type == Collider2DType::Box) { + if (ImGui::DragFloat2("Box Size", &obj.collider2D.boxSize.x, 0.1f, 0.01f, 10000.0f, "%.2f")) { + obj.collider2D.boxSize.x = std::max(0.01f, obj.collider2D.boxSize.x); + obj.collider2D.boxSize.y = std::max(0.01f, obj.collider2D.boxSize.y); + changed = true; + } + if (ImGui::SmallButton("Match UI Size")) { + obj.collider2D.boxSize = glm::max(obj.ui.size, glm::vec2(1.0f)); + changed = true; + } + } else if (obj.collider2D.type == Collider2DType::Polygon) { + ensureHexagon(obj.collider2D, glm::max(obj.ui.size, glm::vec2(1.0f))); + ImGui::TextDisabled("Points (local space)"); + for (size_t i = 0; i < obj.collider2D.points.size(); ++i) { + ImGui::PushID(static_cast(i)); + if (ImGui::DragFloat2("##point", &obj.collider2D.points[i].x, 0.1f)) { + changed = true; + } + ImGui::SameLine(); + if (ImGui::SmallButton("Remove")) { + obj.collider2D.points.erase(obj.collider2D.points.begin() + static_cast(i)); + changed = true; + ImGui::PopID(); + break; + } + ImGui::PopID(); + } + if (ImGui::SmallButton("Add Point")) { + obj.collider2D.points.push_back(glm::vec2(0.0f)); + changed = true; + } + } else if (obj.collider2D.type == Collider2DType::Edge) { + ensureEdge(obj.collider2D, glm::max(obj.ui.size, glm::vec2(1.0f))); + if (ImGui::Checkbox("Closed Loop", &obj.collider2D.closed)) { + changed = true; + } + if (ImGui::DragFloat("Thickness", &obj.collider2D.edgeThickness, 0.01f, 0.01f, 10.0f, "%.2f")) { + obj.collider2D.edgeThickness = std::max(0.01f, obj.collider2D.edgeThickness); + changed = true; + } + ImGui::TextDisabled("Points (local space)"); + for (size_t i = 0; i < obj.collider2D.points.size(); ++i) { + ImGui::PushID(static_cast(i)); + if (ImGui::DragFloat2("##edgepoint", &obj.collider2D.points[i].x, 0.1f)) { + changed = true; + } + ImGui::SameLine(); + if (ImGui::SmallButton("Remove")) { + obj.collider2D.points.erase(obj.collider2D.points.begin() + static_cast(i)); + changed = true; + ImGui::PopID(); + break; + } + ImGui::PopID(); + } + if (ImGui::SmallButton("Add Point")) { + obj.collider2D.points.push_back(glm::vec2(0.0f)); + changed = true; + } + } + + ImGui::Unindent(10.0f); + ImGui::PopID(); + } + if (removeCollider2D) { + obj.hasCollider2D = false; + changed = true; + } + if (changed) { + projectManager.currentProject.hasUnsavedChanges = true; + } + ImGui::PopStyleColor(); + } + + if (obj.hasParallaxLayer2D) { + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.28f, 0.45f, 0.6f, 1.0f)); + bool removeParallax = false; + bool changed = false; + auto header = drawComponentHeader("Parallax Layer 2D", "ParallaxLayer2D", &obj.parallaxLayer2D.enabled, true, [&]() { + if (ImGui::MenuItem("Remove")) { + removeParallax = true; + } + }); + if (header.enabledChanged) { + changed = true; + } + if (header.open) { + ImGui::PushID("ParallaxLayer2D"); + ImGui::Indent(10.0f); + if (!isUIObject(obj)) { + ImGui::TextDisabled("Parallax layers are for UI world objects."); + } + if (ImGui::DragInt("Order", &obj.parallaxLayer2D.order, 1.0f)) { + changed = true; + } + if (ImGui::DragFloat("Parallax Factor", &obj.parallaxLayer2D.factor, 0.01f, 0.0f, 1.0f, "%.2f")) { + obj.parallaxLayer2D.factor = std::clamp(obj.parallaxLayer2D.factor, 0.0f, 1.0f); + changed = true; + } + if (ImGui::Checkbox("Repeat X", &obj.parallaxLayer2D.repeatX)) { + changed = true; + } + if (ImGui::Checkbox("Repeat Y", &obj.parallaxLayer2D.repeatY)) { + changed = true; + } + if (ImGui::DragFloat2("Repeat Spacing", &obj.parallaxLayer2D.repeatSpacing.x, 0.1f, 0.0f, 10000.0f, "%.1f")) { + obj.parallaxLayer2D.repeatSpacing.x = std::max(0.0f, obj.parallaxLayer2D.repeatSpacing.x); + obj.parallaxLayer2D.repeatSpacing.y = std::max(0.0f, obj.parallaxLayer2D.repeatSpacing.y); + changed = true; + } + ImGui::Unindent(10.0f); + ImGui::PopID(); + } + if (removeParallax) { + obj.hasParallaxLayer2D = false; + changed = true; + } + if (changed) { + projectManager.currentProject.hasUnsavedChanges = true; + } + ImGui::PopStyleColor(); + } + if (obj.hasAudioSource) { ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.55f, 0.4f, 0.3f, 1.0f)); @@ -1556,6 +1882,31 @@ void Engine::renderInspectorPanel() { src.maxDistance = std::max(src.maxDistance, src.minDistance + 0.5f); changed = true; } + const char* rolloffModes[] = { "Logarithmic", "Linear", "Exponential", "Custom" }; + int rolloffIndex = static_cast(src.rolloffMode); + if (ImGui::Combo("Rolloff Mode", &rolloffIndex, rolloffModes, IM_ARRAYSIZE(rolloffModes))) { + src.rolloffMode = static_cast(rolloffIndex); + changed = true; + } + if (src.rolloffMode != AudioRolloffMode::Custom) { + if (ImGui::SliderFloat("Rolloff Factor", &src.rolloff, 0.1f, 4.0f, "%.2f")) { + src.rolloff = std::max(0.1f, src.rolloff); + changed = true; + } + } else { + if (ImGui::SliderFloat("Mid Distance", &src.customMidDistance, 0.0f, 1.0f, "%.2f")) { + src.customMidDistance = std::clamp(src.customMidDistance, 0.0f, 1.0f); + changed = true; + } + if (ImGui::SliderFloat("Mid Gain", &src.customMidGain, 0.0f, 1.0f, "%.2f")) { + src.customMidGain = std::clamp(src.customMidGain, 0.0f, 1.0f); + changed = true; + } + if (ImGui::SliderFloat("End Gain", &src.customEndGain, 0.0f, 1.0f, "%.2f")) { + src.customEndGain = std::clamp(src.customEndGain, 0.0f, 1.0f); + changed = true; + } + } ImGui::EndDisabled(); const AudioClipPreview* clipPreview = audio.getPreview(src.clipPath); @@ -1599,44 +1950,413 @@ void Engine::renderInspectorPanel() { ImGui::PopStyleColor(); } - if (obj.type == ObjectType::Camera) { + if (obj.hasAnimation) { + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.4f, 0.35f, 0.55f, 1.0f)); + bool removeAnimation = false; + bool changed = false; + auto header = drawComponentHeader("Animation", "Animation", &obj.animation.enabled, true, [&]() { + if (ImGui::MenuItem("Open Animator")) { + showAnimationWindow = true; + animationTargetId = obj.id; + } + if (ImGui::MenuItem("Remove")) { + removeAnimation = true; + } + }); + if (header.enabledChanged) { + changed = true; + } + if (header.open) { + ImGui::PushID("Animation"); + ImGui::Indent(10.0f); + if (ImGui::Button("Open Animator")) { + showAnimationWindow = true; + animationTargetId = obj.id; + } + ImGui::SameLine(); + ImGui::TextDisabled("Keyframes: %zu", obj.animation.keyframes.size()); + + if (ImGui::DragFloat("Clip Length", &obj.animation.clipLength, 0.05f, 0.1f, 120.0f, "%.2f")) { + obj.animation.clipLength = std::max(0.1f, obj.animation.clipLength); + changed = true; + } + if (ImGui::DragFloat("Play Speed", &obj.animation.playSpeed, 0.05f, 0.05f, 8.0f, "%.2f")) { + obj.animation.playSpeed = std::max(0.05f, obj.animation.playSpeed); + changed = true; + } + if (ImGui::Checkbox("Loop", &obj.animation.loop)) { + changed = true; + } + if (ImGui::Checkbox("Apply On Scrub", &obj.animation.applyOnScrub)) { + changed = true; + } + + if (ImGui::Button("Clear Keyframes")) { + obj.animation.keyframes.clear(); + changed = true; + } + + ImGui::Unindent(10.0f); + ImGui::PopID(); + } + if (removeAnimation) { + obj.hasAnimation = false; + obj.animation = AnimationComponent{}; + changed = true; + } + if (changed) { + projectManager.currentProject.hasUnsavedChanges = true; + } + ImGui::PopStyleColor(); + } + + if (obj.hasSkeletalAnimation) { + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.4f, 0.6f, 1.0f)); + bool removeSkeletal = false; + bool changed = false; + auto header = drawComponentHeader("Skeletal", "Skeletal", &obj.skeletal.enabled, true, [&]() { + if (ImGui::MenuItem("Remove")) { + removeSkeletal = true; + } + }); + if (header.enabledChanged) { + changed = true; + } + if (header.open) { + ImGui::PushID("Skeletal"); + ImGui::Indent(10.0f); + + ModelSceneData sceneData; + std::string err; + bool hasClips = !obj.meshPath.empty() && getModelLoader().loadModelScene(obj.meshPath, sceneData, err) && + !sceneData.animations.empty(); + if (hasClips) { + std::vector clipNames; + clipNames.reserve(sceneData.animations.size()); + for (const auto& clip : sceneData.animations) { + clipNames.push_back(clip.name.c_str()); + } + int clipIndex = std::clamp(obj.skeletal.clipIndex, 0, (int)clipNames.size() - 1); + if (ImGui::Combo("Clip", &clipIndex, clipNames.data(), (int)clipNames.size())) { + obj.skeletal.clipIndex = clipIndex; + obj.skeletal.time = 0.0f; + changed = true; + } + } else { + ImGui::TextDisabled("No animation clips found"); + } + + if (ImGui::Checkbox("Use Animation", &obj.skeletal.useAnimation)) { + changed = true; + } + if (ImGui::DragFloat("Play Speed", &obj.skeletal.playSpeed, 0.05f, 0.05f, 8.0f, "%.2f")) { + obj.skeletal.playSpeed = std::max(0.05f, obj.skeletal.playSpeed); + changed = true; + } + if (ImGui::Checkbox("Loop", &obj.skeletal.loop)) { + changed = true; + } + if (ImGui::Checkbox("GPU Skinning", &obj.skeletal.useGpuSkinning)) { + changed = true; + } + if (ImGui::Checkbox("Allow CPU Fallback", &obj.skeletal.allowCpuFallback)) { + changed = true; + } + if (ImGui::DragInt("Max Bones", &obj.skeletal.maxBones, 1, 8, 256)) { + obj.skeletal.maxBones = std::clamp(obj.skeletal.maxBones, 8, 256); + changed = true; + } + + ImGui::Unindent(10.0f); + ImGui::PopID(); + } + if (removeSkeletal) { + obj.hasSkeletalAnimation = false; + obj.skeletal = SkeletalAnimationComponent{}; + changed = true; + } + if (changed) { + projectManager.currentProject.hasUnsavedChanges = true; + } + ImGui::PopStyleColor(); + } + + if (obj.hasReverbZone) { + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.4f, 0.45f, 0.6f, 1.0f)); + bool removeReverbZone = false; + bool changed = false; + auto header = drawComponentHeader("Reverb Zone", "ReverbZone", &obj.reverbZone.enabled, true, [&]() { + if (ImGui::MenuItem("Remove")) { + removeReverbZone = true; + } + }); + if (header.enabledChanged) { + changed = true; + } + if (header.open) { + ImGui::PushID("ReverbZone"); + ImGui::Indent(10.0f); + auto& zone = obj.reverbZone; + + const char* presets[] = { "Room", "Living Room", "Hall", "Forest", "Custom" }; + int presetIndex = static_cast(zone.preset); + if (ImGui::Combo("Preset", &presetIndex, presets, IM_ARRAYSIZE(presets))) { + ApplyReverbPreset(zone, static_cast(presetIndex)); + changed = true; + } + + const char* shapes[] = { "Box", "Sphere" }; + int shapeIndex = static_cast(zone.shape); + if (ImGui::Combo("Shape", &shapeIndex, shapes, IM_ARRAYSIZE(shapes))) { + zone.shape = static_cast(shapeIndex); + changed = true; + } + + if (zone.shape == ReverbZoneShape::Sphere) { + if (ImGui::DragFloat("Radius", &zone.radius, 0.1f, 0.1f, 500.0f, "%.2f")) { + zone.radius = std::max(0.1f, zone.radius); + changed = true; + } + } else { + if (ImGui::DragFloat3("Box Size", &zone.boxSize.x, 0.1f, 0.1f, 500.0f, "%.2f")) { + zone.boxSize = glm::max(zone.boxSize, glm::vec3(0.1f)); + changed = true; + } + } + + if (zone.shape == ReverbZoneShape::Sphere) { + if (ImGui::DragFloat("Min Distance", &zone.minDistance, 0.05f, 0.0f, 500.0f, "%.2f")) { + zone.minDistance = std::max(0.0f, zone.minDistance); + changed = true; + } + if (ImGui::DragFloat("Max Distance", &zone.maxDistance, 0.05f, zone.minDistance + 0.1f, 1000.0f, "%.2f")) { + zone.maxDistance = std::max(zone.maxDistance, zone.minDistance + 0.1f); + changed = true; + } + } else if (ImGui::DragFloat("Blend Distance", &zone.blendDistance, 0.05f, 0.0f, 50.0f, "%.2f")) { + zone.blendDistance = std::max(0.0f, zone.blendDistance); + changed = true; + } + + if (ImGui::SliderFloat("Room", &zone.room, -10000.0f, 0.0f, "%.0f dB")) { + zone.preset = ReverbPreset::Custom; + changed = true; + } + if (ImGui::SliderFloat("Room HF", &zone.roomHF, -10000.0f, 0.0f, "%.0f dB")) { + zone.preset = ReverbPreset::Custom; + changed = true; + } + if (ImGui::SliderFloat("Room LF", &zone.roomLF, -10000.0f, 0.0f, "%.0f dB")) { + zone.preset = ReverbPreset::Custom; + changed = true; + } + if (ImGui::SliderFloat("Decay Time", &zone.decayTime, 0.1f, 20.0f, "%.2f s")) { + zone.preset = ReverbPreset::Custom; + changed = true; + } + if (ImGui::SliderFloat("Decay HF Ratio", &zone.decayHFRatio, 0.1f, 2.0f, "%.2f")) { + zone.preset = ReverbPreset::Custom; + changed = true; + } + if (ImGui::SliderFloat("Reflections", &zone.reflections, -10000.0f, 1000.0f, "%.0f dB")) { + zone.preset = ReverbPreset::Custom; + changed = true; + } + if (ImGui::SliderFloat("Reflections Delay", &zone.reflectionsDelay, 0.0f, 0.1f, "%.3f s")) { + zone.preset = ReverbPreset::Custom; + changed = true; + } + if (ImGui::SliderFloat("Reverb", &zone.reverb, -10000.0f, 2000.0f, "%.0f dB")) { + zone.preset = ReverbPreset::Custom; + changed = true; + } + if (ImGui::SliderFloat("Reverb Delay", &zone.reverbDelay, 0.0f, 0.1f, "%.3f s")) { + zone.preset = ReverbPreset::Custom; + changed = true; + } + if (ImGui::SliderFloat("HF Reference", &zone.hfReference, 1000.0f, 20000.0f, "%.0f Hz")) { + zone.preset = ReverbPreset::Custom; + changed = true; + } + if (ImGui::SliderFloat("LF Reference", &zone.lfReference, 20.0f, 1000.0f, "%.0f Hz")) { + zone.preset = ReverbPreset::Custom; + changed = true; + } + if (ImGui::SliderFloat("Room Rolloff Factor", &zone.roomRolloffFactor, 0.0f, 10.0f, "%.2f")) { + zone.preset = ReverbPreset::Custom; + changed = true; + } + if (ImGui::SliderFloat("Diffusion", &zone.diffusion, 0.0f, 100.0f, "%.0f")) { + zone.preset = ReverbPreset::Custom; + changed = true; + } + if (ImGui::SliderFloat("Density", &zone.density, 0.0f, 100.0f, "%.0f")) { + zone.preset = ReverbPreset::Custom; + changed = true; + } + + ImGui::Unindent(10.0f); + ImGui::PopID(); + } + if (removeReverbZone) { + obj.hasReverbZone = false; + changed = true; + } + if (changed) { + projectManager.currentProject.hasUnsavedChanges = true; + } + ImGui::PopStyleColor(); + } + + if (obj.hasCamera) { ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.45f, 0.35f, 0.65f, 1.0f)); - if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen)) { + bool changed = false; + bool removeCamera = false; + auto header = drawComponentHeader("Camera", "Camera", nullptr, true, [&]() { + if (ImGui::MenuItem("Remove")) { + removeCamera = true; + } + }); + if (header.open) { ImGui::PushID("Camera"); ImGui::Indent(10.0f); const char* cameraTypes[] = { "Scene", "Player" }; int camType = static_cast(obj.camera.type); if (ImGui::Combo("Type", &camType, cameraTypes, IM_ARRAYSIZE(cameraTypes))) { obj.camera.type = static_cast(camType); - projectManager.currentProject.hasUnsavedChanges = true; + changed = true; } if (ImGui::SliderFloat("FOV", &obj.camera.fov, 20.0f, 120.0f, "%.0f deg")) { - projectManager.currentProject.hasUnsavedChanges = true; + changed = true; } if (ImGui::DragFloat("Near Clip", &obj.camera.nearClip, 0.01f, 0.01f, obj.camera.farClip - 0.01f, "%.3f")) { obj.camera.nearClip = std::max(0.01f, std::min(obj.camera.nearClip, obj.camera.farClip - 0.01f)); - projectManager.currentProject.hasUnsavedChanges = true; + changed = true; } if (ImGui::DragFloat("Far Clip", &obj.camera.farClip, 0.1f, obj.camera.nearClip + 0.05f, 1000.0f, "%.1f")) { obj.camera.farClip = std::max(obj.camera.nearClip + 0.05f, obj.camera.farClip); - projectManager.currentProject.hasUnsavedChanges = true; + changed = true; } if (ImGui::Checkbox("Apply Post Processing", &obj.camera.applyPostFX)) { - projectManager.currentProject.hasUnsavedChanges = true; + changed = true; + } + if (ImGui::Checkbox("2D Camera", &obj.camera.use2D)) { + changed = true; + } + if (obj.camera.use2D) { + if (ImGui::DragFloat("Pixels Per Unit", &obj.camera.pixelsPerUnit, 1.0f, 1.0f, 2000.0f, "%.1f")) { + obj.camera.pixelsPerUnit = std::max(1.0f, obj.camera.pixelsPerUnit); + changed = true; + } + ImGui::TextDisabled("Uses X/Y for 2D view; Z stays fixed."); } ImGui::Unindent(10.0f); ImGui::PopID(); } + if (removeCamera) { + obj.hasCamera = false; + UpdateLegacyTypeFromComponents(obj); + changed = true; + } + if (changed) { + projectManager.currentProject.hasUnsavedChanges = true; + } ImGui::PopStyleColor(); } - if (obj.type == ObjectType::PostFXNode) { + if (obj.hasCameraFollow2D) { + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.55f, 0.4f, 1.0f)); + bool changed = false; + bool removeFollow = false; + auto header = drawComponentHeader("Camera Follow 2D", "CameraFollow2D", &obj.cameraFollow2D.enabled, true, [&]() { + if (ImGui::MenuItem("Remove")) { + removeFollow = true; + } + }); + if (header.enabledChanged) { + changed = true; + } + if (header.open) { + ImGui::PushID("CameraFollow2D"); + ImGui::Indent(10.0f); + if (!obj.hasCamera) { + ImGui::TextDisabled("Requires a Camera component."); + } + + std::string targetLabel = "None"; + if (obj.cameraFollow2D.targetId >= 0) { + auto it = std::find_if(sceneObjects.begin(), sceneObjects.end(), + [&](const SceneObject& o) { return o.id == obj.cameraFollow2D.targetId; }); + if (it != sceneObjects.end()) { + targetLabel = it->name + " (" + std::to_string(it->id) + ")"; + } + } + if (ImGui::BeginCombo("Target", targetLabel.c_str())) { + if (ImGui::Selectable("None", obj.cameraFollow2D.targetId < 0)) { + obj.cameraFollow2D.targetId = -1; + changed = true; + } + for (const auto& candidate : sceneObjects) { + if (candidate.id == obj.id) continue; + std::string label = candidate.name + " (" + std::to_string(candidate.id) + ")"; + bool selected = (candidate.id == obj.cameraFollow2D.targetId); + if (ImGui::Selectable(label.c_str(), selected)) { + obj.cameraFollow2D.targetId = candidate.id; + changed = true; + } + if (selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + if (ImGui::Button("Use Selected")) { + if (selectedObjectId >= 0 && selectedObjectId != obj.id) { + obj.cameraFollow2D.targetId = selectedObjectId; + changed = true; + } + } + ImGui::SameLine(); + if (ImGui::Button("Clear Target")) { + obj.cameraFollow2D.targetId = -1; + changed = true; + } + if (ImGui::DragFloat2("Offset", &obj.cameraFollow2D.offset.x, 0.1f)) { + changed = true; + } + if (ImGui::DragFloat("Smooth Time", &obj.cameraFollow2D.smoothTime, 0.01f, 0.0f, 10.0f, "%.2f s")) { + obj.cameraFollow2D.smoothTime = std::max(0.0f, obj.cameraFollow2D.smoothTime); + changed = true; + } + ImGui::Unindent(10.0f); + ImGui::PopID(); + } + if (removeFollow) { + obj.hasCameraFollow2D = false; + changed = true; + } + if (changed) { + projectManager.currentProject.hasUnsavedChanges = true; + } + ImGui::PopStyleColor(); + } + + if (obj.hasPostFX) { ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.25f, 0.55f, 0.6f, 1.0f)); bool changed = false; - auto header = drawComponentHeader("Post Processing", "PostFX", &obj.postFx.enabled, true, {}); + bool removePostFx = false; + auto header = drawComponentHeader("Post Processing", "PostFX", &obj.postFx.enabled, true, [&]() { + if (ImGui::MenuItem("Remove")) { + removePostFx = true; + } + }); if (header.enabledChanged) { changed = true; } @@ -1736,6 +2456,11 @@ void Engine::renderInspectorPanel() { ImGui::Unindent(10.0f); ImGui::PopID(); } + if (removePostFx) { + obj.hasPostFX = false; + UpdateLegacyTypeFromComponents(obj); + changed = true; + } if (changed) { projectManager.currentProject.hasUnsavedChanges = true; } @@ -1743,7 +2468,47 @@ void Engine::renderInspectorPanel() { } // Material section (skip for pure light objects) - if (obj.type != ObjectType::DirectionalLight && obj.type != ObjectType::PointLight && obj.type != ObjectType::SpotLight && obj.type != ObjectType::AreaLight && obj.type != ObjectType::Camera && obj.type != ObjectType::PostFXNode && obj.type != ObjectType::Canvas && obj.type != ObjectType::UIImage && obj.type != ObjectType::UISlider && obj.type != ObjectType::UIButton && obj.type != ObjectType::UIText && obj.type != ObjectType::Sprite2D) { + if (obj.hasRenderer) { + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.35f, 0.55f, 1.0f)); + bool rendererChanged = false; + bool removeRenderer = false; + auto rendererHeader = drawComponentHeader("Renderer", "Renderer", nullptr, true, [&]() { + if (ImGui::MenuItem("Remove")) { + removeRenderer = true; + } + }); + if (rendererHeader.open) { + ImGui::Indent(10.0f); + const char* renderLabel = "None"; + switch (obj.renderType) { + case RenderType::Cube: renderLabel = "Cube"; break; + case RenderType::Sphere: renderLabel = "Sphere"; break; + case RenderType::Capsule: renderLabel = "Capsule"; break; + case RenderType::OBJMesh: renderLabel = "OBJ Mesh"; break; + case RenderType::Model: renderLabel = "Model"; break; + case RenderType::Mirror: renderLabel = "Mirror"; break; + case RenderType::Plane: renderLabel = "Plane"; break; + case RenderType::Torus: renderLabel = "Torus"; break; + case RenderType::Sprite: renderLabel = "Sprite"; break; + case RenderType::None: break; + } + ImGui::Text("Render Type: %s", renderLabel); + ImGui::Unindent(10.0f); + } + if (removeRenderer) { + obj.hasRenderer = false; + obj.renderType = RenderType::None; + UpdateLegacyTypeFromComponents(obj); + rendererChanged = true; + } + if (rendererChanged) { + projectManager.currentProject.hasUnsavedChanges = true; + } + ImGui::PopStyleColor(); + } + + if (obj.hasRenderer) { ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.35f, 0.55f, 1.0f)); @@ -1959,11 +2724,16 @@ void Engine::renderInspectorPanel() { ImGui::PopStyleColor(); } - if (obj.type == ObjectType::DirectionalLight || obj.type == ObjectType::PointLight || obj.type == ObjectType::SpotLight || obj.type == ObjectType::AreaLight) { + if (obj.hasLight) { ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.5f, 0.45f, 0.2f, 1.0f)); bool changed = false; - auto header = drawComponentHeader("Light", "Light", &obj.light.enabled, true, {}); + bool removeLight = false; + auto header = drawComponentHeader("Light", "Light", &obj.light.enabled, true, [&]() { + if (ImGui::MenuItem("Remove")) { + removeLight = true; + } + }); if (header.enabledChanged) { changed = true; } @@ -1971,30 +2741,24 @@ void Engine::renderInspectorPanel() { ImGui::PushID("Light"); ImGui::Indent(10.0f); - int currentType = (obj.type == ObjectType::DirectionalLight) ? 0 : - (obj.type == ObjectType::PointLight) ? 1 : - (obj.type == ObjectType::SpotLight) ? 2 : 3; + int currentType = static_cast(obj.light.type); const char* typeLabels[] = { "Directional", "Point", "Spot", "Area" }; if (ImGui::Combo("Type", ¤tType, typeLabels, IM_ARRAYSIZE(typeLabels))) { - if (currentType == 0) obj.type = ObjectType::DirectionalLight; - else if (currentType == 1) obj.type = ObjectType::PointLight; - else if (currentType == 2) obj.type = ObjectType::SpotLight; - else obj.type = ObjectType::AreaLight; obj.light.type = (currentType == 0 ? LightType::Directional : currentType == 1 ? LightType::Point : currentType == 2 ? LightType::Spot : LightType::Area); // Reset sensible defaults when type changes - if (obj.type == ObjectType::DirectionalLight) { + if (obj.light.type == LightType::Directional) { obj.light.intensity = 1.0f; - } else if (obj.type == ObjectType::PointLight) { + } else if (obj.light.type == LightType::Point) { obj.light.range = 12.0f; obj.light.intensity = 2.0f; - } else if (obj.type == ObjectType::SpotLight) { + } else if (obj.light.type == LightType::Spot) { obj.light.range = 15.0f; obj.light.intensity = 2.5f; obj.light.innerAngle = 15.0f; obj.light.outerAngle = 25.0f; - } else if (obj.type == ObjectType::AreaLight) { + } else if (obj.light.type == LightType::Area) { obj.light.range = 10.0f; obj.light.intensity = 3.0f; obj.light.size = glm::vec2(2.0f, 2.0f); @@ -2009,13 +2773,13 @@ void Engine::renderInspectorPanel() { if (ImGui::SliderFloat("Intensity", &obj.light.intensity, 0.0f, 10.0f)) { changed = true; } - if (obj.type != ObjectType::DirectionalLight) { + if (obj.light.type != LightType::Directional) { if (ImGui::SliderFloat("Range", &obj.light.range, 0.0f, 50.0f)) { changed = true; } } - if (obj.type == ObjectType::SpotLight) { + if (obj.light.type == LightType::Spot) { if (ImGui::SliderFloat("Inner Angle", &obj.light.innerAngle, 1.0f, 90.0f)) { changed = true; } @@ -2024,7 +2788,7 @@ void Engine::renderInspectorPanel() { } } - if (obj.type == ObjectType::AreaLight) { + if (obj.light.type == LightType::Area) { if (ImGui::DragFloat2("Size", &obj.light.size.x, 0.05f, 0.1f, 10.0f)) { changed = true; } @@ -2036,13 +2800,18 @@ void Engine::renderInspectorPanel() { ImGui::Unindent(10.0f); ImGui::PopID(); } + if (removeLight) { + obj.hasLight = false; + UpdateLegacyTypeFromComponents(obj); + changed = true; + } if (changed) { projectManager.currentProject.hasUnsavedChanges = true; } ImGui::PopStyleColor(); } - if (obj.type == ObjectType::OBJMesh) { + if (obj.hasRenderer && obj.renderType == RenderType::OBJMesh) { ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.3f, 0.5f, 0.4f, 1.0f)); @@ -2097,7 +2866,7 @@ void Engine::renderInspectorPanel() { ImGui::PopStyleColor(); } - if (obj.type == ObjectType::Model) { + if (obj.hasRenderer && obj.renderType == RenderType::Model) { ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.45f, 0.65f, 1.0f)); @@ -2120,12 +2889,29 @@ void Engine::renderInspectorPanel() { ImGui::Spacing(); if (ImGui::Button("Reload Model", ImVec2(-1, 0))) { - ModelLoadResult result = getModelLoader().loadModel(obj.meshPath); - if (result.success) { - obj.meshId = result.meshIndex; + bool reloaded = false; + if (obj.meshSourceIndex >= 0) { + ModelSceneData sceneData; + std::string err; + if (getModelLoader().loadModelScene(obj.meshPath, sceneData, err)) { + int sourceIndex = obj.meshSourceIndex; + if (sourceIndex >= 0 && sourceIndex < (int)sceneData.meshIndices.size()) { + obj.meshId = sceneData.meshIndices[sourceIndex]; + reloaded = true; + } + } + } + if (!reloaded) { + ModelLoadResult result = getModelLoader().loadModel(obj.meshPath); + if (result.success) { + obj.meshId = result.meshIndex; + reloaded = true; + } else { + addConsoleMessage("Failed to reload: " + result.errorMessage, ConsoleMessageType::Error); + } + } + if (reloaded) { addConsoleMessage("Reloaded model: " + obj.name, ConsoleMessageType::Success); - } else { - addConsoleMessage("Failed to reload: " + result.errorMessage, ConsoleMessageType::Error); } } } else { @@ -2133,12 +2919,29 @@ void Engine::renderInspectorPanel() { ImGui::TextDisabled("Path: %s", obj.meshPath.c_str()); if (ImGui::Button("Try Reload", ImVec2(-1, 0))) { - ModelLoadResult result = getModelLoader().loadModel(obj.meshPath); - if (result.success) { - obj.meshId = result.meshIndex; + bool reloaded = false; + if (obj.meshSourceIndex >= 0) { + ModelSceneData sceneData; + std::string err; + if (getModelLoader().loadModelScene(obj.meshPath, sceneData, err)) { + int sourceIndex = obj.meshSourceIndex; + if (sourceIndex >= 0 && sourceIndex < (int)sceneData.meshIndices.size()) { + obj.meshId = sceneData.meshIndices[sourceIndex]; + reloaded = true; + } + } + } + if (!reloaded) { + ModelLoadResult result = getModelLoader().loadModel(obj.meshPath); + if (result.success) { + obj.meshId = result.meshIndex; + reloaded = true; + } else { + addConsoleMessage("Failed to reload: " + result.errorMessage, ConsoleMessageType::Error); + } + } + if (reloaded) { addConsoleMessage("Reloaded model: " + obj.name, ConsoleMessageType::Success); - } else { - addConsoleMessage("Failed to reload: " + result.errorMessage, ConsoleMessageType::Error); } } } @@ -2157,11 +2960,20 @@ void Engine::renderInspectorPanel() { ImGui::PushID(static_cast(i)); ScriptComponent& sc = obj.scripts[i]; - std::string headerLabel = sc.path.empty() ? "Script" : fs::path(sc.path).filename().string(); + std::string headerLabel = "Script"; + if (sc.language == ScriptLanguage::CSharp && !sc.managedType.empty()) { + headerLabel = sc.managedType; + } else if (!sc.path.empty()) { + headerLabel = fs::path(sc.path).filename().string(); + } std::string scriptId = "ScriptComponent" + std::to_string(i); auto header = drawComponentHeader(headerLabel.c_str(), scriptId.c_str(), &sc.enabled, true, [&]() { - if (ImGui::MenuItem("Compile", nullptr, false, !sc.path.empty())) { - compileScriptFile(sc.path); + if (ImGui::MenuItem("Compile", nullptr, false, sc.language == ScriptLanguage::Cpp ? !sc.path.empty() : true)) { + if (sc.language == ScriptLanguage::Cpp) { + compileScriptFile(sc.path); + } else { + compileManagedScripts(); + } } if (ImGui::MenuItem("Remove")) { scriptToRemove = static_cast(i); @@ -2177,9 +2989,18 @@ void Engine::renderInspectorPanel() { } if (header.open) { + const char* languageLabels[] = {"C++", "C#"}; + int languageIndex = (sc.language == ScriptLanguage::CSharp) ? 1 : 0; + ImGui::TextDisabled("Language"); + ImGui::SetNextItemWidth(140); + if (ImGui::Combo("##ScriptLanguage", &languageIndex, languageLabels, IM_ARRAYSIZE(languageLabels))) { + sc.language = (languageIndex == 1) ? ScriptLanguage::CSharp : ScriptLanguage::Cpp; + scriptsChanged = true; + } + char pathBuf[512] = {}; std::snprintf(pathBuf, sizeof(pathBuf), "%s", sc.path.c_str()); - ImGui::TextDisabled("Path"); + ImGui::TextDisabled(sc.language == ScriptLanguage::CSharp ? "Assembly Path" : "Path"); ImGui::SetNextItemWidth(-140); if (ImGui::InputText("##ScriptPath", pathBuf, sizeof(pathBuf))) { sc.path = pathBuf; @@ -2190,35 +3011,77 @@ void Engine::renderInspectorPanel() { if (ImGui::SmallButton("Use Selection")) { if (!fileBrowser.selectedFile.empty()) { fs::directory_entry entry(fileBrowser.selectedFile); - if (fileBrowser.getFileCategory(entry) == FileCategory::Script) { + bool useSelection = false; + if (sc.language == ScriptLanguage::Cpp) { + useSelection = (fileBrowser.getFileCategory(entry) == FileCategory::Script); + } else { + std::string ext = entry.path().extension().string(); + useSelection = (ext == ".dll" || ext == ".cs"); + } + if (useSelection) { sc.path = entry.path().string(); scriptsChanged = true; } } } + if (sc.language == ScriptLanguage::CSharp) { + char typeBuf[256] = {}; + std::snprintf(typeBuf, sizeof(typeBuf), "%s", sc.managedType.c_str()); + ImGui::TextDisabled("Type"); + ImGui::SetNextItemWidth(-140); + if (ImGui::InputText("##ScriptType", typeBuf, sizeof(typeBuf))) { + sc.managedType = typeBuf; + scriptsChanged = true; + } + } + if (!sc.path.empty()) { - fs::path binary = resolveScriptBinary(sc.path); - sc.lastBinaryPath = binary.string(); - ScriptRuntime::InspectorFn inspector = scriptRuntime.getInspector(binary); - if (inspector) { - ImGui::Separator(); - ImGui::TextDisabled("Inspector (from script)"); - ScriptContext ctx; - ctx.engine = this; - ctx.object = &obj; - ctx.script = ≻ - // Scope script inspector to avoid shared ImGui IDs across objects or multiple instances - std::string inspectorId = "ScriptInspector##" + std::to_string(obj.id) + sc.path; - ImGui::PushID(inspectorId.c_str()); - inspector(ctx); - ImGui::PopID(); - ctx.SaveAutoSettings(); - } else if (!scriptRuntime.getLastError().empty()) { - ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.6f, 1.0f), "Inspector load failed"); - ImGui::TextWrapped("%s", scriptRuntime.getLastError().c_str()); + ScriptContext ctx; + ctx.engine = this; + ctx.object = &obj; + ctx.script = ≻ + // Scope script inspector to avoid shared ImGui IDs across objects or multiple instances + std::string inspectorId = "ScriptInspector##" + std::to_string(obj.id) + sc.path; + if (sc.language == ScriptLanguage::Cpp) { + fs::path binary = resolveScriptBinary(sc.path); + sc.lastBinaryPath = binary.string(); + ScriptRuntime::InspectorFn inspector = scriptRuntime.getInspector(binary); + if (inspector) { + ImGui::Separator(); + ImGui::TextDisabled("Inspector (from script)"); + ImGui::PushID(inspectorId.c_str()); + inspector(ctx); + ImGui::PopID(); + ctx.SaveAutoSettings(); + } else if (!scriptRuntime.getLastError().empty()) { + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.6f, 1.0f), "Inspector load failed"); + ImGui::TextWrapped("%s", scriptRuntime.getLastError().c_str()); + } else { + ImGui::TextDisabled("No inspector exported (Script_OnInspector)"); + } } else { - ImGui::TextDisabled("No inspector exported (Script_OnInspector)"); + fs::path assembly = resolveManagedAssembly(sc.path); + sc.lastBinaryPath = assembly.string(); + bool hasInspector = managedRuntime.hasInspector(assembly, sc.managedType); + if (hasInspector) { + ImGui::Separator(); + ImGui::TextDisabled("Inspector (from managed script)"); + ImGui::PushID(inspectorId.c_str()); + bool ranInspector = managedRuntime.invokeInspector(assembly, sc.managedType, ctx); + ImGui::PopID(); + if (ranInspector) { + ctx.SaveAutoSettings(); + } else if (!managedRuntime.getLastError().empty()) { + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.6f, 1.0f), "Inspector load failed"); + ImGui::TextWrapped("%s", managedRuntime.getLastError().c_str()); + } + } else if (!managedRuntime.getLastError().empty()) { + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.6f, 1.0f), "Inspector load failed"); + ImGui::TextWrapped("%s", managedRuntime.getLastError().c_str()); + } else { + ImGui::TextDisabled("No inspector exported (Script_OnInspector)"); + } } } @@ -2315,7 +3178,38 @@ void Engine::renderInspectorPanel() { ImGui::OpenPopup("AddComponentPopup"); } if (ImGui::BeginPopup("AddComponentPopup")) { - bool isUIType = isUIObjectType(obj.type); + bool isUIType = isUIObject(obj); + auto applyUiDefaults = [](SceneObject& target, UIElementType type) { + target.ui.type = type; + switch (type) { + case UIElementType::Canvas: + target.ui.label = "Canvas"; + target.ui.size = glm::vec2(600.0f, 400.0f); + break; + case UIElementType::Image: + target.ui.label = "Image"; + target.ui.size = glm::vec2(200.0f, 200.0f); + break; + case UIElementType::Slider: + target.ui.label = "Slider"; + target.ui.size = glm::vec2(240.0f, 32.0f); + break; + case UIElementType::Button: + target.ui.label = "Button"; + target.ui.size = glm::vec2(160.0f, 40.0f); + break; + case UIElementType::Text: + target.ui.label = "Text"; + target.ui.size = glm::vec2(240.0f, 32.0f); + break; + case UIElementType::Sprite2D: + target.ui.label = "Sprite2D"; + target.ui.size = glm::vec2(128.0f, 128.0f); + break; + case UIElementType::None: + break; + } + }; ImGui::BeginDisabled(isUIType); if (!obj.hasRigidbody && ImGui::MenuItem("Rigidbody3D")) { obj.hasRigidbody = true; @@ -2329,6 +3223,17 @@ void Engine::renderInspectorPanel() { obj.rigidbody2D = Rigidbody2DComponent{}; componentChanged = true; } + if (!obj.hasCollider2D && ImGui::MenuItem("Collider2D")) { + obj.hasCollider2D = true; + obj.collider2D = Collider2DComponent{}; + obj.collider2D.boxSize = glm::max(obj.ui.size, glm::vec2(1.0f)); + componentChanged = true; + } + if (!obj.hasParallaxLayer2D && ImGui::MenuItem("Parallax Layer 2D")) { + obj.hasParallaxLayer2D = true; + obj.parallaxLayer2D = ParallaxLayer2DComponent{}; + componentChanged = true; + } ImGui::EndDisabled(); if (!obj.hasPlayerController && ImGui::MenuItem("Player Controller")) { obj.hasPlayerController = true; @@ -2350,6 +3255,172 @@ void Engine::renderInspectorPanel() { obj.audioSource = AudioSourceComponent{}; componentChanged = true; } + ImGui::BeginDisabled(isUIType); + if (!obj.hasReverbZone && ImGui::MenuItem("Reverb Zone")) { + obj.hasReverbZone = true; + obj.reverbZone = ReverbZoneComponent{}; + obj.reverbZone.boxSize = glm::max(obj.scale, glm::vec3(1.0f)); + componentChanged = true; + } + ImGui::EndDisabled(); + if (!obj.hasAnimation && ImGui::MenuItem("Animation")) { + obj.hasAnimation = true; + obj.animation = AnimationComponent{}; + showAnimationWindow = true; + animationTargetId = obj.id; + componentChanged = true; + } + if (!obj.hasCamera && ImGui::MenuItem("Camera")) { + obj.hasCamera = true; + obj.camera = CameraComponent{}; + UpdateLegacyTypeFromComponents(obj); + componentChanged = true; + } + ImGui::BeginDisabled(!obj.hasCamera); + if (!obj.hasCameraFollow2D && ImGui::MenuItem("Camera Follow 2D")) { + obj.hasCameraFollow2D = true; + obj.cameraFollow2D = CameraFollow2DComponent{}; + componentChanged = true; + } + ImGui::EndDisabled(); + if (!obj.hasPostFX && ImGui::MenuItem("Post Processing")) { + obj.hasPostFX = true; + obj.postFx = PostFXSettings{}; + UpdateLegacyTypeFromComponents(obj); + componentChanged = true; + } + if (!obj.hasLight && ImGui::BeginMenu("Light")) { + if (ImGui::MenuItem("Directional")) { + obj.hasLight = true; + obj.light = LightComponent{}; + obj.light.type = LightType::Directional; + UpdateLegacyTypeFromComponents(obj); + componentChanged = true; + } + if (ImGui::MenuItem("Point")) { + obj.hasLight = true; + obj.light = LightComponent{}; + obj.light.type = LightType::Point; + obj.light.range = 12.0f; + obj.light.intensity = 2.0f; + UpdateLegacyTypeFromComponents(obj); + componentChanged = true; + } + if (ImGui::MenuItem("Spot")) { + obj.hasLight = true; + obj.light = LightComponent{}; + obj.light.type = LightType::Spot; + obj.light.range = 15.0f; + obj.light.intensity = 2.5f; + UpdateLegacyTypeFromComponents(obj); + componentChanged = true; + } + if (ImGui::MenuItem("Area")) { + obj.hasLight = true; + obj.light = LightComponent{}; + obj.light.type = LightType::Area; + obj.light.range = 10.0f; + obj.light.intensity = 3.0f; + obj.light.size = glm::vec2(2.0f, 2.0f); + UpdateLegacyTypeFromComponents(obj); + componentChanged = true; + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Renderer")) { + if (ImGui::MenuItem("Cube")) { + obj.hasRenderer = true; + obj.renderType = RenderType::Cube; + UpdateLegacyTypeFromComponents(obj); + componentChanged = true; + } + if (ImGui::MenuItem("Sphere")) { + obj.hasRenderer = true; + obj.renderType = RenderType::Sphere; + UpdateLegacyTypeFromComponents(obj); + componentChanged = true; + } + if (ImGui::MenuItem("Capsule")) { + obj.hasRenderer = true; + obj.renderType = RenderType::Capsule; + UpdateLegacyTypeFromComponents(obj); + componentChanged = true; + } + if (ImGui::MenuItem("Plane")) { + obj.hasRenderer = true; + obj.renderType = RenderType::Plane; + obj.scale = glm::vec3(2.0f, 2.0f, 0.05f); + syncLocalTransform(obj); + UpdateLegacyTypeFromComponents(obj); + componentChanged = true; + } + if (ImGui::MenuItem("Torus")) { + obj.hasRenderer = true; + obj.renderType = RenderType::Torus; + UpdateLegacyTypeFromComponents(obj); + componentChanged = true; + } + if (ImGui::MenuItem("Sprite (Quad)")) { + obj.hasRenderer = true; + obj.renderType = RenderType::Sprite; + obj.scale = glm::vec3(1.0f, 1.0f, 0.05f); + obj.material.ambientStrength = 1.0f; + syncLocalTransform(obj); + UpdateLegacyTypeFromComponents(obj); + componentChanged = true; + } + if (ImGui::MenuItem("Mirror")) { + obj.hasRenderer = true; + obj.renderType = RenderType::Mirror; + obj.useOverlay = true; + obj.material.textureMix = 1.0f; + obj.material.color = glm::vec3(1.0f); + obj.scale = glm::vec3(2.0f, 2.0f, 0.05f); + syncLocalTransform(obj); + UpdateLegacyTypeFromComponents(obj); + componentChanged = true; + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("UI Element")) { + if (ImGui::MenuItem("Canvas")) { + obj.hasUI = true; + applyUiDefaults(obj, UIElementType::Canvas); + UpdateLegacyTypeFromComponents(obj); + componentChanged = true; + } + if (ImGui::MenuItem("Image")) { + obj.hasUI = true; + applyUiDefaults(obj, UIElementType::Image); + UpdateLegacyTypeFromComponents(obj); + componentChanged = true; + } + if (ImGui::MenuItem("Slider")) { + obj.hasUI = true; + applyUiDefaults(obj, UIElementType::Slider); + UpdateLegacyTypeFromComponents(obj); + componentChanged = true; + } + if (ImGui::MenuItem("Button")) { + obj.hasUI = true; + applyUiDefaults(obj, UIElementType::Button); + UpdateLegacyTypeFromComponents(obj); + componentChanged = true; + } + if (ImGui::MenuItem("Text")) { + obj.hasUI = true; + applyUiDefaults(obj, UIElementType::Text); + UpdateLegacyTypeFromComponents(obj); + componentChanged = true; + } + if (ImGui::MenuItem("Sprite2D")) { + obj.hasUI = true; + applyUiDefaults(obj, UIElementType::Sprite2D); + UpdateLegacyTypeFromComponents(obj); + componentChanged = true; + } + ImGui::EndMenu(); + } if (!obj.hasCollider && ImGui::BeginMenu("Collider")) { if (ImGui::MenuItem("Box Collider")) { obj.hasCollider = true; @@ -2408,6 +3479,7 @@ void Engine::renderInspectorPanel() { void Engine::renderConsolePanel() { ImGui::Begin("Console", &showConsole); + bool settingsChanged = false; if (ImGui::Button("Clear")) { consoleLog.clear(); } @@ -2415,11 +3487,19 @@ void Engine::renderConsolePanel() { ImGui::SameLine(); static bool autoScroll = true; ImGui::Checkbox("Auto-scroll", &autoScroll); + ImGui::SameLine(); + if (ImGui::Checkbox("Wrap Text", &consoleWrapText)) { + settingsChanged = true; + } ImGui::Separator(); ImGui::BeginChild("ConsoleOutput", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar); + if (consoleWrapText) { + ImGui::PushTextWrapPos(0.0f); + } + for (const auto& log : consoleLog) { ImVec4 color = ImVec4(0.8f, 0.8f, 0.8f, 1.0f); if (log.find("Error") != std::string::npos) { @@ -2432,11 +3512,18 @@ void Engine::renderConsolePanel() { ImGui::TextColored(color, "%s", log.c_str()); } + if (consoleWrapText) { + ImGui::PopTextWrapPos(); + } + if (autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) { ImGui::SetScrollHereY(1.0f); } ImGui::EndChild(); + if (settingsChanged) { + saveEditorUserSettings(); + } ImGui::End(); } @@ -2637,10 +3724,16 @@ void Engine::renderDialogs() { bool allowClose = !compileInProgress; if (ImGui::BeginPopupModal("Script Compile", allowClose ? &showCompilePopup : nullptr, flags)) { ImGui::TextWrapped("%s", lastCompileStatus.c_str()); - float t = static_cast(glfwGetTime()); - float pulse = 0.5f + 0.5f * std::sin(t * 2.5f); - ImGui::ProgressBar(compileInProgress ? pulse : 1.0f, ImVec2(-1, 0), - compileInProgress ? "Working..." : "Done"); + float progress = 1.0f; + std::string stageText; + { + std::lock_guard lock(compileMutex); + progress = compileInProgress ? compileProgress : 1.0f; + stageText = compileInProgress ? compileStage : (lastCompileSuccess ? "Done" : "Failed"); + } + const char* stageLabel = stageText.empty() ? "Working..." : stageText.c_str(); + if (progress <= 0.0f) progress = 0.02f; + ImGui::ProgressBar(progress, ImVec2(-1, 0), stageLabel); ImGui::Separator(); ImGui::BeginChild("CompileLog", ImVec2(0, -40), true); if (lastCompileLog.empty() && compileInProgress) { diff --git a/src/EditorWindows/ScriptingWindow.cpp b/src/EditorWindows/ScriptingWindow.cpp new file mode 100644 index 0000000..9f6c44b --- /dev/null +++ b/src/EditorWindows/ScriptingWindow.cpp @@ -0,0 +1,538 @@ +#include "Engine.h" + +#include +#include +#include +#include +#include + +namespace { + static uint64_t hashBuffer(const std::string& text) { + uint64_t hash = 1469598103934665603ull; + for (unsigned char c : text) { + hash ^= static_cast(c); + hash *= 1099511628211ull; + } + return hash; + } + + static std::string trimLeft(const std::string& value) { + size_t start = value.find_first_not_of(" \t"); + if (start == std::string::npos) return ""; + return value.substr(start); + } + + static std::vector buildSymbolList(const std::string& text) { + std::vector symbols; + std::istringstream input(text); + std::string line; + while (std::getline(input, line)) { + std::string trimmed = trimLeft(line); + if (trimmed.empty()) continue; + if (trimmed.rfind("//", 0) == 0) continue; + + auto captureToken = [&](const std::string& prefix) -> bool { + if (trimmed.rfind(prefix, 0) != 0) return false; + size_t start = prefix.size(); + while (start < trimmed.size() && std::isspace(static_cast(trimmed[start]))) { + ++start; + } + size_t end = start; + while (end < trimmed.size() && + (std::isalnum(static_cast(trimmed[end])) || trimmed[end] == '_' || trimmed[end] == ':')) { + ++end; + } + if (end > start) { + symbols.emplace_back(trimmed.substr(0, end)); + return true; + } + return false; + }; + + if (captureToken("class ") || captureToken("struct ") || captureToken("enum ") || captureToken("namespace ")) { + continue; + } + + if (trimmed.find('(') != std::string::npos && trimmed.find(')') != std::string::npos && + (trimmed.find('{') != std::string::npos || trimmed.back() == ';')) { + static const char* kSkip[] = {"if", "for", "while", "switch", "catch"}; + bool skip = false; + for (const char* keyword : kSkip) { + if (trimmed.rfind(keyword, 0) == 0) { + skip = true; + break; + } + } + if (skip) continue; + size_t paren = trimmed.find('('); + if (paren != std::string::npos && paren > 0) { + size_t end = paren; + while (end > 0 && std::isspace(static_cast(trimmed[end - 1]))) { + --end; + } + size_t start = end; + while (start > 0 && + (std::isalnum(static_cast(trimmed[start - 1])) || trimmed[start - 1] == '_' || + trimmed[start - 1] == ':')) { + --start; + } + if (end > start) { + symbols.emplace_back(trimmed.substr(start, paren - start)); + } + } + } + } + return symbols; + } + + static std::vector buildCompletionList(const std::vector& pool, + const std::string& prefix, + size_t limit = 16) { + std::vector matches; + if (prefix.empty()) return matches; + for (const auto& entry : pool) { + if (entry.rfind(prefix, 0) == 0) { + matches.push_back(entry); + if (matches.size() >= limit) break; + } + } + return matches; + } + + static const std::unordered_set& cppKeywordSet() { + static const std::unordered_set kKeywords = { + "auto", "bool", "break", "case", "catch", "char", "class", "const", "constexpr", "continue", + "default", "delete", "do", "double", "else", "enum", "explicit", "extern", "false", "float", + "for", "friend", "if", "inline", "int", "long", "mutable", "namespace", "new", "noexcept", + "operator", "private", "protected", "public", "return", "short", "signed", "sizeof", "static", + "struct", "switch", "template", "this", "throw", "true", "try", "typedef", "typename", + "union", "unsigned", "using", "virtual", "void", "volatile", "while" + }; + return kKeywords; + } + + static std::vector extractIdentifiers(const std::string& text) { + std::unordered_set unique; + const auto& keywords = cppKeywordSet(); + std::string token; + token.reserve(64); + auto flushToken = [&]() { + if (token.size() >= 2 && keywords.find(token) == keywords.end()) { + unique.insert(token); + } + token.clear(); + }; + for (char c : text) { + if (std::isalnum(static_cast(c)) || c == '_') { + token.push_back(c); + } else if (!token.empty()) { + flushToken(); + } + } + if (!token.empty()) { + flushToken(); + } + std::vector out(unique.begin(), unique.end()); + std::sort(out.begin(), out.end()); + return out; + } + +} + +void Engine::refreshScriptingFileList() { + scriptingFileList.clear(); + scriptingCompletions.clear(); + if (!projectManager.currentProject.isLoaded) { + return; + } + + fs::path configPath = resolveScriptsConfigPath(projectManager.currentProject); + ScriptBuildConfig config; + std::string error; + if (!scriptCompiler.loadConfig(configPath, config, error)) { + return; + } + fs::path scriptsRoot = config.scriptsDir; + if (!scriptsRoot.is_absolute()) { + scriptsRoot = projectManager.currentProject.projectPath / scriptsRoot; + } + std::error_code ec; + if (!fs::exists(scriptsRoot, ec)) { + return; + } + + const std::unordered_set validExt = { + ".cpp", ".cc", ".cxx", ".c", ".hpp", ".h", ".inl" + }; + + for (auto it = fs::recursive_directory_iterator(scriptsRoot, ec); + it != fs::recursive_directory_iterator(); ++it) { + if (it->is_directory()) continue; + std::string ext = it->path().extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); + if (validExt.find(ext) == validExt.end()) continue; + scriptingFileList.push_back(it->path()); + } + + std::sort(scriptingFileList.begin(), scriptingFileList.end()); + + std::unordered_set uniqueSymbols; + for (const auto& scriptPath : scriptingFileList) { + std::ifstream file(scriptPath); + if (!file.is_open()) continue; + std::stringstream buffer; + buffer << file.rdbuf(); + std::vector symbols = buildSymbolList(buffer.str()); + for (auto& symbol : symbols) { + uniqueSymbols.insert(symbol); + } + } + scriptingCompletions.assign(uniqueSymbols.begin(), uniqueSymbols.end()); + std::sort(scriptingCompletions.begin(), scriptingCompletions.end()); +} + +void Engine::openScriptInEditor(const fs::path& path) { + if (path.empty()) return; + std::error_code ec; + fs::path absPath = fs::absolute(path, ec); + fs::path normalized = (ec ? path : absPath).lexically_normal(); + + std::ifstream file(normalized); + std::stringstream buffer; + if (file.is_open()) { + buffer << file.rdbuf(); + } + scriptEditorState.filePath = normalized; + scriptEditorState.buffer = buffer.str(); + if (!scriptTextEditorReady) { + auto lang = TextEditor::LanguageDefinition::CPlusPlus(); + lang.mIdentifiers.insert({"Begin", {}}); + lang.mIdentifiers.insert({"TickUpdate", {}}); + lang.mIdentifiers.insert({"Spec", {}}); + lang.mIdentifiers.insert({"TestEditor", {}}); + lang.mIdentifiers.insert({"Update", {}}); + scriptTextEditor.SetLanguageDefinition(lang); + auto palette = scriptTextEditor.GetPalette(); + palette[(int)TextEditor::PaletteIndex::KnownIdentifier] = IM_COL32(220, 180, 70, 255); + scriptTextEditor.SetPalette(palette); + scriptTextEditor.SetShowWhitespaces(true); + scriptTextEditor.SetAllowTabInput(false); + scriptTextEditor.SetSmartTabDelete(true); + scriptTextEditorReady = true; + } + scriptTextEditor.SetText(scriptEditorState.buffer); + scriptEditorState.dirty = false; + scriptEditorState.hasWriteTime = false; + if (fs::exists(normalized, ec)) { + scriptEditorState.lastWriteTime = fs::last_write_time(normalized, ec); + scriptEditorState.hasWriteTime = !ec; + } + showScriptingWindow = true; +} + +void Engine::renderScriptingWindow() { + if (!showScriptingWindow) return; + + if (scriptingFilesDirty) { + refreshScriptingFileList(); + scriptingFilesDirty = false; + } + + ImGui::Begin("Scripting", &showScriptingWindow); + if (!projectManager.currentProject.isLoaded) { + ImGui::TextDisabled("Load a project to edit scripts."); + ImGui::End(); + return; + } + + static std::vector symbols; + static uint64_t symbolsHash = 0; + static std::vector bufferIdentifiers; + static uint64_t identifiersHash = 0; + static std::vector completionPool; + static std::vector activeSuggestions; + static std::string activePrefix; + static bool completionPanelOpen = true; + + ImGui::TextDisabled("C++ Script Editor"); + ImGui::SameLine(); + if (ImGui::Button("Refresh List")) { + scriptingFilesDirty = true; + } + + ImGui::Separator(); + + float leftWidth = 240.0f; + ImGui::BeginChild("ScriptingFiles", ImVec2(leftWidth, 0.0f), true); + ImGui::TextDisabled("Scripts"); + ImGui::InputTextWithHint("##ScriptFilter", "Filter", scriptingFilter, sizeof(scriptingFilter)); + ImGui::Separator(); + + for (const auto& scriptPath : scriptingFileList) { + std::string label = scriptPath.filename().string(); + std::string filter = scriptingFilter; + std::transform(filter.begin(), filter.end(), filter.begin(), ::tolower); + std::string lowerLabel = label; + std::transform(lowerLabel.begin(), lowerLabel.end(), lowerLabel.begin(), ::tolower); + if (!filter.empty() && lowerLabel.find(filter) == std::string::npos) { + continue; + } + bool selected = (scriptEditorState.filePath == scriptPath); + if (ImGui::Selectable(label.c_str(), selected)) { + openScriptInEditor(scriptPath); + } + } + ImGui::EndChild(); + + ImGui::SameLine(); + + ImGui::BeginChild("ScriptingEditor", ImVec2(0.0f, 0.0f), false); + ImGui::TextDisabled("Active File"); + ImGui::SameLine(); + std::string fileLabel = scriptEditorState.filePath.empty() + ? std::string("None") + : scriptEditorState.filePath.filename().string(); + ImGui::TextUnformatted(fileLabel.c_str()); + + bool hasFile = !scriptEditorState.filePath.empty(); + ImGui::SameLine(); + if (!hasFile) { + ImGui::BeginDisabled(); + } + if (ImGui::Button("Save")) { + std::ofstream out(scriptEditorState.filePath); + if (out.is_open()) { + scriptEditorState.buffer = scriptTextEditor.GetText(); + out << scriptEditorState.buffer; + scriptEditorState.dirty = false; + std::error_code ec; + scriptEditorState.lastWriteTime = fs::last_write_time(scriptEditorState.filePath, ec); + scriptEditorState.hasWriteTime = !ec; + if (scriptEditorState.autoCompileOnSave) { + compileScriptFile(scriptEditorState.filePath); + } + } + } + ImGui::SameLine(); + if (ImGui::Button("Compile")) { + compileScriptFile(scriptEditorState.filePath); + } + ImGui::SameLine(); + ImGui::Checkbox("Auto-compile on save", &scriptEditorState.autoCompileOnSave); + if (!hasFile) { + ImGui::EndDisabled(); + } + + bool canReload = false; + if (hasFile && scriptEditorState.hasWriteTime) { + std::error_code ec; + if (fs::exists(scriptEditorState.filePath, ec)) { + auto diskTime = fs::last_write_time(scriptEditorState.filePath, ec); + if (!ec && diskTime > scriptEditorState.lastWriteTime) { + canReload = true; + } + } + } + if (canReload) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.35f, 1.0f), "File changed on disk"); + ImGui::SameLine(); + if (ImGui::Button("Reload")) { + openScriptInEditor(scriptEditorState.filePath); + } + } + + ImGui::Separator(); + + if (ImGui::BeginTabBar("ScriptingTabs")) { + if (ImGui::BeginTabItem("Editor")) { + if (hasFile) { + if (!scriptTextEditorReady) { + auto lang = TextEditor::LanguageDefinition::CPlusPlus(); + lang.mIdentifiers.insert({"Begin", {}}); + lang.mIdentifiers.insert({"TickUpdate", {}}); + lang.mIdentifiers.insert({"Spec", {}}); + lang.mIdentifiers.insert({"TestEditor", {}}); + lang.mIdentifiers.insert({"Update", {}}); + scriptTextEditor.SetLanguageDefinition(lang); + auto palette = scriptTextEditor.GetPalette(); + palette[(int)TextEditor::PaletteIndex::KnownIdentifier] = IM_COL32(220, 180, 70, 255); + scriptTextEditor.SetPalette(palette); + scriptTextEditor.SetShowWhitespaces(true); + scriptTextEditor.SetAllowTabInput(false); + scriptTextEditor.SetSmartTabDelete(true); + scriptTextEditorReady = true; + } + completionPool.clear(); + std::unordered_set poolSet; + for (const auto& kw : cppKeywordSet()) { + poolSet.insert(kw); + } + for (const auto& entry : scriptingCompletions) { + poolSet.insert(entry); + } + for (const auto& entry : symbols) { + poolSet.insert(entry); + } + uint64_t bufferHash = hashBuffer(scriptEditorState.buffer); + if (bufferHash != identifiersHash) { + identifiersHash = bufferHash; + bufferIdentifiers = extractIdentifiers(scriptEditorState.buffer); + } + for (const auto& entry : bufferIdentifiers) { + poolSet.insert(entry); + } + completionPool.assign(poolSet.begin(), poolSet.end()); + std::sort(completionPool.begin(), completionPool.end()); + + TextEditor::Coordinates cursorBefore = scriptTextEditor.GetCursorPosition(); + activePrefix = scriptTextEditor.GetWordAtPublic(cursorBefore); + if (activePrefix.empty() && cursorBefore.mColumn > 0) { + TextEditor::Coordinates prev(cursorBefore.mLine, cursorBefore.mColumn - 1); + activePrefix = scriptTextEditor.GetWordAtPublic(prev); + } + if (!activePrefix.empty() && activePrefix.size() >= 2) { + activeSuggestions = buildCompletionList(completionPool, activePrefix); + } else { + activeSuggestions.clear(); + } + + bool tabPressed = ImGui::IsKeyPressed(ImGuiKey_Tab); + bool canComplete = !activeSuggestions.empty() && !ImGui::GetIO().KeyShift; + scriptTextEditor.SetAllowTabInput(!canComplete); + + float completionHeight = completionPanelOpen ? 140.0f : 0.0f; + float availHeight = ImGui::GetContentRegionAvail().y; + float editorHeight = std::max(120.0f, availHeight - completionHeight - 12.0f); + ImVec2 editorSize = ImVec2(0.0f, editorHeight); + scriptTextEditor.Render("##ScriptEditor", editorSize, false); + if (scriptTextEditor.IsTextChanged()) { + scriptEditorState.dirty = true; + scriptEditorState.buffer = scriptTextEditor.GetText(); + } + uint64_t newHash = hashBuffer(scriptEditorState.buffer); + if (newHash != symbolsHash) { + symbolsHash = newHash; + symbols = buildSymbolList(scriptEditorState.buffer); + } + + TextEditor::Coordinates cursorAfter = scriptTextEditor.GetCursorPosition(); + activePrefix = scriptTextEditor.GetWordAtPublic(cursorAfter); + if (activePrefix.empty() && cursorAfter.mColumn > 0) { + TextEditor::Coordinates prev(cursorAfter.mLine, cursorAfter.mColumn - 1); + activePrefix = scriptTextEditor.GetWordAtPublic(prev); + } + if (!activePrefix.empty() && activePrefix.size() >= 2) { + activeSuggestions = buildCompletionList(completionPool, activePrefix); + } else { + activeSuggestions.clear(); + } + bool canCompleteNow = !activeSuggestions.empty() && !ImGui::GetIO().KeyShift; + + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && + tabPressed && canCompleteNow) { + TextEditor::Coordinates cursor = scriptTextEditor.GetCursorPosition(); + TextEditor::Coordinates start(cursor.mLine, + std::max(0, cursor.mColumn - static_cast(activePrefix.size()))); + scriptTextEditor.SetSelection(start, cursor); + scriptTextEditor.Delete(); + scriptTextEditor.InsertText(activeSuggestions.front().c_str()); + scriptEditorState.dirty = true; + scriptEditorState.buffer = scriptTextEditor.GetText(); + } + + if (completionPanelOpen) { + ImGui::Separator(); + ImGui::TextDisabled("Completions"); + ImGui::SameLine(); + ImGui::Checkbox("Show", &completionPanelOpen); + ImGui::BeginChild("CompletionList", ImVec2(0.0f, completionHeight), true); + if (activeSuggestions.empty()) { + ImGui::TextDisabled("No suggestions"); + } else { + for (const auto& suggestion : activeSuggestions) { + if (ImGui::Selectable(suggestion.c_str())) { + TextEditor::Coordinates cursor = scriptTextEditor.GetCursorPosition(); + TextEditor::Coordinates start(cursor.mLine, + std::max(0, cursor.mColumn - static_cast(activePrefix.size()))); + scriptTextEditor.SetSelection(start, cursor); + scriptTextEditor.Delete(); + scriptTextEditor.InsertText(suggestion.c_str()); + scriptEditorState.dirty = true; + scriptEditorState.buffer = scriptTextEditor.GetText(); + } + } + } + ImGui::EndChild(); + } + + if (canCompleteNow && scriptTextEditor.HasCursorScreenPosition()) { + const std::string& suggestion = activeSuggestions.front(); + if (suggestion.size() > activePrefix.size() && + suggestion.rfind(activePrefix, 0) == 0) { + std::string ghost = suggestion.substr(activePrefix.size()); + ImVec2 ghostPos = scriptTextEditor.GetCursorScreenPositionPublic(); + ImU32 ghostColor = IM_COL32(180, 180, 180, 110); + ImGui::GetWindowDrawList()->AddText(ghostPos, ghostColor, ghost.c_str()); + } + } + } else { + ImGui::TextDisabled("Select a script file to start editing."); + } + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Intellisense")) { + ScriptBuildConfig config; + std::string error; + fs::path configPath = resolveScriptsConfigPath(projectManager.currentProject); + bool hasConfig = scriptCompiler.loadConfig(configPath, config, error); + if (hasConfig) { + packageManager.applyToBuildConfig(config); + ImGui::TextDisabled("Compiler"); + ImGui::Text("Standard: %s", config.cppStandard.c_str()); + ImGui::Separator(); + ImGui::TextDisabled("Include Dirs"); + for (const auto& includeDir : config.includeDirs) { + ImGui::BulletText("%s", includeDir.string().c_str()); + } + ImGui::Separator(); + ImGui::TextDisabled("Defines"); + for (const auto& def : config.defines) { + ImGui::BulletText("%s", def.c_str()); + } + } else { + ImGui::TextColored(ImVec4(0.95f, 0.55f, 0.55f, 1.0f), "Scripts.modu not loaded"); + if (!error.empty()) { + ImGui::TextWrapped("%s", error.c_str()); + } + } + + ImGui::Separator(); + ImGui::TextDisabled("Outline"); + if (!symbols.empty()) { + for (const auto& symbol : symbols) { + ImGui::BulletText("%s", symbol.c_str()); + } + } else { + ImGui::TextDisabled("No symbols detected."); + } + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Build")) { + ImGui::TextDisabled("Compile Status"); + ImGui::Text("%s", lastCompileStatus.c_str()); + if (!lastCompileLog.empty()) { + ImGui::Separator(); + ImGui::TextDisabled("Output"); + ImGui::BeginChild("CompileLog", ImVec2(0.0f, 0.0f), true); + ImGui::TextUnformatted(lastCompileLog.c_str()); + ImGui::EndChild(); + } + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + + ImGui::EndChild(); + ImGui::End(); +} diff --git a/src/EditorWindows/ViewportWindows.cpp b/src/EditorWindows/ViewportWindows.cpp index f35c2b7..e64c667 100644 --- a/src/EditorWindows/ViewportWindows.cpp +++ b/src/EditorWindows/ViewportWindows.cpp @@ -261,6 +261,7 @@ void Engine::renderGameViewportWindow() { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.0f, 6.0f)); ImGui::Begin("Game Viewport", &showGameViewport, ImGuiWindowFlags_NoScrollbar); + const bool showGameViewportToolbar = true; bool windowFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); struct GameResolutionOption { const char* label; @@ -282,64 +283,72 @@ void Engine::renderGameViewportWindow() { SceneObject* playerCam = nullptr; for (auto& obj : sceneObjects) { - if (obj.type == ObjectType::Camera && obj.camera.type == SceneCameraType::Player) { + if (obj.hasCamera && obj.camera.type == SceneCameraType::Player) { playerCam = &obj; break; } } - ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.08f, 0.09f, 0.10f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.12f, 0.14f, 0.16f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(0.14f, 0.18f, 0.20f, 1.0f)); - ImGui::BeginDisabled(playerCam == nullptr); - bool dummyToggle = false; bool postFxChanged = false; - if (playerCam) { - bool before = playerCam->camera.applyPostFX; - if (ImGui::Checkbox("Post FX", &playerCam->camera.applyPostFX)) { - postFxChanged = (before != playerCam->camera.applyPostFX); - } - } else { - ImGui::Checkbox("Post FX", &dummyToggle); - } - ImGui::SameLine(); - ImGui::Checkbox("Profiler", &showGameProfiler); - ImGui::SameLine(); - ImGui::Checkbox("Canvas Guides", &showCanvasOverlay); - ImGui::EndDisabled(); - ImGui::PopStyleColor(3); - - ImGui::Spacing(); - const GameResolutionOption& resOption = kGameResolutions[gameViewportResolutionIndex]; - ImGui::SetNextItemWidth(180.0f); - if (ImGui::BeginCombo("Resolution", resOption.label)) { - for (int i = 0; i < (int)kGameResolutions.size(); ++i) { - bool selected = (i == gameViewportResolutionIndex); - if (ImGui::Selectable(kGameResolutions[i].label, selected)) { - gameViewportResolutionIndex = i; + if (!isPlaying && showGameViewportToolbar) { + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.08f, 0.09f, 0.10f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.12f, 0.14f, 0.16f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(0.14f, 0.18f, 0.20f, 1.0f)); + ImGui::BeginDisabled(playerCam == nullptr); + bool dummyToggle = false; + if (playerCam) { + bool before = playerCam->camera.applyPostFX; + if (ImGui::Checkbox("Post FX", &playerCam->camera.applyPostFX)) { + postFxChanged = (before != playerCam->camera.applyPostFX); } - if (selected) ImGui::SetItemDefaultFocus(); + } else { + ImGui::Checkbox("Post FX", &dummyToggle); } - ImGui::EndCombo(); - } - if (kGameResolutions[gameViewportResolutionIndex].custom) { ImGui::SameLine(); - ImGui::SetNextItemWidth(90.0f); - ImGui::DragInt("W", &gameViewportCustomWidth, 1.0f, 64, 8192); + ImGui::Checkbox("Profiler", &showGameProfiler); ImGui::SameLine(); - ImGui::SetNextItemWidth(90.0f); - ImGui::DragInt("H", &gameViewportCustomHeight, 1.0f, 64, 8192); + ImGui::Checkbox("Canvas Guides", &showCanvasOverlay); + ImGui::SameLine(); + ImGui::Checkbox("UI World", &uiWorldMode); + ImGui::SameLine(); + ImGui::Checkbox("UI Grid", &showUIWorldGrid); + ImGui::EndDisabled(); + ImGui::PopStyleColor(3); + + ImGui::Spacing(); } - ImGui::SameLine(); - ImGui::Checkbox("Auto Fit", &gameViewportAutoFit); - ImGui::SameLine(); - ImGui::BeginDisabled(gameViewportAutoFit); - float zoomPercent = gameViewportZoom * 100.0f; - ImGui::SetNextItemWidth(140.0f); - if (ImGui::SliderFloat("Zoom", &zoomPercent, 10.0f, 200.0f, "%.0f%%")) { - gameViewportZoom = zoomPercent / 100.0f; + const GameResolutionOption& resOption = kGameResolutions[gameViewportResolutionIndex]; + if (!isPlaying && showGameViewportToolbar) { + ImGui::SetNextItemWidth(180.0f); + if (ImGui::BeginCombo("Resolution", resOption.label)) { + for (int i = 0; i < (int)kGameResolutions.size(); ++i) { + bool selected = (i == gameViewportResolutionIndex); + if (ImGui::Selectable(kGameResolutions[i].label, selected)) { + gameViewportResolutionIndex = i; + } + if (selected) ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + if (kGameResolutions[gameViewportResolutionIndex].custom) { + ImGui::SameLine(); + ImGui::SetNextItemWidth(90.0f); + ImGui::DragInt("W", &gameViewportCustomWidth, 1.0f, 64, 8192); + ImGui::SameLine(); + ImGui::SetNextItemWidth(90.0f); + ImGui::DragInt("H", &gameViewportCustomHeight, 1.0f, 64, 8192); + } + ImGui::SameLine(); + ImGui::Checkbox("Auto Fit", &gameViewportAutoFit); + ImGui::SameLine(); + ImGui::BeginDisabled(gameViewportAutoFit); + float zoomPercent = gameViewportZoom * 100.0f; + ImGui::SetNextItemWidth(140.0f); + if (ImGui::SliderFloat("Zoom", &zoomPercent, 10.0f, 200.0f, "%.0f%%")) { + gameViewportZoom = zoomPercent / 100.0f; + } + ImGui::EndDisabled(); } - ImGui::EndDisabled(); ImVec2 avail = ImGui::GetContentRegionAvail(); int renderWidth = 0; @@ -398,13 +407,13 @@ void Engine::renderGameViewportWindow() { ImDrawList* drawList = ImGui::GetWindowDrawList(); float uiScaleX = (renderWidth > 0) ? (imageSize.x / (float)renderWidth) : 1.0f; float uiScaleY = (renderHeight > 0) ? (imageSize.y / (float)renderHeight) : 1.0f; - if (showCanvasOverlay) { + if (showGameViewportToolbar && showCanvasOverlay) { ImVec2 pad(8.0f, 8.0f); ImVec2 tl(imageMin.x + pad.x, imageMin.y + pad.y); ImVec2 br(imageMax.x - pad.x, imageMax.y - pad.y); drawList->AddRect(tl, br, IM_COL32(110, 170, 255, 180), 8.0f, 0, 2.0f); } - if (showGameProfiler) { + if (showGameViewportToolbar && showGameProfiler) { float fps = ImGui::GetIO().Framerate; float frameMs = (fps > 0.0f) ? (1000.0f / fps) : 0.0f; int zoomPercent = (int)std::round(zoom * 100.0f); @@ -438,13 +447,8 @@ void Engine::renderGameViewportWindow() { } } bool uiInteracting = false; - auto isUIType = [](ObjectType type) { - return type == ObjectType::Canvas || - type == ObjectType::UIImage || - type == ObjectType::UISlider || - type == ObjectType::UIButton || - type == ObjectType::UIText || - type == ObjectType::Sprite2D; + auto isUIType = [](const SceneObject& target) { + return target.hasUI && target.ui.type != UIElementType::None; }; ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); ImGui::SetCursorScreenPos(imageMin); @@ -484,7 +488,7 @@ void Engine::renderGameViewportWindow() { std::vector chain; const SceneObject* current = &obj; while (current) { - if (isUIType(current->type)) { + if (isUIType(*current)) { chain.push_back(current); } if (current->parentId < 0) break; @@ -516,11 +520,121 @@ void Engine::renderGameViewportWindow() { ImVec2 overlayPos = ImGui::GetWindowPos(); ImVec2 overlaySize = ImGui::GetWindowSize(); - auto clampRectToOverlay = [&](const ImVec2& min, const ImVec2& max, ImVec2& outMin, ImVec2& outMax) { - outMin = ImVec2(std::max(min.x, overlayPos.x), std::max(min.y, overlayPos.y)); - outMax = ImVec2(std::min(max.x, overlayPos.x + overlaySize.x), std::min(max.y, overlayPos.y + overlaySize.y)); - return (outMax.x > outMin.x && outMax.y > outMin.y); + bool allowEditorUi = !isPlaying; + bool useWorldUi = uiWorldMode; + UIWorldCamera2D uiWorldCameraBackup = uiWorldCamera; + bool restoreUiWorldCamera = false; + if (playerCam && playerCam->camera.use2D) { + useWorldUi = true; + restoreUiWorldCamera = true; + uiWorldCamera.position = glm::vec2(playerCam->position.x, playerCam->position.y); + uiWorldCamera.zoom = std::max(1.0f, playerCam->camera.pixelsPerUnit); + } + if (!useWorldUi || !allowEditorUi) { + uiWorldPanning = false; + } + if (useWorldUi) { + uiWorldCamera.viewportSize = glm::vec2(overlaySize.x, overlaySize.y); + } + auto worldToScreen = [&](const glm::vec2& world) { + glm::vec2 local = uiWorldCamera.WorldToScreen(world); + return ImVec2(overlayPos.x + local.x, overlayPos.y + local.y); }; + auto screenToWorld = [&](const ImVec2& screen) { + glm::vec2 local(screen.x - overlayPos.x, screen.y - overlayPos.y); + return uiWorldCamera.ScreenToWorld(local); + }; + auto getWorldParentOffset = [&](const SceneObject& obj) { + glm::vec2 offset(0.0f); + const SceneObject* current = &obj; + while (current && current->parentId >= 0) { + auto pit = std::find_if(sceneObjects.begin(), sceneObjects.end(), + [&](const SceneObject& o) { return o.id == current->parentId; }); + if (pit == sceneObjects.end()) break; + current = &(*pit); + if (current->hasUI && current->ui.type != UIElementType::None) { + offset += glm::vec2(current->ui.position.x, current->ui.position.y); + } + } + return offset; + }; + auto parallaxOffset = [&](const SceneObject& obj) { + if (!obj.hasParallaxLayer2D || !obj.parallaxLayer2D.enabled) return glm::vec2(0.0f); + float factor = std::clamp(obj.parallaxLayer2D.factor, 0.0f, 1.0f); + return uiWorldCamera.position * (1.0f - factor); + }; + auto resolveUIRectWorld = [&](const SceneObject& obj, ImVec2& outMin, ImVec2& outMax) { + glm::vec2 parentOffset = getWorldParentOffset(obj); + glm::vec2 worldPos = parentOffset + glm::vec2(obj.ui.position.x, obj.ui.position.y) + parallaxOffset(obj); + glm::vec2 sizeWorld(obj.ui.size.x, obj.ui.size.y); + ImVec2 pivotOffset = anchorToPivot(obj.ui.anchor, ImVec2(sizeWorld.x, sizeWorld.y)); + glm::vec2 worldMin = worldPos - glm::vec2(pivotOffset.x, pivotOffset.y); + glm::vec2 worldMax = worldMin + sizeWorld; + ImVec2 s0 = worldToScreen(worldMin); + ImVec2 s1 = worldToScreen(worldMax); + outMin = ImVec2(std::min(s0.x, s1.x), std::min(s0.y, s1.y)); + outMax = ImVec2(std::max(s0.x, s1.x), std::max(s0.y, s1.y)); + }; + auto rectOutsideOverlay = [&](const ImVec2& min, const ImVec2& max) { + return (max.x < overlayPos.x || min.x > overlayPos.x + overlaySize.x || + max.y < overlayPos.y || min.y > overlayPos.y + overlaySize.y); + }; + + bool uiWorldHover = imageHovered || ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); + bool uiWorldCameraActive = false; + if (useWorldUi && allowEditorUi) { + ImGuiIO& io = ImGui::GetIO(); + bool panHeld = uiWorldHover && (ImGui::IsMouseDown(ImGuiMouseButton_Middle) || + (ImGui::IsKeyDown(ImGuiKey_Space) && ImGui::IsMouseDown(ImGuiMouseButton_Left))); + if (panHeld) { + uiWorldPanning = true; + } else if (!ImGui::IsMouseDown(ImGuiMouseButton_Middle) && + !(ImGui::IsKeyDown(ImGuiKey_Space) && ImGui::IsMouseDown(ImGuiMouseButton_Left))) { + uiWorldPanning = false; + } + if (uiWorldPanning) { + ImVec2 delta = io.MouseDelta; + if (delta.x != 0.0f || delta.y != 0.0f) { + uiWorldCamera.position.x -= delta.x / uiWorldCamera.zoom; + uiWorldCamera.position.y += delta.y / uiWorldCamera.zoom; + } + uiWorldCameraActive = true; + } + if (uiWorldHover && io.MouseWheel != 0.0f) { + glm::vec2 mouseLocal(io.MousePos.x - overlayPos.x, io.MousePos.y - overlayPos.y); + glm::vec2 worldBefore = uiWorldCamera.ScreenToWorld(mouseLocal); + float zoomFactor = 1.0f + io.MouseWheel * 0.1f; + float newZoom = std::clamp(uiWorldCamera.zoom * zoomFactor, 5.0f, 2000.0f); + if (newZoom != uiWorldCamera.zoom) { + uiWorldCamera.zoom = newZoom; + glm::vec2 worldAfter = uiWorldCamera.ScreenToWorld(mouseLocal); + uiWorldCamera.position += (worldBefore - worldAfter); + uiWorldCameraActive = true; + } + } + if (uiWorldHover) { + glm::vec2 panDir(0.0f); + if (ImGui::IsKeyDown(ImGuiKey_A)) panDir.x -= 1.0f; + if (ImGui::IsKeyDown(ImGuiKey_D)) panDir.x += 1.0f; + if (ImGui::IsKeyDown(ImGuiKey_W)) panDir.y += 1.0f; + if (ImGui::IsKeyDown(ImGuiKey_S)) panDir.y -= 1.0f; + if (panDir.x != 0.0f || panDir.y != 0.0f) { + float panSpeed = 6.0f; + if (ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) { + panSpeed *= 2.5f; + } + uiWorldCamera.position += panDir * (panSpeed * deltaTime); + uiWorldCameraActive = true; + } + } + } + if (playerCam && playerCam->camera.use2D && allowEditorUi && uiWorldCameraActive) { + playerCam->position.x = uiWorldCamera.position.x; + playerCam->position.y = uiWorldCamera.position.y; + playerCam->camera.pixelsPerUnit = uiWorldCamera.zoom; + syncLocalTransform(*playerCam); + projectManager.currentProject.hasUnsavedChanges = true; + } auto brighten = [](const ImVec4& c, float k) { return ImVec4(std::clamp(c.x * k, 0.0f, 1.0f), @@ -528,13 +642,97 @@ void Engine::renderGameViewportWindow() { std::clamp(c.z * k, 0.0f, 1.0f), c.w); }; + float animSpeed = 0.0f; + if (uiAnimationMode == UIAnimationMode::Fluid) { + animSpeed = 8.0f; + } else if (uiAnimationMode == UIAnimationMode::Snappy) { + animSpeed = 18.0f; + } + float animStep = (uiAnimationMode == UIAnimationMode::Off) ? 1.0f + : (1.0f - std::exp(-animSpeed * ImGui::GetIO().DeltaTime)); + auto animateValue = [&](float& current, float target, bool immediate) { + if (uiAnimationMode == UIAnimationMode::Off || immediate) { + current = target; + } else { + current += (target - current) * animStep; + } + return current; + }; + if (useWorldUi && showUIWorldGrid) { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImVec2 overlayMax(overlayPos.x + overlaySize.x, overlayPos.y + overlaySize.y); + dl->PushClipRect(overlayPos, overlayMax, true); + float step = 1.0f; + float minPx = 30.0f; + float maxPx = 140.0f; + while (step * uiWorldCamera.zoom < minPx) step *= 2.0f; + while (step * uiWorldCamera.zoom > maxPx) step *= 0.5f; + + glm::vec2 worldMin = uiWorldCamera.ScreenToWorld(glm::vec2(0.0f, overlaySize.y)); + glm::vec2 worldMax = uiWorldCamera.ScreenToWorld(glm::vec2(overlaySize.x, 0.0f)); + float startX = std::floor(worldMin.x / step) * step; + float endX = std::ceil(worldMax.x / step) * step; + float startY = std::floor(worldMin.y / step) * step; + float endY = std::ceil(worldMax.y / step) * step; + ImU32 gridColor = IM_COL32(90, 110, 140, 50); + ImU32 axisColorX = IM_COL32(240, 120, 120, 170); + ImU32 axisColorY = IM_COL32(120, 240, 150, 170); + + for (float x = startX; x <= endX; x += step) { + ImVec2 p0 = worldToScreen(glm::vec2(x, worldMin.y)); + ImVec2 p1 = worldToScreen(glm::vec2(x, worldMax.y)); + dl->AddLine(p0, p1, gridColor, 1.0f); + } + for (float y = startY; y <= endY; y += step) { + ImVec2 p0 = worldToScreen(glm::vec2(worldMin.x, y)); + ImVec2 p1 = worldToScreen(glm::vec2(worldMax.x, y)); + dl->AddLine(p0, p1, gridColor, 1.0f); + } + + ImVec2 axisX0 = worldToScreen(glm::vec2(worldMin.x, 0.0f)); + ImVec2 axisX1 = worldToScreen(glm::vec2(worldMax.x, 0.0f)); + ImVec2 axisY0 = worldToScreen(glm::vec2(0.0f, worldMin.y)); + ImVec2 axisY1 = worldToScreen(glm::vec2(0.0f, worldMax.y)); + dl->AddLine(axisX0, axisX1, axisColorX, 2.0f); + dl->AddLine(axisY0, axisY1, axisColorY, 2.0f); + + ImVec2 indicator = ImVec2(overlayPos.x + 36.0f, overlayPos.y + overlaySize.y - 36.0f); + dl->AddLine(indicator, ImVec2(indicator.x + 22.0f, indicator.y), axisColorX, 2.0f); + dl->AddLine(indicator, ImVec2(indicator.x, indicator.y - 22.0f), axisColorY, 2.0f); + dl->AddText(ImVec2(indicator.x + 26.0f, indicator.y - 8.0f), axisColorX, "+X"); + dl->AddText(ImVec2(indicator.x - 16.0f, indicator.y - 30.0f), axisColorY, "+Y"); + dl->PopClipRect(); + } + + std::vector uiDrawList; + uiDrawList.reserve(sceneObjects.size()); for (auto& obj : sceneObjects) { - if (!obj.enabled || !isUIType(obj.type)) continue; + if (!obj.enabled || !isUIType(obj)) continue; + uiDrawList.push_back(&obj); + } + if (uiWorldMode) { + std::stable_sort(uiDrawList.begin(), uiDrawList.end(), + [](const SceneObject* a, const SceneObject* b) { + int orderA = (a->hasParallaxLayer2D && a->parallaxLayer2D.enabled) ? a->parallaxLayer2D.order : 0; + int orderB = (b->hasParallaxLayer2D && b->parallaxLayer2D.enabled) ? b->parallaxLayer2D.order : 0; + return orderA < orderB; + }); + } + glm::vec2 worldViewMin = useWorldUi ? uiWorldCamera.ScreenToWorld(glm::vec2(0.0f, overlaySize.y)) : glm::vec2(0.0f); + glm::vec2 worldViewMax = useWorldUi ? uiWorldCamera.ScreenToWorld(glm::vec2(overlaySize.x, 0.0f)) : glm::vec2(0.0f); + + for (SceneObject* objPtr : uiDrawList) { + SceneObject& obj = *objPtr; ImVec2 rectMin, rectMax; - resolveUIRect(obj, rectMin, rectMax); + if (useWorldUi) { + resolveUIRectWorld(obj, rectMin, rectMax); + } else { + resolveUIRect(obj, rectMin, rectMax); + } ImVec2 rectSize(rectMax.x - rectMin.x, rectMax.y - rectMin.y); if (rectSize.x <= 1.0f || rectSize.y <= 1.0f) continue; + if (rectOutsideOverlay(rectMin, rectMax)) continue; ImGuiStyle savedStyle = ImGui::GetStyle(); bool styleApplied = false; @@ -545,49 +743,160 @@ void Engine::renderGameViewportWindow() { } } - if (obj.type == ObjectType::Canvas) { + if (obj.ui.type == UIElementType::Canvas) { ImDrawList* dl = ImGui::GetWindowDrawList(); dl->AddRect(rectMin, rectMax, IM_COL32(110, 170, 255, 140), 6.0f, 0, 1.5f); if (styleApplied) ImGui::GetStyle() = savedStyle; continue; } - ImVec2 clippedMin, clippedMax; - if (!clampRectToOverlay(rectMin, rectMax, clippedMin, clippedMax)) { - continue; - } - ImVec2 clippedSize(clippedMax.x - clippedMin.x, clippedMax.y - clippedMin.y); - ImVec2 localMin(clippedMin.x - overlayPos.x, clippedMin.y - overlayPos.y); + ImVec2 drawMin = rectMin; + ImVec2 drawMax = rectMax; + ImVec2 drawSize(drawMax.x - drawMin.x, drawMax.y - drawMin.y); + ImVec2 localMin(drawMin.x - overlayPos.x, drawMin.y - overlayPos.y); ImGui::PushID(obj.id); - if (obj.type == ObjectType::UIImage || obj.type == ObjectType::Sprite2D) { + UIAnimationState& animState = uiAnimationStates[obj.id]; + if (!animState.initialized) { + animState.sliderValue = obj.ui.sliderValue; + animState.initialized = true; + } + if (obj.ui.type == UIElementType::Image || obj.ui.type == UIElementType::Sprite2D) { unsigned int texId = 0; if (!obj.albedoTexturePath.empty()) { if (auto* tex = renderer.getTexture(obj.albedoTexturePath)) { texId = tex->GetID(); } } - ImGui::SetCursorPos(localMin); ImVec4 tint(obj.ui.color.r, obj.ui.color.g, obj.ui.color.b, obj.ui.color.a); - if (texId != 0) { - ImGui::Image((ImTextureID)(intptr_t)texId, clippedSize, ImVec2(0, 1), ImVec2(1, 0), tint, ImVec4(0, 0, 0, 0)); - } else { - ImDrawList* dl = ImGui::GetWindowDrawList(); - ImU32 fill = ImGui::GetColorU32(tint); - ImU32 border = ImGui::GetColorU32(brighten(tint, 0.85f)); - dl->AddRectFilled(clippedMin, clippedMax, fill, 6.0f); - dl->AddRect(clippedMin, clippedMax, border, 6.0f); - ImVec2 textSize = ImGui::CalcTextSize(obj.ui.label.c_str()); - ImVec2 textPos(clippedMin.x + (clippedSize.x - textSize.x) * 0.5f, clippedMin.y + (clippedSize.y - textSize.y) * 0.5f); - dl->AddText(textPos, IM_COL32(210, 210, 220, 220), obj.ui.label.c_str()); - ImGui::Dummy(clippedSize); + bool repeatX = useWorldUi && obj.hasParallaxLayer2D && obj.parallaxLayer2D.enabled && obj.parallaxLayer2D.repeatX; + bool repeatY = useWorldUi && obj.hasParallaxLayer2D && obj.parallaxLayer2D.enabled && obj.parallaxLayer2D.repeatY; + glm::vec2 spacing = obj.hasParallaxLayer2D ? obj.parallaxLayer2D.repeatSpacing : glm::vec2(0.0f); + float stepX = obj.ui.size.x + spacing.x; + float stepY = obj.ui.size.y + spacing.y; + glm::vec2 baseWorldMin = worldViewMin; + if (repeatX || repeatY) { + glm::vec2 sizeWorld(obj.ui.size.x, obj.ui.size.y); + ImVec2 pivotOffset = anchorToPivot(obj.ui.anchor, ImVec2(sizeWorld.x, sizeWorld.y)); + glm::vec2 parentOffset = getWorldParentOffset(obj); + glm::vec2 worldPos = parentOffset + glm::vec2(obj.ui.position.x, obj.ui.position.y) + parallaxOffset(obj); + baseWorldMin = worldPos - glm::vec2(pivotOffset.x, pivotOffset.y); } - } else if (obj.type == ObjectType::UISlider) { + float angle = glm::radians(obj.ui.rotation); + auto drawImageRect = [&](const ImVec2& min, const ImVec2& max) { + ImVec2 size(max.x - min.x, max.y - min.y); + if (size.x <= 1.0f || size.y <= 1.0f) return; + if (std::abs(angle) > 1e-4f) { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImVec2 center = ImVec2((min.x + max.x) * 0.5f, (min.y + max.y) * 0.5f); + ImVec2 half = ImVec2(size.x * 0.5f, size.y * 0.5f); + float c = std::cos(angle); + float s = std::sin(angle); + auto rotPt = [&](float x, float y) { + return ImVec2(center.x + x * c - y * s, center.y + x * s + y * c); + }; + ImVec2 p0 = rotPt(-half.x, -half.y); + ImVec2 p1 = rotPt( half.x, -half.y); + ImVec2 p2 = rotPt( half.x, half.y); + ImVec2 p3 = rotPt(-half.x, half.y); + if (texId != 0) { + dl->AddImageQuad((ImTextureID)(intptr_t)texId, p0, p1, p2, p3, + ImVec2(0, 1), ImVec2(1, 1), ImVec2(1, 0), ImVec2(0, 0), + ImGui::GetColorU32(tint)); + } else { + ImU32 fill = ImGui::GetColorU32(tint); + ImU32 border = ImGui::GetColorU32(brighten(tint, 0.85f)); + dl->AddQuadFilled(p0, p1, p2, p3, fill); + dl->AddQuad(p0, p1, p2, p3, border, 2.0f); + ImVec2 textSize = ImGui::CalcTextSize(obj.ui.label.c_str()); + ImVec2 textPos(center.x - textSize.x * 0.5f, center.y - textSize.y * 0.5f); + dl->AddText(textPos, IM_COL32(210, 210, 220, 220), obj.ui.label.c_str()); + } + } else { + ImGui::SetCursorPos(ImVec2(min.x - overlayPos.x, min.y - overlayPos.y)); + if (texId != 0) { + ImGui::Image((ImTextureID)(intptr_t)texId, size, ImVec2(0, 1), ImVec2(1, 0), tint, ImVec4(0, 0, 0, 0)); + } else { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImU32 fill = ImGui::GetColorU32(tint); + ImU32 border = ImGui::GetColorU32(brighten(tint, 0.85f)); + dl->AddRectFilled(min, max, fill, 6.0f); + dl->AddRect(min, max, border, 6.0f); + ImVec2 textSize = ImGui::CalcTextSize(obj.ui.label.c_str()); + ImVec2 textPos(min.x + (size.x - textSize.x) * 0.5f, + min.y + (size.y - textSize.y) * 0.5f); + dl->AddText(textPos, IM_COL32(210, 210, 220, 220), obj.ui.label.c_str()); + } + } + ImGui::Dummy(size); + }; + + if (repeatX || repeatY) { + int startX = repeatX ? static_cast(std::floor((worldViewMin.x - baseWorldMin.x) / stepX)) - 1 : 0; + int endX = repeatX ? static_cast(std::ceil((worldViewMax.x - baseWorldMin.x) / stepX)) + 1 : 0; + int startY = repeatY ? static_cast(std::floor((worldViewMin.y - baseWorldMin.y) / stepY)) - 1 : 0; + int endY = repeatY ? static_cast(std::ceil((worldViewMax.y - baseWorldMin.y) / stepY)) + 1 : 0; + for (int ix = startX; ix <= endX; ++ix) { + for (int iy = startY; iy <= endY; ++iy) { + float dx = repeatX ? (float)ix * stepX : 0.0f; + float dy = repeatY ? (float)iy * stepY : 0.0f; + glm::vec2 tileMin = baseWorldMin + glm::vec2(dx, dy); + ImVec2 s0 = worldToScreen(tileMin); + ImVec2 s1 = worldToScreen(tileMin + glm::vec2(obj.ui.size.x, obj.ui.size.y)); + ImVec2 tMin(std::min(s0.x, s1.x), std::min(s0.y, s1.y)); + ImVec2 tMax(std::max(s0.x, s1.x), std::max(s0.y, s1.y)); + drawImageRect(tMin, tMax); + } + } + } else if (std::abs(angle) > 1e-4f) { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImVec2 center = ImVec2((drawMin.x + drawMax.x) * 0.5f, (drawMin.y + drawMax.y) * 0.5f); + ImVec2 half = ImVec2(drawSize.x * 0.5f, drawSize.y * 0.5f); + float c = std::cos(angle); + float s = std::sin(angle); + auto rotPt = [&](float x, float y) { + return ImVec2(center.x + x * c - y * s, center.y + x * s + y * c); + }; + ImVec2 p0 = rotPt(-half.x, -half.y); + ImVec2 p1 = rotPt( half.x, -half.y); + ImVec2 p2 = rotPt( half.x, half.y); + ImVec2 p3 = rotPt(-half.x, half.y); + if (texId != 0) { + dl->AddImageQuad((ImTextureID)(intptr_t)texId, p0, p1, p2, p3, + ImVec2(0, 1), ImVec2(1, 1), ImVec2(1, 0), ImVec2(0, 0), + ImGui::GetColorU32(tint)); + } else { + ImU32 fill = ImGui::GetColorU32(tint); + ImU32 border = ImGui::GetColorU32(brighten(tint, 0.85f)); + dl->AddQuadFilled(p0, p1, p2, p3, fill); + dl->AddQuad(p0, p1, p2, p3, border, 2.0f); + ImVec2 textSize = ImGui::CalcTextSize(obj.ui.label.c_str()); + ImVec2 textPos(center.x - textSize.x * 0.5f, center.y - textSize.y * 0.5f); + dl->AddText(textPos, IM_COL32(210, 210, 220, 220), obj.ui.label.c_str()); + } + ImGui::Dummy(drawSize); + } else { + ImGui::SetCursorPos(localMin); + if (texId != 0) { + ImGui::Image((ImTextureID)(intptr_t)texId, drawSize, ImVec2(0, 1), ImVec2(1, 0), tint, ImVec4(0, 0, 0, 0)); + } else { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImU32 fill = ImGui::GetColorU32(tint); + ImU32 border = ImGui::GetColorU32(brighten(tint, 0.85f)); + dl->AddRectFilled(drawMin, drawMax, fill, 6.0f); + dl->AddRect(drawMin, drawMax, border, 6.0f); + ImVec2 textSize = ImGui::CalcTextSize(obj.ui.label.c_str()); + ImVec2 textPos(drawMin.x + (drawSize.x - textSize.x) * 0.5f, drawMin.y + (drawSize.y - textSize.y) * 0.5f); + dl->AddText(textPos, IM_COL32(210, 210, 220, 220), obj.ui.label.c_str()); + ImGui::Dummy(drawSize); + } + } + } else if (obj.ui.type == UIElementType::Slider) { ImGui::SetCursorPos(localMin); ImVec4 tint(obj.ui.color.r, obj.ui.color.g, obj.ui.color.b, obj.ui.color.a); if (obj.ui.sliderStyle == UISliderStyle::ImGui) { - ImGui::PushItemWidth(clippedSize.x); - ImGui::BeginDisabled(!obj.ui.interactable); + ImGui::PushItemWidth(drawSize.x); + ImGui::BeginDisabled(!obj.ui.interactable || uiWorldCameraActive); ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(tint.x * 0.2f, tint.y * 0.2f, tint.z * 0.2f, tint.w * 0.6f)); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, brighten(tint, 0.5f)); ImGui::PushStyleColor(ImGuiCol_FrameBgActive, brighten(tint, 0.7f)); @@ -608,14 +917,11 @@ void Engine::renderGameViewportWindow() { float maxValue = obj.ui.sliderMax; float range = (maxValue - minValue); if (range <= 1e-6f) range = 1.0f; - float t = (obj.ui.sliderValue - minValue) / range; - t = std::clamp(t, 0.0f, 1.0f); - - ImGui::BeginDisabled(!obj.ui.interactable); - ImGui::InvisibleButton("##UISlider", clippedSize); + ImGui::BeginDisabled(!obj.ui.interactable || uiWorldCameraActive); + ImGui::InvisibleButton("##UISlider", drawSize); bool held = obj.ui.interactable && ImGui::IsItemActive(); - if (held && ImGui::IsMouseDown(ImGuiMouseButton_Left) && clippedSize.x > 1.0f) { - float mouseT = (ImGui::GetIO().MousePos.x - clippedMin.x) / clippedSize.x; + if (held && ImGui::IsMouseDown(ImGuiMouseButton_Left) && drawSize.x > 1.0f) { + float mouseT = (ImGui::GetIO().MousePos.x - drawMin.x) / drawSize.x; mouseT = std::clamp(mouseT, 0.0f, 1.0f); float newValue = minValue + mouseT * range; if (newValue != obj.ui.sliderValue) { @@ -625,21 +931,26 @@ void Engine::renderGameViewportWindow() { } ImGui::EndDisabled(); + animateValue(animState.sliderValue, obj.ui.sliderValue, held); + float displayValue = (uiAnimationMode == UIAnimationMode::Off) ? obj.ui.sliderValue : animState.sliderValue; + float t = (displayValue - minValue) / range; + t = std::clamp(t, 0.0f, 1.0f); + if (obj.ui.sliderStyle == UISliderStyle::Fill) { float rounding = 6.0f; - ImVec2 fillMax(clippedMin.x + clippedSize.x * t, clippedMax.y); - dl->AddRectFilled(clippedMin, clippedMax, bg, rounding); - if (fillMax.x > clippedMin.x) { - dl->AddRectFilled(clippedMin, fillMax, fill, rounding); + ImVec2 fillMax(drawMin.x + drawSize.x * t, drawMax.y); + dl->AddRectFilled(drawMin, drawMax, bg, rounding); + if (fillMax.x > drawMin.x) { + dl->AddRectFilled(drawMin, fillMax, fill, rounding); } - dl->AddRect(clippedMin, clippedMax, border, rounding); + dl->AddRect(drawMin, drawMax, border, rounding); ImVec2 textSize = ImGui::CalcTextSize(obj.ui.label.c_str()); - ImVec2 textPos(clippedMin.x + (clippedSize.x - textSize.x) * 0.5f, - clippedMin.y + (clippedSize.y - textSize.y) * 0.5f); + ImVec2 textPos(drawMin.x + (drawSize.x - textSize.x) * 0.5f, + drawMin.y + (drawSize.y - textSize.y) * 0.5f); dl->AddText(textPos, IM_COL32(240, 240, 245, 220), obj.ui.label.c_str()); } else if (obj.ui.sliderStyle == UISliderStyle::Circle) { - ImVec2 center((clippedMin.x + clippedMax.x) * 0.5f, (clippedMin.y + clippedMax.y) * 0.5f); - float radius = std::max(2.0f, std::min(clippedSize.x, clippedSize.y) * 0.5f - 2.0f); + ImVec2 center((drawMin.x + drawMax.x) * 0.5f, (drawMin.y + drawMax.y) * 0.5f); + float radius = std::max(2.0f, std::min(drawSize.x, drawSize.y) * 0.5f - 2.0f); dl->AddCircleFilled(center, radius, bg, 32); float start = -IM_PI * 0.5f; float end = start + t * IM_PI * 2.0f; @@ -653,7 +964,7 @@ void Engine::renderGameViewportWindow() { dl->AddText(textPos, IM_COL32(240, 240, 245, 220), obj.ui.label.c_str()); } } - } else if (obj.type == ObjectType::UIButton) { + } else if (obj.ui.type == UIElementType::Button) { ImGui::SetCursorPos(localMin); ImVec4 tint(obj.ui.color.r, obj.ui.color.g, obj.ui.color.b, obj.ui.color.a); obj.ui.buttonPressed = false; @@ -661,41 +972,47 @@ void Engine::renderGameViewportWindow() { ImGui::PushStyleColor(ImGuiCol_Button, tint); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, brighten(tint, 1.1f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, brighten(tint, 1.2f)); - ImGui::BeginDisabled(!obj.ui.interactable); - obj.ui.buttonPressed = ImGui::Button(obj.ui.label.c_str(), clippedSize); + ImGui::BeginDisabled(!obj.ui.interactable || uiWorldCameraActive); + obj.ui.buttonPressed = ImGui::Button(obj.ui.label.c_str(), drawSize); ImGui::EndDisabled(); ImGui::PopStyleColor(3); } else if (obj.ui.buttonStyle == UIButtonStyle::Outline) { ImDrawList* dl = ImGui::GetWindowDrawList(); ImU32 border = ImGui::GetColorU32(tint); - ImU32 fill = ImGui::GetColorU32(brighten(tint, 0.45f)); - ImGui::BeginDisabled(!obj.ui.interactable); - if (ImGui::InvisibleButton("##UIButton", clippedSize)) { + ImGui::BeginDisabled(!obj.ui.interactable || uiWorldCameraActive); + if (ImGui::InvisibleButton("##UIButton", drawSize)) { obj.ui.buttonPressed = obj.ui.interactable; } bool hovered = ImGui::IsItemHovered(); bool active = ImGui::IsItemActive(); ImGui::EndDisabled(); - if (hovered) { - dl->AddRectFilled(clippedMin, clippedMax, fill, 6.0f); + float hoverT = animateValue(animState.hover, hovered ? 1.0f : 0.0f, false); + float activeT = animateValue(animState.active, active ? 1.0f : 0.0f, false); + if (hoverT > 0.001f) { + ImVec4 hoverCol = brighten(tint, 0.45f); + hoverCol.w *= std::clamp(hoverT, 0.0f, 1.0f); + dl->AddRectFilled(drawMin, drawMax, ImGui::GetColorU32(hoverCol), 6.0f); } - if (active) { - dl->AddRectFilled(clippedMin, clippedMax, ImGui::GetColorU32(brighten(tint, 0.65f)), 6.0f); + if (activeT > 0.001f) { + ImVec4 activeCol = brighten(tint, 0.65f); + activeCol.w *= std::clamp(activeT, 0.0f, 1.0f); + dl->AddRectFilled(drawMin, drawMax, ImGui::GetColorU32(activeCol), 6.0f); } - dl->AddRect(clippedMin, clippedMax, border, 6.0f, 0, 2.0f); + dl->AddRect(drawMin, drawMax, border, 6.0f, 0, 2.0f); ImVec2 textSize = ImGui::CalcTextSize(obj.ui.label.c_str()); - ImVec2 textPos(clippedMin.x + (clippedSize.x - textSize.x) * 0.5f, - clippedMin.y + (clippedSize.y - textSize.y) * 0.5f); + ImVec2 textPos(drawMin.x + (drawSize.x - textSize.x) * 0.5f, + drawMin.y + (drawSize.y - textSize.y) * 0.5f); dl->AddText(textPos, IM_COL32(240, 240, 245, 220), obj.ui.label.c_str()); } - } else if (obj.type == ObjectType::UIText) { + } else if (obj.ui.type == UIElementType::Text) { ImDrawList* dl = ImGui::GetWindowDrawList(); ImVec4 tint(obj.ui.color.r, obj.ui.color.g, obj.ui.color.b, obj.ui.color.a); float scale = std::max(0.1f, obj.ui.textScale); - float scaleFactor = std::min(uiScaleX, uiScaleY); + float scaleFactor = useWorldUi ? std::max(0.01f, uiWorldCamera.zoom / 100.0f) + : std::min(uiScaleX, uiScaleY); float fontSize = std::max(1.0f, ImGui::GetFontSize() * scale * scaleFactor); - ImVec2 textPos = ImVec2(clippedMin.x + 4.0f, clippedMin.y + 2.0f); - ImGui::PushClipRect(clippedMin, clippedMax, true); + ImVec2 textPos = ImVec2(drawMin.x + 4.0f, drawMin.y + 2.0f); + ImGui::PushClipRect(drawMin, drawMax, true); dl->AddText(ImGui::GetFont(), fontSize, textPos, ImGui::GetColorU32(tint), obj.ui.label.c_str()); ImGui::PopClipRect(); } @@ -706,49 +1023,89 @@ void Engine::renderGameViewportWindow() { bool gizmoUsed = false; if (!isPlaying) { SceneObject* selected = getSelectedObject(); - if (selected && isUIType(selected->type) && selected->type != ObjectType::Canvas) { + if (selected && isUIType(*selected) && selected->ui.type != UIElementType::Canvas) { ImVec2 rectMin, rectMax; ImVec2 parentMin, parentMax; - resolveUIRect(*selected, rectMin, rectMax, &parentMin, &parentMax); + if (useWorldUi) { + resolveUIRectWorld(*selected, rectMin, rectMax); + } else { + resolveUIRect(*selected, rectMin, rectMax, &parentMin, &parentMax); + } ImVec2 rectSize(rectMax.x - rectMin.x, rectMax.y - rectMin.y); + ImGuizmo::OPERATION op = ImGuizmo::TRANSLATE; + if (mCurrentGizmoOperation == ImGuizmo::SCALE) { + op = ImGuizmo::SCALE; + } else if (mCurrentGizmoOperation == ImGuizmo::ROTATE) { + op = ImGuizmo::ROTATE; + } glm::mat4 view(1.0f); glm::mat4 proj = glm::ortho(0.0f, (float)(imageMax.x - imageMin.x), (float)(imageMax.y - imageMin.y), 0.0f, -1.0f, 1.0f); + glm::vec2 parentOffset = getWorldParentOffset(*selected); + glm::vec2 pivotWorld = parentOffset + glm::vec2(selected->ui.position.x, selected->ui.position.y); + ImVec2 pivotScreen; + if (useWorldUi) { + pivotScreen = worldToScreen(pivotWorld); + } else { + ImVec2 anchorPoint = anchorToPoint(selected->ui.anchor, parentMin, parentMax); + pivotScreen = ImVec2(anchorPoint.x + selected->ui.position.x * uiScaleX, + anchorPoint.y + selected->ui.position.y * uiScaleY); + } + ImVec2 rectCenter(pivotScreen.x - imageMin.x, pivotScreen.y - imageMin.y); + glm::vec3 gizmoScale(1.0f, 1.0f, 1.0f); + if (op == ImGuizmo::SCALE) { + gizmoScale = glm::vec3(rectSize.x, rectSize.y, 1.0f); + } glm::mat4 model(1.0f); - model = glm::translate(model, glm::vec3(rectMin.x - imageMin.x, rectMin.y - imageMin.y, 0.0f)); - model = glm::scale(model, glm::vec3(rectSize.x, rectSize.y, 1.0f)); + model = glm::translate(model, glm::vec3(rectCenter.x, rectCenter.y, 0.0f)); + model = glm::rotate(model, glm::radians(selected->ui.rotation), glm::vec3(0.0f, 0.0f, 1.0f)); + model = glm::scale(model, gizmoScale); ImGuizmo::BeginFrame(); ImGuizmo::Enable(true); ImGuizmo::SetOrthographic(true); ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList()); ImGuizmo::SetRect(imageMin.x, imageMin.y, imageMax.x - imageMin.x, imageMax.y - imageMin.y); - - ImGuizmo::OPERATION op = (mCurrentGizmoOperation == ImGuizmo::SCALE) ? ImGuizmo::SCALE : ImGuizmo::TRANSLATE; glm::mat4 delta(1.0f); ImGuizmo::Manipulate(glm::value_ptr(view), glm::value_ptr(proj), op, ImGuizmo::LOCAL, glm::value_ptr(model), glm::value_ptr(delta)); if (ImGuizmo::IsUsing()) { glm::vec3 pos, rot, scl; DecomposeMatrix(model, pos, rot, scl); - (void)rot; - ImVec2 newMin(imageMin.x + pos.x, imageMin.y + pos.y); - ImVec2 newSize(std::max(1.0f, scl.x), std::max(1.0f, scl.y)); - ImVec2 anchorPoint = anchorToPoint(selected->ui.anchor, parentMin, parentMax); - ImVec2 pivotOffset = anchorToPivot(selected->ui.anchor, newSize); - ImVec2 pivot(newMin.x + pivotOffset.x, newMin.y + pivotOffset.y); - float invScaleX = (uiScaleX > 0.0f) ? 1.0f / uiScaleX : 1.0f; - float invScaleY = (uiScaleY > 0.0f) ? 1.0f / uiScaleY : 1.0f; - selected->ui.position = glm::vec2((pivot.x - anchorPoint.x) * invScaleX, - (pivot.y - anchorPoint.y) * invScaleY); - selected->ui.size = glm::vec2(newSize.x * invScaleX, newSize.y * invScaleY); + glm::vec3 euler = NormalizeEulerDegrees(glm::degrees(rot)); + ImVec2 newPivot(imageMin.x + pos.x, imageMin.y + pos.y); + if (op == ImGuizmo::ROTATE) { + selected->ui.rotation = euler.z; + } else if (op == ImGuizmo::TRANSLATE) { + if (useWorldUi) { + glm::vec2 worldPivot = screenToWorld(newPivot); + selected->ui.position = worldPivot - parentOffset - parallaxOffset(*selected); + } else { + ImVec2 anchorPoint = anchorToPoint(selected->ui.anchor, parentMin, parentMax); + float invScaleX = (uiScaleX > 0.0f) ? 1.0f / uiScaleX : 1.0f; + float invScaleY = (uiScaleY > 0.0f) ? 1.0f / uiScaleY : 1.0f; + selected->ui.position = glm::vec2((newPivot.x - anchorPoint.x) * invScaleX, + (newPivot.y - anchorPoint.y) * invScaleY); + } + } else if (op == ImGuizmo::SCALE) { + ImVec2 newSize(std::max(1.0f, scl.x), std::max(1.0f, scl.y)); + if (useWorldUi) { + glm::vec2 worldSize = glm::vec2(newSize.x, newSize.y) / uiWorldCamera.zoom; + selected->ui.position = pivotWorld - parentOffset - parallaxOffset(*selected); + selected->ui.size = worldSize; + } else { + float invScaleX = (uiScaleX > 0.0f) ? 1.0f / uiScaleX : 1.0f; + float invScaleY = (uiScaleY > 0.0f) ? 1.0f / uiScaleY : 1.0f; + selected->ui.size = glm::vec2(newSize.x * invScaleX, newSize.y * invScaleY); + } + } projectManager.currentProject.hasUnsavedChanges = true; gizmoUsed = true; } } } - uiInteracting = ImGui::IsAnyItemHovered() || ImGui::IsAnyItemActive() || gizmoUsed; + uiInteracting = ImGui::IsAnyItemActive() || gizmoUsed || uiWorldCameraActive; ImGui::EndChild(); ImGui::PopStyleVar(); @@ -757,11 +1114,14 @@ void Engine::renderGameViewportWindow() { if (clicked && !gameViewCursorLocked) { gameViewCursorLocked = true; } - if (gameViewCursorLocked && (!isPlaying || !windowFocused || ImGui::IsKeyPressed(ImGuiKey_Escape))) { + if (gameViewCursorLocked && (!isPlaying || ImGui::IsKeyPressed(ImGuiKey_Escape))) { gameViewCursorLocked = false; } - gameViewportFocused = windowFocused && gameViewCursorLocked; + gameViewportFocused = windowFocused || gameViewCursorLocked; + if (restoreUiWorldCamera) { + uiWorldCamera = uiWorldCameraBackup; + } } else { ImGui::TextDisabled("No player camera found (Camera Type: Player)."); gameViewportFocused = ImGui::IsWindowFocused(); @@ -818,6 +1178,17 @@ void Engine::renderPlayControlsBar() { addConsoleMessage("PhysX failed to initialize; physics disabled for play mode", ConsoleMessageType::Warning); } audio.onPlayStart(sceneObjects); + bool hasPlayerController = false; + for (const auto& obj : sceneObjects) { + if (obj.enabled && obj.hasPlayerController && obj.playerController.enabled) { + hasPlayerController = true; + break; + } + } + if (hasPlayerController && showGameViewport) { + gameViewCursorLocked = true; + gameViewportFocused = true; + } } else { physics.onPlayStop(); audio.onPlayStop(); @@ -876,6 +1247,9 @@ void Engine::renderMainMenuBar() { strncpy(saveSceneAsName, projectManager.currentProject.currentSceneName.c_str(), sizeof(saveSceneAsName) - 1); } + if (ImGui::MenuItem("Build Settings...")) { + showBuildSettings = true; + } ImGui::Separator(); if (ImGui::MenuItem("Close Project")) { if (projectManager.currentProject.hasUnsavedChanges) { @@ -886,6 +1260,10 @@ void Engine::renderMainMenuBar() { clearSelection(); scriptEditorWindows.clear(); scriptEditorWindowsDirty = true; + resetBuildSettings(); + showBuildSettings = false; + playerMode = false; + autoStartRequested = false; showLauncher = true; } ImGui::Separator(); @@ -906,11 +1284,21 @@ void Engine::renderMainMenuBar() { ImGui::MenuItem("Inspector", nullptr, &showInspector); ImGui::MenuItem("File Browser", nullptr, &showFileBrowser); ImGui::MenuItem("Console", nullptr, &showConsole); + ImGui::MenuItem("Scripting", nullptr, &showScriptingWindow); ImGui::MenuItem("Project Manager", nullptr, &showProjectBrowser); ImGui::MenuItem("Mesh Builder", nullptr, &showMeshBuilder); ImGui::MenuItem("Environment", nullptr, &showEnvironmentWindow); ImGui::MenuItem("Camera", nullptr, &showCameraWindow); + bool prevAnimationWindow = showAnimationWindow; + ImGui::MenuItem("Animation", nullptr, &showAnimationWindow); + if (prevAnimationWindow != showAnimationWindow) { + saveEditorUserSettings(); + } ImGui::MenuItem("View Output", nullptr, &showViewOutput); + ImGui::Separator(); + ImGui::MenuItem("UI World Overlay", nullptr, &uiWorldMode); + ImGui::MenuItem("UI World Grid", nullptr, &showUIWorldGrid); + ImGui::MenuItem("3D Grid", nullptr, &showSceneGrid3D); if (!scriptEditorWindows.empty()) { ImGui::Separator(); ImGui::TextDisabled("Scripted Windows"); @@ -925,6 +1313,37 @@ void Engine::renderMainMenuBar() { ImGui::EndMenu(); } + if (ImGui::BeginMenu("Style")) { + ImGui::TextDisabled("Editor Styles"); + for (size_t i = 0; i < uiStylePresets.size(); ++i) { + bool selected = static_cast(i) == uiStylePresetIndex; + if (ImGui::MenuItem(uiStylePresets[i].name.c_str(), nullptr, selected)) { + applyUIStylePresetByName(uiStylePresets[i].name); + saveEditorUserSettings(); + } + } + ImGui::Separator(); + ImGui::TextDisabled("UI Animations"); + if (ImGui::MenuItem("Fluid", nullptr, uiAnimationMode == UIAnimationMode::Fluid)) { + uiAnimationMode = UIAnimationMode::Fluid; + saveEditorUserSettings(); + } + if (ImGui::MenuItem("Snappy", nullptr, uiAnimationMode == UIAnimationMode::Snappy)) { + uiAnimationMode = UIAnimationMode::Snappy; + saveEditorUserSettings(); + } + if (ImGui::MenuItem("Off", nullptr, uiAnimationMode == UIAnimationMode::Off)) { + uiAnimationMode = UIAnimationMode::Off; + saveEditorUserSettings(); + } + ImGui::Separator(); + ImGui::MenuItem("Style Editor", nullptr, &showStyleEditor); + if (ImGui::MenuItem("Export Theme + Layout")) { + exportEditorThemeLayout(); + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Scripts")) { auto toggleSpec = [&](bool enabled) { if (specMode == enabled) return; @@ -948,6 +1367,7 @@ void Engine::renderMainMenuBar() { } if (ImGui::BeginMenu("Create")) { + if (ImGui::MenuItem("Empty")) addObject(ObjectType::Empty, "Empty"); if (ImGui::MenuItem("Cube")) addObject(ObjectType::Cube, "Cube"); if (ImGui::MenuItem("Sphere")) addObject(ObjectType::Sphere, "Sphere"); if (ImGui::MenuItem("Capsule")) addObject(ObjectType::Capsule, "Capsule"); @@ -960,17 +1380,50 @@ void Engine::renderMainMenuBar() { if (ImGui::MenuItem("Spot Light")) addObject(ObjectType::SpotLight, "Spot Light"); if (ImGui::MenuItem("Area Light")) addObject(ObjectType::AreaLight, "Area Light"); if (ImGui::MenuItem("Post FX Node")) addObject(ObjectType::PostFXNode, "Post FX"); + if (ImGui::MenuItem("Audio Reverb Zone")) { + addObject(ObjectType::Empty, "Reverb Zone"); + if (!sceneObjects.empty()) { + sceneObjects.back().hasReverbZone = true; + sceneObjects.back().reverbZone = ReverbZoneComponent{}; + sceneObjects.back().reverbZone.boxSize = glm::max(sceneObjects.back().scale, glm::vec3(1.0f)); + } + } ImGui::EndMenu(); } if (ImGui::BeginMenu("Help")) { if (ImGui::MenuItem("About")) { - logToConsole("Modularity Engine - Beta V1.0\nThis build is in beta and might have issues,\n\nif you'd like to report any bugs or missing features, feel free to contact us!"); + logToConsole("Modularity Engine - Beta V6.3\nThis build is in beta and might have issues,\n\nif you'd like to report any bugs or missing features, feel free to contact us!"); } ImGui::EndMenu(); } ImGui::Separator(); + ImGui::TextColored(subtle, "Workspace"); + ImGui::SameLine(); + auto drawWorkspaceButton = [&](const char* label, WorkspaceMode mode) { + bool selected = (currentWorkspace == mode); + ImVec4 base = ImGui::GetStyleColorVec4(ImGuiCol_Button); + ImVec4 hover = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + ImVec4 active = ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive); + if (selected) { + base = ImVec4(accent.x * 0.9f, accent.y * 0.9f, accent.z * 0.9f, 1.0f); + hover = ImVec4(accent.x, accent.y, accent.z, 1.0f); + active = ImVec4(accent.x, accent.y, accent.z, 1.0f); + } + ImGui::PushStyleColor(ImGuiCol_Button, base); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, hover); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, active); + if (ImGui::Button(label)) { + applyWorkspacePreset(mode, true); + saveEditorUserSettings(); + } + ImGui::PopStyleColor(3); + ImGui::SameLine(); + }; + drawWorkspaceButton("Default", WorkspaceMode::Default); + drawWorkspaceButton("Animation", WorkspaceMode::Animation); + drawWorkspaceButton("Scripting", WorkspaceMode::Scripting); ImGui::TextColored(subtle, "Project"); ImGui::SameLine(); std::string projectLabel = projectManager.currentProject.name.empty() ? @@ -996,6 +1449,129 @@ void Engine::renderMainMenuBar() { ImGui::PopStyleVar(2); ImGui::EndMainMenuBar(); } + + if (workspaceLayoutDirty) { + buildWorkspaceLayout(currentWorkspace); + } + + if (showStyleEditor) { + if (ImGui::Begin("Style Editor", &showStyleEditor)) { + if (ImGui::Button("Save Colors")) { + saveEditorUserSettings(); + } + ImGui::SameLine(); + if (ImGui::Button("Export Theme + Layout")) { + exportEditorThemeLayout(); + } + ImGui::SameLine(); + ImGui::TextDisabled("Applies to all presets"); + ImGui::Separator(); + ImGuiStyle& style = ImGui::GetStyle(); + ImGui::ShowStyleEditor(&style); + } + ImGui::End(); + } +} + +void Engine::applyWorkspacePreset(WorkspaceMode mode, bool rebuildLayout) { + currentWorkspace = mode; + switch (mode) { + case WorkspaceMode::Default: + showHierarchy = true; + showInspector = true; + showFileBrowser = true; + showConsole = true; + showScriptingWindow = false; + showAnimationWindow = false; + showEnvironmentWindow = true; + showCameraWindow = true; + showGameViewport = true; + break; + case WorkspaceMode::Animation: + showHierarchy = true; + showInspector = true; + showFileBrowser = false; + showConsole = true; + showScriptingWindow = false; + showAnimationWindow = true; + showEnvironmentWindow = false; + showCameraWindow = false; + showGameViewport = true; + break; + case WorkspaceMode::Scripting: + showHierarchy = true; + showInspector = true; + showFileBrowser = true; + showConsole = true; + showScriptingWindow = true; + showAnimationWindow = false; + showEnvironmentWindow = false; + showCameraWindow = false; + showGameViewport = true; + break; + } + + fs::path layoutPath = getWorkspaceLayoutPath(mode); + if (!layoutPath.empty() && fs::exists(layoutPath)) { + pendingWorkspaceIniPath = layoutPath; + pendingWorkspaceReload = true; + workspaceLayoutDirty = false; + return; + } + + if (rebuildLayout) { + buildWorkspaceLayout(mode); + } + workspaceLayoutDirty = false; +} + +void Engine::buildWorkspaceLayout(WorkspaceMode mode) { + ImGuiID dockspaceId = ImGui::GetID("MainDockspace"); + ImGuiViewport* viewport = ImGui::GetMainViewport(); + + ImGui::DockBuilderRemoveNode(dockspaceId); + ImGui::DockBuilderAddNode(dockspaceId, ImGuiDockNodeFlags_DockSpace); + ImGui::DockBuilderSetNodeSize(dockspaceId, viewport->WorkSize); + + ImGuiID dockMain = dockspaceId; + if (mode == WorkspaceMode::Default) { + ImGuiID dockLeft = ImGui::DockBuilderSplitNode(dockMain, ImGuiDir_Left, 0.20f, nullptr, &dockMain); + ImGuiID dockRight = ImGui::DockBuilderSplitNode(dockMain, ImGuiDir_Right, 0.26f, nullptr, &dockMain); + ImGuiID dockBottom = ImGui::DockBuilderSplitNode(dockMain, ImGuiDir_Down, 0.28f, nullptr, &dockMain); + + ImGui::DockBuilderDockWindow("Hierarchy", dockLeft); + ImGui::DockBuilderDockWindow("Inspector", dockRight); + ImGui::DockBuilderDockWindow("Project", dockBottom); + ImGui::DockBuilderDockWindow("Console", dockBottom); + ImGui::DockBuilderDockWindow("Viewport", dockMain); + ImGui::DockBuilderDockWindow("Game Viewport", dockMain); + } else if (mode == WorkspaceMode::Animation) { + ImGuiID dockLeft = ImGui::DockBuilderSplitNode(dockMain, ImGuiDir_Left, 0.20f, nullptr, &dockMain); + ImGuiID dockRight = ImGui::DockBuilderSplitNode(dockMain, ImGuiDir_Right, 0.25f, nullptr, &dockMain); + ImGuiID dockBottom = ImGui::DockBuilderSplitNode(dockMain, ImGuiDir_Down, 0.35f, nullptr, &dockMain); + + ImGui::DockBuilderDockWindow("Hierarchy", dockLeft); + ImGui::DockBuilderDockWindow("Inspector", dockRight); + ImGui::DockBuilderDockWindow("Animation", dockBottom); + ImGui::DockBuilderDockWindow("Console", dockBottom); + ImGui::DockBuilderDockWindow("Project", dockBottom); + ImGui::DockBuilderDockWindow("Viewport", dockMain); + } else { + ImGuiID dockLeft = ImGui::DockBuilderSplitNode(dockMain, ImGuiDir_Left, 0.25f, nullptr, &dockMain); + ImGuiID dockRight = ImGui::DockBuilderSplitNode(dockMain, ImGuiDir_Right, 0.35f, nullptr, &dockMain); + ImGuiID dockBottom = ImGui::DockBuilderSplitNode(dockMain, ImGuiDir_Down, 0.25f, nullptr, &dockMain); + + ImGui::DockBuilderDockWindow("Project", dockLeft); + ImGui::DockBuilderDockWindow("Hierarchy", dockLeft); + ImGui::DockBuilderDockWindow("Scripting", dockRight); + ImGui::DockBuilderDockWindow("Inspector", dockRight); + ImGui::DockBuilderDockWindow("Console", dockBottom); + ImGui::DockBuilderDockWindow("Viewport", dockMain); + ImGui::DockBuilderDockWindow("Game Viewport", dockMain); + } + + ImGui::DockBuilderFinish(dockspaceId); + workspaceLayoutDirty = false; } #pragma endregion @@ -1054,6 +1630,105 @@ void Engine::renderViewport() { mouseOverViewportImage = ImGui::IsItemHovered(); ImDrawList* viewportDrawList = ImGui::GetWindowDrawList(); + if (uiWorldMode) { + viewportDrawList->AddRectFilled(imageMin, imageMax, IM_COL32(14, 16, 20, 255)); + } else if (showSceneGrid3D) { + auto projectToScreen = [&](const glm::vec3& p) -> std::optional { + glm::vec4 clip = proj * view * glm::vec4(p, 1.0f); + if (clip.w <= 0.0f) return std::nullopt; + glm::vec3 ndc = glm::vec3(clip) / clip.w; + ImVec2 screen; + screen.x = imageMin.x + (ndc.x * 0.5f + 0.5f) * (imageMax.x - imageMin.x); + screen.y = imageMin.y + (1.0f - (ndc.y * 0.5f + 0.5f)) * (imageMax.y - imageMin.y); + return screen; + }; + auto clipLineToScreen = [&](glm::vec3 a, glm::vec3 b, ImVec2& outA, ImVec2& outB) -> bool { + glm::vec4 va = view * glm::vec4(a, 1.0f); + glm::vec4 vb = view * glm::vec4(b, 1.0f); + const float nearZ = -NEAR_PLANE; + if (va.z > nearZ && vb.z > nearZ) { + return false; + } + if (va.z > nearZ || vb.z > nearZ) { + float t = (nearZ - va.z) / (vb.z - va.z); + t = std::clamp(t, 0.0f, 1.0f); + glm::vec4 vclip = va + (vb - va) * t; + if (va.z > nearZ) { + va = vclip; + } else { + vb = vclip; + } + } + glm::vec4 ca = proj * va; + glm::vec4 cb = proj * vb; + if (ca.w <= 0.0f || cb.w <= 0.0f) return false; + glm::vec3 ndcA = glm::vec3(ca) / ca.w; + glm::vec3 ndcB = glm::vec3(cb) / cb.w; + outA = ImVec2( + imageMin.x + (ndcA.x * 0.5f + 0.5f) * (imageMax.x - imageMin.x), + imageMin.y + (1.0f - (ndcA.y * 0.5f + 0.5f)) * (imageMax.y - imageMin.y) + ); + outB = ImVec2( + imageMin.x + (ndcB.x * 0.5f + 0.5f) * (imageMax.x - imageMin.x), + imageMin.y + (1.0f - (ndcB.y * 0.5f + 0.5f)) * (imageMax.y - imageMin.y) + ); + return true; + }; + glm::vec2 camXZ(camera.position.x, camera.position.z); + float camDist = glm::length(camXZ); + float extent = 60.0f + camDist * 0.5f + std::abs(camera.position.y) * 4.0f; + extent = std::clamp(extent, 60.0f, 1200.0f); + float step = 1.0f; + if (extent > 400.0f) { + step = 20.0f; + } else if (extent > 200.0f) { + step = 10.0f; + } else if (extent > 120.0f) { + step = 5.0f; + } else if (extent > 70.0f) { + step = 2.0f; + } + float gridStrength = std::clamp(camDist / 120.0f, 0.15f, 1.0f); + ImVec4 baseCol(0.35f, 0.43f, 0.55f, 0.55f * gridStrength); + ImVec4 axisXCol(0.94f, 0.45f, 0.45f, 0.9f); + ImVec4 axisZCol(0.5f, 0.7f, 0.95f, 0.9f); + + float startX = std::floor((camera.position.x - extent) / step) * step; + float endX = std::floor((camera.position.x + extent) / step) * step; + for (float x = startX; x <= endX; x += step) { + float t = 1.0f - std::min(1.0f, std::abs(x - camera.position.x) / extent); + ImVec4 col = baseCol; + col.w *= t; + if (col.w < 0.02f) continue; + ImVec2 s0, s1; + if (clipLineToScreen(glm::vec3(x, 0.0f, camera.position.z - extent), + glm::vec3(x, 0.0f, camera.position.z + extent), s0, s1)) { + viewportDrawList->AddLine(s0, s1, ImGui::GetColorU32(col), 1.0f); + } + } + float startZ = std::floor((camera.position.z - extent) / step) * step; + float endZ = std::floor((camera.position.z + extent) / step) * step; + for (float z = startZ; z <= endZ; z += step) { + float t = 1.0f - std::min(1.0f, std::abs(z - camera.position.z) / extent); + ImVec4 col = baseCol; + col.w *= t; + if (col.w < 0.02f) continue; + ImVec2 s0, s1; + if (clipLineToScreen(glm::vec3(camera.position.x - extent, 0.0f, z), + glm::vec3(camera.position.x + extent, 0.0f, z), s0, s1)) { + viewportDrawList->AddLine(s0, s1, ImGui::GetColorU32(col), 1.0f); + } + } + ImVec2 ax0, ax1; + if (clipLineToScreen(glm::vec3(-extent, 0.0f, 0.0f), glm::vec3(extent, 0.0f, 0.0f), ax0, ax1)) { + viewportDrawList->AddLine(ax0, ax1, ImGui::GetColorU32(axisXCol), 2.0f); + } + ImVec2 az0, az1; + if (clipLineToScreen(glm::vec3(0.0f, 0.0f, -extent), glm::vec3(0.0f, 0.0f, extent), az0, az1)) { + viewportDrawList->AddLine(az0, az1, ImGui::GetColorU32(axisZCol), 2.0f); + } + } + auto importDroppedModel = [&](const fs::path& path) { std::error_code ec; fs::directory_entry entry(path, ec); @@ -1097,7 +1772,7 @@ void Engine::renderViewport() { }; // Draw small axis widget in top-right of viewport - { + if (!uiWorldMode) { const float widgetSize = 94.0f; const float padding = 12.0f; ImVec2 center = ImVec2( @@ -1172,6 +1847,656 @@ void Engine::renderViewport() { } } + bool showViewportToolbar = !gameViewportFocused && !(isPlaying && showGameViewport); + const float toolbarWidthEstimate = 520.0f; + const float toolbarHeightEstimate = 42.0f; + static ImVec2 toolbarSizeCache(toolbarWidthEstimate, toolbarHeightEstimate); + ImVec2 toolbarRectMin(imageMin.x, imageMin.y); + ImVec2 toolbarRectMax(imageMin.x, imageMin.y); + auto computeToolbarRect = [&]() { + ImVec2 desiredBottomLeft = ImVec2(imageMin.x + 12.0f, imageMax.y - 12.0f); + float minX = imageMin.x + 12.0f; + float maxX = imageMax.x - 12.0f; + float toolbarLeft = desiredBottomLeft.x; + if (toolbarLeft + toolbarSizeCache.x > maxX) toolbarLeft = maxX - toolbarSizeCache.x; + if (toolbarLeft < minX) toolbarLeft = minX; + float minY = imageMin.y + 12.0f; + float toolbarTop = desiredBottomLeft.y - toolbarSizeCache.y; + if (toolbarTop < minY) toolbarTop = minY; + toolbarRectMin = ImVec2(toolbarLeft, toolbarTop); + toolbarRectMax = ImVec2(toolbarLeft + toolbarSizeCache.x, toolbarTop + toolbarSizeCache.y); + }; + if (showViewportToolbar) { + computeToolbarRect(); + } else { + toolbarRectMin = imageMin; + toolbarRectMax = imageMin; + } + + bool uiWorldCameraActive = false; + if (uiWorldMode) { + auto isUIType = [](const SceneObject& target) { + return target.hasUI && target.ui.type != UIElementType::None; + }; + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + ImGui::SetCursorScreenPos(imageMin); + ImGui::BeginChild("SceneUIWorldOverlay", + ImVec2(imageMax.x - imageMin.x, imageMax.y - imageMin.y), + false, + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoBackground); + + ImVec2 overlayPos = ImGui::GetWindowPos(); + ImVec2 overlaySize = ImGui::GetWindowSize(); + uiWorldCamera.viewportSize = glm::vec2(overlaySize.x, overlaySize.y); + ImVec2 mousePos = ImGui::GetIO().MousePos; + bool mouseInToolbar = (mousePos.x >= toolbarRectMin.x && mousePos.x <= toolbarRectMax.x && + mousePos.y >= toolbarRectMin.y && mousePos.y <= toolbarRectMax.y); + bool uiWorldHover = (mouseOverViewportImage || ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) && !mouseInToolbar; + auto worldToScreen = [&](const glm::vec2& world) { + glm::vec2 local = uiWorldCamera.WorldToScreen(world); + return ImVec2(overlayPos.x + local.x, overlayPos.y + local.y); + }; + auto screenToWorld = [&](const ImVec2& screen) { + glm::vec2 local(screen.x - overlayPos.x, screen.y - overlayPos.y); + return uiWorldCamera.ScreenToWorld(local); + }; + auto getWorldParentOffset = [&](const SceneObject& obj) { + glm::vec2 offset(0.0f); + const SceneObject* current = &obj; + while (current && current->parentId >= 0) { + auto pit = std::find_if(sceneObjects.begin(), sceneObjects.end(), + [&](const SceneObject& o) { return o.id == current->parentId; }); + if (pit == sceneObjects.end()) break; + current = &(*pit); + if (current->hasUI && current->ui.type != UIElementType::None) { + offset += glm::vec2(current->ui.position.x, current->ui.position.y); + } + } + return offset; + }; + auto parallaxOffset = [&](const SceneObject& obj) { + if (!obj.hasParallaxLayer2D || !obj.parallaxLayer2D.enabled) return glm::vec2(0.0f); + float factor = std::clamp(obj.parallaxLayer2D.factor, 0.0f, 1.0f); + return uiWorldCamera.position * (1.0f - factor); + }; + auto resolveUIRectWorld = [&](const SceneObject& obj, ImVec2& outMin, ImVec2& outMax) { + glm::vec2 parentOffset = getWorldParentOffset(obj); + glm::vec2 worldPos = parentOffset + glm::vec2(obj.ui.position.x, obj.ui.position.y) + parallaxOffset(obj); + glm::vec2 sizeWorld(obj.ui.size.x, obj.ui.size.y); + ImVec2 pivotOffset = ImVec2(sizeWorld.x * 0.5f, sizeWorld.y * 0.5f); + switch (obj.ui.anchor) { + case UIAnchor::TopLeft: pivotOffset = ImVec2(0.0f, 0.0f); break; + case UIAnchor::TopRight: pivotOffset = ImVec2(sizeWorld.x, 0.0f); break; + case UIAnchor::BottomLeft: pivotOffset = ImVec2(0.0f, sizeWorld.y); break; + case UIAnchor::BottomRight: pivotOffset = ImVec2(sizeWorld.x, sizeWorld.y); break; + default: break; + } + glm::vec2 worldMin = worldPos - glm::vec2(pivotOffset.x, pivotOffset.y); + glm::vec2 worldMax = worldMin + sizeWorld; + ImVec2 s0 = worldToScreen(worldMin); + ImVec2 s1 = worldToScreen(worldMax); + outMin = ImVec2(std::min(s0.x, s1.x), std::min(s0.y, s1.y)); + outMax = ImVec2(std::max(s0.x, s1.x), std::max(s0.y, s1.y)); + }; + auto rectOutsideOverlay = [&](const ImVec2& min, const ImVec2& max) { + return (max.x < overlayPos.x || min.x > overlayPos.x + overlaySize.x || + max.y < overlayPos.y || min.y > overlayPos.y + overlaySize.y); + }; + + if (uiWorldHover) { + ImGuiIO& io = ImGui::GetIO(); + bool panHeld = ImGui::IsMouseDown(ImGuiMouseButton_Middle) || + (ImGui::IsKeyDown(ImGuiKey_Space) && ImGui::IsMouseDown(ImGuiMouseButton_Left)); + if (panHeld) { + uiWorldPanning = true; + } else if (!ImGui::IsMouseDown(ImGuiMouseButton_Middle) && + !(ImGui::IsKeyDown(ImGuiKey_Space) && ImGui::IsMouseDown(ImGuiMouseButton_Left))) { + uiWorldPanning = false; + } + if (uiWorldPanning) { + ImVec2 delta = io.MouseDelta; + if (delta.x != 0.0f || delta.y != 0.0f) { + uiWorldCamera.position.x -= delta.x / uiWorldCamera.zoom; + uiWorldCamera.position.y += delta.y / uiWorldCamera.zoom; + } + uiWorldCameraActive = true; + } + if (io.MouseWheel != 0.0f) { + glm::vec2 mouseLocal(io.MousePos.x - overlayPos.x, io.MousePos.y - overlayPos.y); + glm::vec2 worldBefore = uiWorldCamera.ScreenToWorld(mouseLocal); + float zoomFactor = 1.0f + io.MouseWheel * 0.1f; + float newZoom = std::clamp(uiWorldCamera.zoom * zoomFactor, 5.0f, 2000.0f); + if (newZoom != uiWorldCamera.zoom) { + uiWorldCamera.zoom = newZoom; + glm::vec2 worldAfter = uiWorldCamera.ScreenToWorld(mouseLocal); + uiWorldCamera.position += (worldBefore - worldAfter); + uiWorldCameraActive = true; + } + } + glm::vec2 panDir(0.0f); + if (ImGui::IsKeyDown(ImGuiKey_A)) panDir.x -= 1.0f; + if (ImGui::IsKeyDown(ImGuiKey_D)) panDir.x += 1.0f; + if (ImGui::IsKeyDown(ImGuiKey_W)) panDir.y += 1.0f; + if (ImGui::IsKeyDown(ImGuiKey_S)) panDir.y -= 1.0f; + if (panDir.x != 0.0f || panDir.y != 0.0f) { + float panSpeed = 6.0f; + if (ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) { + panSpeed *= 2.5f; + } + uiWorldCamera.position += panDir * (panSpeed * deltaTime); + uiWorldCameraActive = true; + } + } + + auto brighten = [](const ImVec4& c, float k) { + return ImVec4(std::clamp(c.x * k, 0.0f, 1.0f), + std::clamp(c.y * k, 0.0f, 1.0f), + std::clamp(c.z * k, 0.0f, 1.0f), + c.w); + }; + + if (showUIWorldGrid) { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImVec2 overlayMax(overlayPos.x + overlaySize.x, overlayPos.y + overlaySize.y); + if (showViewportToolbar && toolbarRectMin.y > overlayPos.y) { + overlayMax.y = std::min(overlayMax.y, toolbarRectMin.y - 2.0f); + } + dl->PushClipRect(overlayPos, overlayMax, true); + float step = 1.0f; + float minPx = 30.0f; + float maxPx = 140.0f; + while (step * uiWorldCamera.zoom < minPx) step *= 2.0f; + while (step * uiWorldCamera.zoom > maxPx) step *= 0.5f; + + glm::vec2 worldMin = uiWorldCamera.ScreenToWorld(glm::vec2(0.0f, overlaySize.y)); + glm::vec2 worldMax = uiWorldCamera.ScreenToWorld(glm::vec2(overlaySize.x, 0.0f)); + float startX = std::floor(worldMin.x / step) * step; + float endX = std::ceil(worldMax.x / step) * step; + float startY = std::floor(worldMin.y / step) * step; + float endY = std::ceil(worldMax.y / step) * step; + ImU32 gridColor = IM_COL32(90, 110, 140, 50); + ImU32 axisColorX = IM_COL32(240, 120, 120, 170); + ImU32 axisColorY = IM_COL32(120, 240, 150, 170); + + for (float x = startX; x <= endX; x += step) { + ImVec2 p0 = worldToScreen(glm::vec2(x, worldMin.y)); + ImVec2 p1 = worldToScreen(glm::vec2(x, worldMax.y)); + dl->AddLine(p0, p1, gridColor, 1.0f); + } + for (float y = startY; y <= endY; y += step) { + ImVec2 p0 = worldToScreen(glm::vec2(worldMin.x, y)); + ImVec2 p1 = worldToScreen(glm::vec2(worldMax.x, y)); + dl->AddLine(p0, p1, gridColor, 1.0f); + } + + ImVec2 axisX0 = worldToScreen(glm::vec2(worldMin.x, 0.0f)); + ImVec2 axisX1 = worldToScreen(glm::vec2(worldMax.x, 0.0f)); + ImVec2 axisY0 = worldToScreen(glm::vec2(0.0f, worldMin.y)); + ImVec2 axisY1 = worldToScreen(glm::vec2(0.0f, worldMax.y)); + dl->AddLine(axisX0, axisX1, axisColorX, 2.0f); + dl->AddLine(axisY0, axisY1, axisColorY, 2.0f); + + ImVec2 indicator = ImVec2(overlayPos.x + 36.0f, overlayPos.y + overlaySize.y - 36.0f); + dl->AddLine(indicator, ImVec2(indicator.x + 22.0f, indicator.y), axisColorX, 2.0f); + dl->AddLine(indicator, ImVec2(indicator.x, indicator.y - 22.0f), axisColorY, 2.0f); + dl->AddText(ImVec2(indicator.x + 26.0f, indicator.y - 8.0f), axisColorX, "+X"); + dl->AddText(ImVec2(indicator.x - 16.0f, indicator.y - 30.0f), axisColorY, "+Y"); + dl->PopClipRect(); + } + + float animSpeed = 0.0f; + if (uiAnimationMode == UIAnimationMode::Fluid) { + animSpeed = 8.0f; + } else if (uiAnimationMode == UIAnimationMode::Snappy) { + animSpeed = 18.0f; + } + float animStep = (uiAnimationMode == UIAnimationMode::Off) ? 1.0f + : (1.0f - std::exp(-animSpeed * ImGui::GetIO().DeltaTime)); + auto animateValue = [&](float& current, float target, bool immediate) { + if (uiAnimationMode == UIAnimationMode::Off || immediate) { + current = target; + } else { + current += (target - current) * animStep; + } + return current; + }; + + std::vector uiDrawList; + uiDrawList.reserve(sceneObjects.size()); + for (auto& obj : sceneObjects) { + if (!obj.enabled || !isUIType(obj)) continue; + uiDrawList.push_back(&obj); + } + if (uiWorldMode) { + std::stable_sort(uiDrawList.begin(), uiDrawList.end(), + [](const SceneObject* a, const SceneObject* b) { + int orderA = (a->hasParallaxLayer2D && a->parallaxLayer2D.enabled) ? a->parallaxLayer2D.order : 0; + int orderB = (b->hasParallaxLayer2D && b->parallaxLayer2D.enabled) ? b->parallaxLayer2D.order : 0; + return orderA < orderB; + }); + } + + glm::vec2 worldViewMin = uiWorldCamera.ScreenToWorld(glm::vec2(0.0f, overlaySize.y)); + glm::vec2 worldViewMax = uiWorldCamera.ScreenToWorld(glm::vec2(overlaySize.x, 0.0f)); + + for (SceneObject* objPtr : uiDrawList) { + SceneObject& obj = *objPtr; + ImVec2 rectMin, rectMax; + resolveUIRectWorld(obj, rectMin, rectMax); + ImVec2 rectSize(rectMax.x - rectMin.x, rectMax.y - rectMin.y); + if (rectSize.x <= 1.0f || rectSize.y <= 1.0f) continue; + if (rectOutsideOverlay(rectMin, rectMax)) continue; + if (rectMin.y < toolbarRectMax.y && rectMax.y > toolbarRectMin.y && + rectMin.x < toolbarRectMax.x && rectMax.x > toolbarRectMin.x) { + continue; + } + + ImGuiStyle savedStyle = ImGui::GetStyle(); + bool styleApplied = false; + if (!obj.ui.stylePreset.empty()) { + if (const auto* preset = getUIStylePreset(obj.ui.stylePreset)) { + ImGui::GetStyle() = preset->style; + styleApplied = true; + } + } + + if (obj.ui.type == UIElementType::Canvas) { + ImDrawList* dl = ImGui::GetWindowDrawList(); + dl->AddRect(rectMin, rectMax, IM_COL32(110, 170, 255, 140), 6.0f, 0, 1.5f); + if (styleApplied) ImGui::GetStyle() = savedStyle; + continue; + } + + ImVec2 drawMin = rectMin; + ImVec2 drawMax = rectMax; + ImVec2 drawSize(drawMax.x - drawMin.x, drawMax.y - drawMin.y); + ImVec2 localMin(drawMin.x - overlayPos.x, drawMin.y - overlayPos.y); + + ImGui::PushID(obj.id); + UIAnimationState& animState = uiAnimationStates[obj.id]; + if (!animState.initialized) { + animState.sliderValue = obj.ui.sliderValue; + animState.initialized = true; + } + if (obj.ui.type == UIElementType::Image || obj.ui.type == UIElementType::Sprite2D) { + unsigned int texId = 0; + if (!obj.albedoTexturePath.empty()) { + if (auto* tex = renderer.getTexture(obj.albedoTexturePath)) { + texId = tex->GetID(); + } + } + ImVec4 tint(obj.ui.color.r, obj.ui.color.g, obj.ui.color.b, obj.ui.color.a); + bool repeatX = obj.hasParallaxLayer2D && obj.parallaxLayer2D.enabled && obj.parallaxLayer2D.repeatX; + bool repeatY = obj.hasParallaxLayer2D && obj.parallaxLayer2D.enabled && obj.parallaxLayer2D.repeatY; + glm::vec2 spacing = obj.hasParallaxLayer2D ? obj.parallaxLayer2D.repeatSpacing : glm::vec2(0.0f); + float stepX = drawSize.x + spacing.x; + float stepY = drawSize.y + spacing.y; + glm::vec2 baseWorldMin = worldViewMin; + if (repeatX || repeatY) { + glm::vec2 sizeWorld(obj.ui.size.x, obj.ui.size.y); + ImVec2 pivotOffset = ImVec2(sizeWorld.x * 0.5f, sizeWorld.y * 0.5f); + switch (obj.ui.anchor) { + case UIAnchor::TopLeft: pivotOffset = ImVec2(0.0f, 0.0f); break; + case UIAnchor::TopRight: pivotOffset = ImVec2(sizeWorld.x, 0.0f); break; + case UIAnchor::BottomLeft: pivotOffset = ImVec2(0.0f, sizeWorld.y); break; + case UIAnchor::BottomRight: pivotOffset = ImVec2(sizeWorld.x, sizeWorld.y); break; + default: break; + } + glm::vec2 parentOffset = getWorldParentOffset(obj); + glm::vec2 worldPos = parentOffset + glm::vec2(obj.ui.position.x, obj.ui.position.y) + parallaxOffset(obj); + baseWorldMin = worldPos - glm::vec2(pivotOffset.x, pivotOffset.y); + } + float angle = glm::radians(obj.ui.rotation); + auto drawImageRect = [&](const ImVec2& min, const ImVec2& max) { + ImVec2 size(max.x - min.x, max.y - min.y); + if (size.x <= 1.0f || size.y <= 1.0f) return; + ImVec2 drawMinLocal(min.x, min.y); + ImVec2 drawMaxLocal(max.x, max.y); + if (std::abs(angle) > 1e-4f) { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImVec2 center((drawMinLocal.x + drawMaxLocal.x) * 0.5f, (drawMinLocal.y + drawMaxLocal.y) * 0.5f); + ImVec2 half(size.x * 0.5f, size.y * 0.5f); + float c = std::cos(angle); + float s = std::sin(angle); + auto rotPt = [&](float x, float y) { + return ImVec2(center.x + x * c - y * s, center.y + x * s + y * c); + }; + ImVec2 p0 = rotPt(-half.x, -half.y); + ImVec2 p1 = rotPt( half.x, -half.y); + ImVec2 p2 = rotPt( half.x, half.y); + ImVec2 p3 = rotPt(-half.x, half.y); + if (texId != 0) { + dl->AddImageQuad((ImTextureID)(intptr_t)texId, p0, p1, p2, p3, + ImVec2(0, 1), ImVec2(1, 1), ImVec2(1, 0), ImVec2(0, 0), + ImGui::GetColorU32(tint)); + } else { + ImU32 fill = ImGui::GetColorU32(tint); + ImU32 border = ImGui::GetColorU32(brighten(tint, 0.85f)); + dl->AddQuadFilled(p0, p1, p2, p3, fill); + dl->AddQuad(p0, p1, p2, p3, border, 2.0f); + ImVec2 textSize = ImGui::CalcTextSize(obj.ui.label.c_str()); + ImVec2 textPos(center.x - textSize.x * 0.5f, center.y - textSize.y * 0.5f); + dl->AddText(textPos, IM_COL32(210, 210, 220, 220), obj.ui.label.c_str()); + } + } else { + ImGui::SetCursorPos(ImVec2(drawMinLocal.x - overlayPos.x, drawMinLocal.y - overlayPos.y)); + if (texId != 0) { + ImGui::Image((ImTextureID)(intptr_t)texId, size, ImVec2(0, 1), ImVec2(1, 0), tint, ImVec4(0, 0, 0, 0)); + } else { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImU32 fill = ImGui::GetColorU32(tint); + ImU32 border = ImGui::GetColorU32(brighten(tint, 0.85f)); + dl->AddRectFilled(drawMinLocal, drawMaxLocal, fill, 6.0f); + dl->AddRect(drawMinLocal, drawMaxLocal, border, 6.0f); + ImVec2 textSize = ImGui::CalcTextSize(obj.ui.label.c_str()); + ImVec2 textPos(drawMinLocal.x + (size.x - textSize.x) * 0.5f, + drawMinLocal.y + (size.y - textSize.y) * 0.5f); + dl->AddText(textPos, IM_COL32(210, 210, 220, 220), obj.ui.label.c_str()); + } + } + ImGui::Dummy(size); + }; + + if (repeatX || repeatY) { + int startX = repeatX ? static_cast(std::floor((worldViewMin.x - baseWorldMin.x) / stepX)) - 1 : 0; + int endX = repeatX ? static_cast(std::ceil((worldViewMax.x - baseWorldMin.x) / stepX)) + 1 : 0; + int startY = repeatY ? static_cast(std::floor((worldViewMin.y - baseWorldMin.y) / stepY)) - 1 : 0; + int endY = repeatY ? static_cast(std::ceil((worldViewMax.y - baseWorldMin.y) / stepY)) + 1 : 0; + for (int ix = startX; ix <= endX; ++ix) { + for (int iy = startY; iy <= endY; ++iy) { + float dx = repeatX ? (float)ix * stepX : 0.0f; + float dy = repeatY ? (float)iy * stepY : 0.0f; + glm::vec2 tileMin = baseWorldMin + glm::vec2(dx, dy); + ImVec2 s0 = worldToScreen(tileMin); + ImVec2 s1 = worldToScreen(tileMin + glm::vec2(obj.ui.size.x, obj.ui.size.y)); + ImVec2 tMin(std::min(s0.x, s1.x), std::min(s0.y, s1.y)); + ImVec2 tMax(std::max(s0.x, s1.x), std::max(s0.y, s1.y)); + drawImageRect(tMin, tMax); + } + } + } else if (std::abs(angle) > 1e-4f) { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImVec2 center = ImVec2((drawMin.x + drawMax.x) * 0.5f, (drawMin.y + drawMax.y) * 0.5f); + ImVec2 half = ImVec2(drawSize.x * 0.5f, drawSize.y * 0.5f); + float c = std::cos(angle); + float s = std::sin(angle); + auto rotPt = [&](float x, float y) { + return ImVec2(center.x + x * c - y * s, center.y + x * s + y * c); + }; + ImVec2 p0 = rotPt(-half.x, -half.y); + ImVec2 p1 = rotPt( half.x, -half.y); + ImVec2 p2 = rotPt( half.x, half.y); + ImVec2 p3 = rotPt(-half.x, half.y); + if (texId != 0) { + dl->AddImageQuad((ImTextureID)(intptr_t)texId, p0, p1, p2, p3, + ImVec2(0, 1), ImVec2(1, 1), ImVec2(1, 0), ImVec2(0, 0), + ImGui::GetColorU32(tint)); + } else { + ImU32 fill = ImGui::GetColorU32(tint); + ImU32 border = ImGui::GetColorU32(brighten(tint, 0.85f)); + dl->AddQuadFilled(p0, p1, p2, p3, fill); + dl->AddQuad(p0, p1, p2, p3, border, 2.0f); + ImVec2 textSize = ImGui::CalcTextSize(obj.ui.label.c_str()); + ImVec2 textPos(center.x - textSize.x * 0.5f, center.y - textSize.y * 0.5f); + dl->AddText(textPos, IM_COL32(210, 210, 220, 220), obj.ui.label.c_str()); + } + ImGui::Dummy(drawSize); + } else { + ImGui::SetCursorPos(localMin); + if (texId != 0) { + ImGui::Image((ImTextureID)(intptr_t)texId, drawSize, ImVec2(0, 1), ImVec2(1, 0), tint, ImVec4(0, 0, 0, 0)); + } else { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImU32 fill = ImGui::GetColorU32(tint); + ImU32 border = ImGui::GetColorU32(brighten(tint, 0.85f)); + dl->AddRectFilled(drawMin, drawMax, fill, 6.0f); + dl->AddRect(drawMin, drawMax, border, 6.0f); + ImVec2 textSize = ImGui::CalcTextSize(obj.ui.label.c_str()); + ImVec2 textPos(drawMin.x + (drawSize.x - textSize.x) * 0.5f, + drawMin.y + (drawSize.y - textSize.y) * 0.5f); + dl->AddText(textPos, IM_COL32(210, 210, 220, 220), obj.ui.label.c_str()); + ImGui::Dummy(drawSize); + } + } + } else if (obj.ui.type == UIElementType::Slider) { + ImGui::SetCursorPos(localMin); + ImVec4 tint(obj.ui.color.r, obj.ui.color.g, obj.ui.color.b, obj.ui.color.a); + if (obj.ui.sliderStyle == UISliderStyle::ImGui) { + ImGui::PushItemWidth(drawSize.x); + ImGui::BeginDisabled(!obj.ui.interactable || uiWorldCameraActive); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(tint.x * 0.2f, tint.y * 0.2f, tint.z * 0.2f, tint.w * 0.6f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, brighten(tint, 0.5f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, brighten(tint, 0.7f)); + ImGui::PushStyleColor(ImGuiCol_SliderGrab, brighten(tint, 0.9f)); + ImGui::PushStyleColor(ImGuiCol_SliderGrabActive, brighten(tint, 1.1f)); + if (ImGui::SliderFloat(obj.ui.label.c_str(), &obj.ui.sliderValue, obj.ui.sliderMin, obj.ui.sliderMax)) { + projectManager.currentProject.hasUnsavedChanges = true; + } + ImGui::PopStyleColor(5); + ImGui::EndDisabled(); + ImGui::PopItemWidth(); + } else { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImU32 bg = ImGui::GetColorU32(ImVec4(tint.x * 0.2f, tint.y * 0.2f, tint.z * 0.2f, tint.w * 0.6f)); + ImU32 fill = ImGui::GetColorU32(tint); + ImU32 border = ImGui::GetColorU32(brighten(tint, 0.85f)); + float minValue = obj.ui.sliderMin; + float maxValue = obj.ui.sliderMax; + float range = (maxValue - minValue); + if (range <= 1e-6f) range = 1.0f; + ImGui::BeginDisabled(!obj.ui.interactable || uiWorldCameraActive); + ImGui::InvisibleButton("##UISlider", drawSize); + bool held = obj.ui.interactable && !uiWorldCameraActive && ImGui::IsItemActive(); + if (held && ImGui::IsMouseDown(ImGuiMouseButton_Left) && drawSize.x > 1.0f) { + float mouseT = (ImGui::GetIO().MousePos.x - drawMin.x) / drawSize.x; + mouseT = std::clamp(mouseT, 0.0f, 1.0f); + float newValue = minValue + mouseT * range; + if (newValue != obj.ui.sliderValue) { + obj.ui.sliderValue = newValue; + projectManager.currentProject.hasUnsavedChanges = true; + } + } + ImGui::EndDisabled(); + + animateValue(animState.sliderValue, obj.ui.sliderValue, held); + float displayValue = (uiAnimationMode == UIAnimationMode::Off) ? obj.ui.sliderValue : animState.sliderValue; + float t = (displayValue - minValue) / range; + t = std::clamp(t, 0.0f, 1.0f); + + if (obj.ui.sliderStyle == UISliderStyle::Fill) { + float rounding = 6.0f; + ImVec2 fillMax(drawMin.x + drawSize.x * t, drawMax.y); + dl->AddRectFilled(drawMin, drawMax, bg, rounding); + if (fillMax.x > drawMin.x) { + dl->AddRectFilled(drawMin, fillMax, fill, rounding); + } + dl->AddRect(drawMin, drawMax, border, rounding); + ImVec2 textSize = ImGui::CalcTextSize(obj.ui.label.c_str()); + ImVec2 textPos(drawMin.x + (drawSize.x - textSize.x) * 0.5f, + drawMin.y + (drawSize.y - textSize.y) * 0.5f); + dl->AddText(textPos, IM_COL32(240, 240, 245, 220), obj.ui.label.c_str()); + } else if (obj.ui.sliderStyle == UISliderStyle::Circle) { + ImVec2 center((drawMin.x + drawMax.x) * 0.5f, (drawMin.y + drawMax.y) * 0.5f); + float radius = std::max(2.0f, std::min(drawSize.x, drawSize.y) * 0.5f - 2.0f); + dl->AddCircleFilled(center, radius, bg, 32); + float start = -IM_PI * 0.5f; + float end = start + t * IM_PI * 2.0f; + dl->PathClear(); + dl->PathArcTo(center, radius, start, end, 32); + dl->PathLineTo(center); + dl->PathFillConvex(fill); + dl->AddCircle(center, radius, border, 32, 2.0f); + ImVec2 textSize = ImGui::CalcTextSize(obj.ui.label.c_str()); + ImVec2 textPos(center.x - textSize.x * 0.5f, center.y - textSize.y * 0.5f); + dl->AddText(textPos, IM_COL32(240, 240, 245, 220), obj.ui.label.c_str()); + } + } + } else if (obj.ui.type == UIElementType::Button) { + ImGui::SetCursorPos(localMin); + ImVec4 tint(obj.ui.color.r, obj.ui.color.g, obj.ui.color.b, obj.ui.color.a); + obj.ui.buttonPressed = false; + if (obj.ui.buttonStyle == UIButtonStyle::ImGui) { + ImGui::PushStyleColor(ImGuiCol_Button, tint); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, brighten(tint, 1.1f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, brighten(tint, 1.2f)); + ImGui::BeginDisabled(!obj.ui.interactable || uiWorldCameraActive); + obj.ui.buttonPressed = ImGui::Button(obj.ui.label.c_str(), drawSize); + ImGui::EndDisabled(); + ImGui::PopStyleColor(3); + } else if (obj.ui.buttonStyle == UIButtonStyle::Outline) { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImU32 border = ImGui::GetColorU32(tint); + ImGui::BeginDisabled(!obj.ui.interactable || uiWorldCameraActive); + if (ImGui::InvisibleButton("##UIButton", drawSize)) { + obj.ui.buttonPressed = obj.ui.interactable && !uiWorldCameraActive; + } + bool hovered = ImGui::IsItemHovered(); + bool active = ImGui::IsItemActive(); + ImGui::EndDisabled(); + float hoverT = animateValue(animState.hover, hovered ? 1.0f : 0.0f, false); + float activeT = animateValue(animState.active, active ? 1.0f : 0.0f, false); + if (hoverT > 0.001f) { + ImVec4 hoverCol = brighten(tint, 0.45f); + hoverCol.w *= std::clamp(hoverT, 0.0f, 1.0f); + dl->AddRectFilled(drawMin, drawMax, ImGui::GetColorU32(hoverCol), 6.0f); + } + if (activeT > 0.001f) { + ImVec4 activeCol = brighten(tint, 0.65f); + activeCol.w *= std::clamp(activeT, 0.0f, 1.0f); + dl->AddRectFilled(drawMin, drawMax, ImGui::GetColorU32(activeCol), 6.0f); + } + dl->AddRect(drawMin, drawMax, border, 6.0f, 0, 2.0f); + ImVec2 textSize = ImGui::CalcTextSize(obj.ui.label.c_str()); + ImVec2 textPos(drawMin.x + (drawSize.x - textSize.x) * 0.5f, + drawMin.y + (drawSize.y - textSize.y) * 0.5f); + dl->AddText(textPos, IM_COL32(240, 240, 245, 220), obj.ui.label.c_str()); + } + } else if (obj.ui.type == UIElementType::Text) { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImVec4 tint(obj.ui.color.r, obj.ui.color.g, obj.ui.color.b, obj.ui.color.a); + float scale = std::max(0.1f, obj.ui.textScale); + float scaleFactor = std::max(0.01f, uiWorldCamera.zoom / 100.0f); + float fontSize = std::max(1.0f, ImGui::GetFontSize() * scale * scaleFactor); + ImVec2 textPos = ImVec2(drawMin.x + 4.0f, drawMin.y + 2.0f); + ImGui::PushClipRect(drawMin, drawMax, true); + dl->AddText(ImGui::GetFont(), fontSize, textPos, ImGui::GetColorU32(tint), obj.ui.label.c_str()); + ImGui::PopClipRect(); + } + ImGui::PopID(); + if (styleApplied) ImGui::GetStyle() = savedStyle; + } + + bool gizmoUsed = false; + if (uiWorldHover && !uiWorldCameraActive && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && + !ImGuizmo::IsUsing() && !ImGuizmo::IsOver()) { + ImVec2 mouse = ImGui::GetIO().MousePos; + bool additive = ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift; + int hitId = -1; + for (auto it = sceneObjects.rbegin(); it != sceneObjects.rend(); ++it) { + const SceneObject& obj = *it; + if (!obj.enabled || !isUIType(obj) || obj.ui.type == UIElementType::Canvas) continue; + ImVec2 rectMin, rectMax; + resolveUIRectWorld(obj, rectMin, rectMax); + if (mouse.x >= rectMin.x && mouse.x <= rectMax.x && + mouse.y >= rectMin.y && mouse.y <= rectMax.y) { + hitId = obj.id; + break; + } + } + if (hitId >= 0) { + setPrimarySelection(hitId, additive); + gizmoUsed = true; + } else if (!additive) { + clearSelection(); + } + } + + SceneObject* selected = getSelectedObject(); + if (selected && isUIType(*selected) && selected->ui.type != UIElementType::Canvas) { + ImVec2 rectMin, rectMax; + resolveUIRectWorld(*selected, rectMin, rectMax); + ImVec2 rectSize(rectMax.x - rectMin.x, rectMax.y - rectMin.y); + if (rectSize.x > 1.0f && rectSize.y > 1.0f) { + ImGuizmo::OPERATION op = ImGuizmo::TRANSLATE; + if (mCurrentGizmoOperation == ImGuizmo::SCALE) { + op = ImGuizmo::SCALE; + } else if (mCurrentGizmoOperation == ImGuizmo::ROTATE) { + op = ImGuizmo::ROTATE; + } + glm::mat4 view(1.0f); + glm::mat4 proj = glm::ortho(0.0f, (float)(imageMax.x - imageMin.x), + (float)(imageMax.y - imageMin.y), 0.0f, -1.0f, 1.0f); + glm::vec2 parentOffset = getWorldParentOffset(*selected); + glm::vec2 worldSize(selected->ui.size.x, selected->ui.size.y); + auto anchorToPivotUI = [](UIAnchor anchor, const ImVec2& size) { + switch (anchor) { + case UIAnchor::TopLeft: return ImVec2(0.0f, 0.0f); + case UIAnchor::TopRight: return ImVec2(size.x, 0.0f); + case UIAnchor::BottomLeft: return ImVec2(0.0f, size.y); + case UIAnchor::BottomRight: return ImVec2(size.x, size.y); + default: return ImVec2(size.x * 0.5f, size.y * 0.5f); + } + }; + ImVec2 rectCenter((rectMin.x + rectMax.x) * 0.5f - imageMin.x, + (rectMin.y + rectMax.y) * 0.5f - imageMin.y); + glm::vec3 gizmoScale(1.0f, 1.0f, 1.0f); + if (op == ImGuizmo::SCALE) { + gizmoScale = glm::vec3(rectSize.x, rectSize.y, 1.0f); + } + glm::mat4 model(1.0f); + model = glm::translate(model, glm::vec3(rectCenter.x, rectCenter.y, 0.0f)); + model = glm::rotate(model, glm::radians(selected->ui.rotation), glm::vec3(0.0f, 0.0f, 1.0f)); + model = glm::scale(model, gizmoScale); + + ImGuizmo::BeginFrame(); + ImGuizmo::Enable(true); + ImGuizmo::SetOrthographic(true); + ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList()); + ImGuizmo::SetRect(imageMin.x, imageMin.y, imageMax.x - imageMin.x, imageMax.y - imageMin.y); + glm::mat4 delta(1.0f); + ImGuizmo::Manipulate(glm::value_ptr(view), glm::value_ptr(proj), op, ImGuizmo::LOCAL, glm::value_ptr(model), glm::value_ptr(delta)); + if (ImGuizmo::IsUsing()) { + glm::vec3 pos, rot, scl; + DecomposeMatrix(model, pos, rot, scl); + glm::vec3 euler = NormalizeEulerDegrees(glm::degrees(rot)); + ImVec2 newCenter(imageMin.x + pos.x, imageMin.y + pos.y); + glm::vec2 worldCenter = screenToWorld(newCenter); + if (op == ImGuizmo::ROTATE) { + selected->ui.rotation = euler.z; + } else if (op == ImGuizmo::TRANSLATE) { + ImVec2 pivotOffset = anchorToPivotUI(selected->ui.anchor, ImVec2(worldSize.x, worldSize.y)); + glm::vec2 worldMin = worldCenter - worldSize * 0.5f; + glm::vec2 worldPivot = worldMin + glm::vec2(pivotOffset.x, pivotOffset.y); + selected->ui.position = worldPivot - parentOffset - parallaxOffset(*selected); + } else if (op == ImGuizmo::SCALE) { + ImVec2 newSize(std::max(1.0f, scl.x), std::max(1.0f, scl.y)); + worldSize = glm::vec2(newSize.x, newSize.y) / uiWorldCamera.zoom; + ImVec2 pivotOffset = anchorToPivotUI(selected->ui.anchor, ImVec2(worldSize.x, worldSize.y)); + glm::vec2 worldMin = worldCenter - worldSize * 0.5f; + glm::vec2 worldPivot = worldMin + glm::vec2(pivotOffset.x, pivotOffset.y); + selected->ui.position = worldPivot - parentOffset - parallaxOffset(*selected); + selected->ui.size = worldSize; + } + projectManager.currentProject.hasUnsavedChanges = true; + gizmoUsed = true; + } + } + } + + ImGui::EndChild(); + ImGui::PopStyleVar(); + + if (ImGui::IsAnyItemActive() || uiWorldCameraActive || gizmoUsed) { + blockSelection = true; + } + } + auto projectToScreen = [&](const glm::vec3& p) -> std::optional { glm::vec4 clip = proj * view * glm::vec4(p, 1.0f); if (clip.w <= 0.0f) return std::nullopt; @@ -1183,7 +2508,7 @@ void Engine::renderViewport() { }; SceneObject* selectedObj = getSelectedObject(); - if (selectedObj && selectedObj->type != ObjectType::PostFXNode && selectedObj->type != ObjectType::Canvas && selectedObj->type != ObjectType::UIImage && selectedObj->type != ObjectType::UISlider && selectedObj->type != ObjectType::UIButton && selectedObj->type != ObjectType::UIText && selectedObj->type != ObjectType::Sprite2D) { + if (!uiWorldMode && selectedObj && !selectedObj->hasPostFX && !HasUIComponent(*selectedObj)) { ImGuizmo::BeginFrame(); ImGuizmo::Enable(true); ImGuizmo::SetOrthographic(false); @@ -1254,6 +2579,7 @@ void Engine::renderViewport() { float selectRadius = 10.0f; ImVec2 mouse = ImGui::GetIO().MousePos; bool clicked = mouseOverViewportImage && ImGui::IsMouseClicked(0) && !ImGuizmo::IsUsing() && !ImGuizmo::IsOver(); + bool doubleClicked = mouseOverViewportImage && ImGui::IsMouseDoubleClicked(0) && !ImGuizmo::IsUsing() && !ImGuizmo::IsOver(); bool additiveClick = ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift; glm::mat4 invModel = glm::inverse(modelMatrix); @@ -1437,6 +2763,134 @@ void Engine::renderViewport() { meshEditSelectedFaces.clear(); } } else if (meshEditSelectionMode == MeshEditSelectionMode::Face) { + auto computeFaceNormal = [&](const glm::u32vec3& f, glm::vec3& out) -> bool { + if (f.x >= meshEditAsset.positions.size() || + f.y >= meshEditAsset.positions.size() || + f.z >= meshEditAsset.positions.size()) { + return false; + } + const glm::vec3& a = meshEditAsset.positions[f.x]; + const glm::vec3& b = meshEditAsset.positions[f.y]; + const glm::vec3& c = meshEditAsset.positions[f.z]; + glm::vec3 n = glm::cross(b - a, c - a); + float len = glm::length(n); + if (len < 1e-6f) { + return false; + } + out = n / len; + return true; + }; + auto gatherCoplanarFaces = [&](int seed) { + std::vector group; + const size_t faceCount = meshEditAsset.faces.size(); + if (seed < 0 || seed >= (int)faceCount) return group; + glm::vec3 seedNormal(0.0f); + if (!computeFaceNormal(meshEditAsset.faces[seed], seedNormal)) { + group.push_back(seed); + return group; + } + + std::unordered_map> edgeToFaces; + edgeToFaces.reserve(faceCount * 3); + auto edgeKey = [](uint32_t a, uint32_t b) { + return (static_cast(std::min(a, b)) << 32) | + static_cast(std::max(a, b)); + }; + for (size_t fi = 0; fi < faceCount; ++fi) { + const auto& f = meshEditAsset.faces[fi]; + uint32_t tri[3] = { f.x, f.y, f.z }; + for (int e = 0; e < 3; ++e) { + edgeToFaces[edgeKey(tri[e], tri[(e + 1) % 3])].push_back((int)fi); + } + } + + std::vector visited(faceCount, 0); + std::vector stack; + visited[seed] = 1; + stack.push_back(seed); + group.push_back(seed); + + const auto& seedFace = meshEditAsset.faces[seed]; + glm::vec3 seedPoint = meshEditAsset.positions[seedFace.x]; + float seedD = glm::dot(seedNormal, seedPoint); + const float normalThreshold = 0.995f; + const float planeEpsilon = 1e-3f; + + while (!stack.empty()) { + int current = stack.back(); + stack.pop_back(); + const auto& f = meshEditAsset.faces[current]; + uint32_t tri[3] = { f.x, f.y, f.z }; + for (int e = 0; e < 3; ++e) { + auto it = edgeToFaces.find(edgeKey(tri[e], tri[(e + 1) % 3])); + if (it == edgeToFaces.end()) continue; + for (int neighbor : it->second) { + if (neighbor < 0 || neighbor >= (int)faceCount) continue; + if (visited[neighbor]) continue; + glm::vec3 n(0.0f); + if (!computeFaceNormal(meshEditAsset.faces[neighbor], n)) continue; + if (glm::dot(seedNormal, n) < normalThreshold) continue; + const auto& nf = meshEditAsset.faces[neighbor]; + const glm::vec3& na = meshEditAsset.positions[nf.x]; + const glm::vec3& nb = meshEditAsset.positions[nf.y]; + const glm::vec3& nc = meshEditAsset.positions[nf.z]; + if (std::abs(glm::dot(seedNormal, na) - seedD) > planeEpsilon || + std::abs(glm::dot(seedNormal, nb) - seedD) > planeEpsilon || + std::abs(glm::dot(seedNormal, nc) - seedD) > planeEpsilon) { + continue; + } + visited[neighbor] = 1; + stack.push_back(neighbor); + group.push_back(neighbor); + } + } + } + std::sort(group.begin(), group.end()); + group.erase(std::unique(group.begin(), group.end()), group.end()); + return group; + }; + auto gatherQuadFaces = [&](int seed) { + std::vector group; + const size_t faceCount = meshEditAsset.faces.size(); + if (seed < 0 || seed >= (int)faceCount) return group; + glm::vec3 seedNormal(0.0f); + if (!computeFaceNormal(meshEditAsset.faces[seed], seedNormal)) { + group.push_back(seed); + return group; + } + const auto& seedFace = meshEditAsset.faces[seed]; + uint32_t seedIdx[3] = { seedFace.x, seedFace.y, seedFace.z }; + group.push_back(seed); + + for (size_t fi = 0; fi < faceCount; ++fi) { + if ((int)fi == seed) continue; + const auto& f = meshEditAsset.faces[fi]; + uint32_t idx[3] = { f.x, f.y, f.z }; + int shared = 0; + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + if (seedIdx[i] == idx[j]) { + shared++; + break; + } + } + } + if (shared >= 2) { + glm::vec3 n(0.0f); + if (!computeFaceNormal(f, n)) continue; + if (glm::dot(seedNormal, n) < 0.995f) continue; + group.push_back((int)fi); + break; + } + } + + if (group.size() > 1) { + std::sort(group.begin(), group.end()); + group.erase(std::unique(group.begin(), group.end()), group.end()); + } + return group; + }; + for (int fi : meshEditSelectedFaces) { if (fi < 0 || fi >= (int)meshEditAsset.faces.size()) continue; const auto& f = meshEditAsset.faces[fi]; @@ -1451,7 +2905,7 @@ void Engine::renderViewport() { dl->AddTriangle(*sa, *sb, *sc, selCol, 2.0f); } - if (clicked) { + if (clicked || doubleClicked) { auto ray = makeRay(mouse); glm::vec3 localOrigin = glm::vec3(invModel * glm::vec4(ray.first, 1.0f)); glm::vec3 localDir = glm::normalize(glm::vec3(invModel * glm::vec4(ray.second, 0.0f))); @@ -1473,16 +2927,36 @@ void Engine::renderViewport() { } } if (clickedIndex >= 0) { + std::vector group; + if (doubleClicked) { + group = gatherQuadFaces(clickedIndex); + } + if (group.empty()) group.push_back(clickedIndex); if (additiveClick) { - auto itSel = std::find(meshEditSelectedFaces.begin(), meshEditSelectedFaces.end(), clickedIndex); - if (itSel == meshEditSelectedFaces.end()) { - meshEditSelectedFaces.push_back(clickedIndex); + bool allSelected = true; + for (int fi : group) { + if (std::find(meshEditSelectedFaces.begin(), meshEditSelectedFaces.end(), fi) == meshEditSelectedFaces.end()) { + allSelected = false; + break; + } + } + if (allSelected) { + for (int fi : group) { + auto itSel = std::find(meshEditSelectedFaces.begin(), meshEditSelectedFaces.end(), fi); + if (itSel != meshEditSelectedFaces.end()) { + meshEditSelectedFaces.erase(itSel); + } + } } else { - meshEditSelectedFaces.erase(itSel); + for (int fi : group) { + if (std::find(meshEditSelectedFaces.begin(), meshEditSelectedFaces.end(), fi) == meshEditSelectedFaces.end()) { + meshEditSelectedFaces.push_back(fi); + } + } } } else { meshEditSelectedFaces.clear(); - meshEditSelectedFaces.push_back(clickedIndex); + meshEditSelectedFaces = std::move(group); } } else if (!additiveClick) { meshEditSelectedFaces.clear(); @@ -1507,14 +2981,105 @@ void Engine::renderViewport() { pushUnique(edges[ei].y); } } else if (meshEditSelectionMode == MeshEditSelectionMode::Face) { + auto computeFaceNormal = [&](const glm::u32vec3& f, glm::vec3& out) -> bool { + if (f.x >= meshEditAsset.positions.size() || + f.y >= meshEditAsset.positions.size() || + f.z >= meshEditAsset.positions.size()) { + return false; + } + const glm::vec3& a = meshEditAsset.positions[f.x]; + const glm::vec3& b = meshEditAsset.positions[f.y]; + const glm::vec3& c = meshEditAsset.positions[f.z]; + glm::vec3 n = glm::cross(b - a, c - a); + float len = glm::length(n); + if (len < 1e-6f) return false; + out = n / len; + return true; + }; for (int fi : meshEditSelectedFaces) { if (fi < 0 || fi >= (int)meshEditAsset.faces.size()) continue; const auto& f = meshEditAsset.faces[fi]; pushUnique(f.x); pushUnique(f.y); pushUnique(f.z); + + glm::vec3 seedNormal(0.0f); + if (!computeFaceNormal(f, seedNormal)) continue; + uint32_t seedIdx[3] = { f.x, f.y, f.z }; + for (size_t nfi = 0; nfi < meshEditAsset.faces.size(); ++nfi) { + if ((int)nfi == fi) continue; + const auto& nf = meshEditAsset.faces[nfi]; + uint32_t idx[3] = { nf.x, nf.y, nf.z }; + int shared = 0; + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + if (seedIdx[i] == idx[j]) { + shared++; + break; + } + } + } + if (shared < 2) continue; + glm::vec3 n(0.0f); + if (!computeFaceNormal(nf, n)) continue; + if (glm::dot(seedNormal, n) < 0.995f) continue; + pushUnique(nf.x); + pushUnique(nf.y); + pushUnique(nf.z); + break; + } } } + if (meshEditSelectionMode == MeshEditSelectionMode::Face && !baseAffectedVerts.empty()) { + struct PosKey { + int64_t x; + int64_t y; + int64_t z; + }; + struct PosKeyHash { + size_t operator()(const PosKey& k) const { + size_t h1 = std::hash{}(k.x); + size_t h2 = std::hash{}(k.y); + size_t h3 = std::hash{}(k.z); + return h1 ^ (h2 << 1) ^ (h3 << 2); + } + }; + struct PosKeyEq { + bool operator()(const PosKey& a, const PosKey& b) const { + return a.x == b.x && a.y == b.y && a.z == b.z; + } + }; + + const float epsilon = 1e-5f; + const float invEps = 1.0f / epsilon; + std::unordered_map, PosKeyHash, PosKeyEq> keyToVerts; + keyToVerts.reserve(meshEditAsset.positions.size()); + + auto makeKey = [&](const glm::vec3& p) { + return PosKey{ + (int64_t)llround(p.x * invEps), + (int64_t)llround(p.y * invEps), + (int64_t)llround(p.z * invEps) + }; + }; + + for (size_t i = 0; i < meshEditAsset.positions.size(); ++i) { + keyToVerts[makeKey(meshEditAsset.positions[i])].push_back((int)i); + } + + std::unordered_set expanded(baseAffectedVerts.begin(), baseAffectedVerts.end()); + for (int idx : baseAffectedVerts) { + if (idx < 0 || idx >= (int)meshEditAsset.positions.size()) continue; + auto it = keyToVerts.find(makeKey(meshEditAsset.positions[idx])); + if (it == keyToVerts.end()) continue; + for (int v : it->second) { + expanded.insert(v); + } + } + + baseAffectedVerts.assign(expanded.begin(), expanded.end()); + std::sort(baseAffectedVerts.begin(), baseAffectedVerts.end()); + } auto recalcMesh = [&]() { meshEditAsset.boundsMin = glm::vec3(FLT_MAX); @@ -1574,6 +3139,9 @@ void Engine::renderViewport() { bool seams = ImGui::GetIO().KeyShift && ImGui::GetIO().KeyCtrl; meshEditExtruding = false; meshEditExtrudeVerts.clear(); + int originalVertexCount = static_cast(meshEditAsset.positions.size()); + int originalFaceCount = static_cast(meshEditAsset.faces.size()); + int newFaceStart = -1; auto duplicateVertex = [&](uint32_t idx) -> uint32_t { uint32_t newIdx = static_cast(meshEditAsset.positions.size()); @@ -1590,13 +3158,77 @@ void Engine::renderViewport() { } return newIdx; }; + auto rebuildAffectedVerts = [&]() { + baseAffectedVerts = meshEditSelectedVertices; + auto pushUnique = [&](int idx) { + if (idx < 0) return; + if (std::find(baseAffectedVerts.begin(), baseAffectedVerts.end(), idx) == baseAffectedVerts.end()) { + baseAffectedVerts.push_back(idx); + } + }; + if (meshEditSelectionMode == MeshEditSelectionMode::Edge) { + for (int ei : meshEditSelectedEdges) { + if (ei < 0 || ei >= (int)edges.size()) continue; + pushUnique(edges[ei].x); + pushUnique(edges[ei].y); + } + } else if (meshEditSelectionMode == MeshEditSelectionMode::Face) { + for (int fi : meshEditSelectedFaces) { + if (fi < 0 || fi >= (int)meshEditAsset.faces.size()) continue; + const auto& f = meshEditAsset.faces[fi]; + pushUnique(f.x); + pushUnique(f.y); + pushUnique(f.z); + } + } + }; auto pushExtrudeVert = [&](int idx) { if (std::find(meshEditExtrudeVerts.begin(), meshEditExtrudeVerts.end(), idx) == meshEditExtrudeVerts.end()) { meshEditExtrudeVerts.push_back(idx); } }; + auto ensureUvs = [&]() { + if (meshEditAsset.uvs.size() < meshEditAsset.positions.size()) { + meshEditAsset.uvs.resize(meshEditAsset.positions.size(), glm::vec2(0.0f)); + } + }; + auto applyPlanarUV = [&](const glm::u32vec3& face) -> bool { + if (face.x >= meshEditAsset.positions.size() || + face.y >= meshEditAsset.positions.size() || + face.z >= meshEditAsset.positions.size()) { + return false; + } + const glm::vec3& a = meshEditAsset.positions[face.x]; + const glm::vec3& b = meshEditAsset.positions[face.y]; + const glm::vec3& c = meshEditAsset.positions[face.z]; + glm::vec3 n = glm::normalize(glm::cross(b - a, c - a)); + glm::vec2 ua(a.x, a.y), ub(b.x, b.y), uc(c.x, c.y); + if (std::abs(n.x) >= std::abs(n.y) && std::abs(n.x) >= std::abs(n.z)) { + ua = glm::vec2(a.y, a.z); + ub = glm::vec2(b.y, b.z); + uc = glm::vec2(c.y, c.z); + } else if (std::abs(n.y) >= std::abs(n.z)) { + ua = glm::vec2(a.x, a.z); + ub = glm::vec2(b.x, b.z); + uc = glm::vec2(c.x, c.z); + } + glm::vec2 minUV = glm::min(glm::min(ua, ub), uc); + glm::vec2 maxUV = glm::max(glm::max(ua, ub), uc); + glm::vec2 span = maxUV - minUV; + auto toUv = [&](const glm::vec2& v) { + return glm::vec2( + span.x > 1e-5f ? (v.x - minUV.x) / span.x : 0.0f, + span.y > 1e-5f ? (v.y - minUV.y) / span.y : 0.0f + ); + }; + meshEditAsset.uvs[face.x] = toUv(ua); + meshEditAsset.uvs[face.y] = toUv(ub); + meshEditAsset.uvs[face.z] = toUv(uc); + return true; + }; if (wantsExtrude && meshEditSelectionMode == MeshEditSelectionMode::Face && !meshEditSelectedFaces.empty()) { + newFaceStart = (int)meshEditAsset.faces.size(); const size_t faceCount = meshEditAsset.faces.size(); std::vector originalFaces = meshEditAsset.faces; std::vector faceSelected(faceCount, false); @@ -1702,6 +3334,7 @@ void Engine::renderViewport() { meshEditExtruding = !meshEditExtrudeVerts.empty(); } else if (wantsExtrude && meshEditSelectionMode == MeshEditSelectionMode::Edge && !meshEditSelectedEdges.empty()) { + newFaceStart = (int)meshEditAsset.faces.size(); std::unordered_map vertexMap; if (!seams) { vertexMap.reserve(meshEditSelectedEdges.size() * 2); @@ -1744,6 +3377,24 @@ void Engine::renderViewport() { meshEditExtruding = !meshEditExtrudeVerts.empty(); } + + if (newFaceStart >= 0 && newFaceStart < (int)meshEditAsset.faces.size()) { + ensureUvs(); + bool wroteUvs = false; + for (int fi = newFaceStart; fi < (int)meshEditAsset.faces.size(); ++fi) { + const auto& f = meshEditAsset.faces[fi]; + bool shouldWrite = !meshEditAsset.hasUVs || + f.x >= (uint32_t)originalVertexCount || + f.y >= (uint32_t)originalVertexCount || + f.z >= (uint32_t)originalVertexCount; + if (shouldWrite) { + wroteUvs |= applyPlanarUV(f); + } + } + if (wroteUvs) { + meshEditAsset.hasUVs = true; + } + } } std::vector affectedVerts = baseAffectedVerts; @@ -1854,6 +3505,10 @@ void Engine::renderViewport() { gizmoBoundsMin = glm::vec3(-0.25f); gizmoBoundsMax = glm::vec3(0.25f); break; + case ObjectType::Empty: + gizmoBoundsMin = glm::vec3(-0.2f); + gizmoBoundsMax = glm::vec3(0.2f); + break; case ObjectType::Sprite2D: case ObjectType::Canvas: case ObjectType::UIImage: @@ -2007,9 +3662,9 @@ void Engine::renderViewport() { } }; - if (showSceneGizmos) { + if (showSceneGizmos && !uiWorldMode) { for (const auto& obj : sceneObjects) { - if (obj.type == ObjectType::Camera) { + if (obj.hasCamera) { drawCameraDirection(obj); } } @@ -2031,7 +3686,7 @@ void Engine::renderViewport() { return f; }; - if (lightObj.type == ObjectType::PointLight) { + if (lightObj.light.type == LightType::Point) { auto center = projectToScreen(lightObj.position); glm::vec3 offset = lightObj.position + glm::vec3(lightObj.light.range, 0.0f, 0.0f); auto edge = projectToScreen(offset); @@ -2039,7 +3694,7 @@ void Engine::renderViewport() { float r = std::sqrt((center->x - edge->x)*(center->x - edge->x) + (center->y - edge->y)*(center->y - edge->y)); dl->AddCircle(*center, r, faint, 48, 2.0f); } - } else if (lightObj.type == ObjectType::SpotLight) { + } else if (lightObj.light.type == LightType::Spot) { glm::vec3 dir = forwardFromRotation(lightObj); glm::vec3 tip = lightObj.position; glm::vec3 end = tip + dir * lightObj.light.range; @@ -2073,7 +3728,7 @@ void Engine::renderViewport() { drawConeRing(innerRad, col); drawConeRing(outerRad, faint); } - } else if (lightObj.type == ObjectType::AreaLight) { + } else if (lightObj.light.type == LightType::Area) { glm::vec3 n = forwardFromRotation(lightObj); glm::vec3 up = glm::abs(n.y) > 0.9f ? glm::vec3(1,0,0) : glm::vec3(0,1,0); glm::vec3 tangent = glm::normalize(glm::cross(up, n)); @@ -2108,33 +3763,88 @@ void Engine::renderViewport() { } }; - if (showSceneGizmos) { + if (showSceneGizmos && !uiWorldMode) { for (const auto& obj : sceneObjects) { - if (obj.type == ObjectType::PointLight || obj.type == ObjectType::SpotLight || obj.type == ObjectType::AreaLight) { + if (!obj.hasLight) continue; + if (obj.light.type == LightType::Point || obj.light.type == LightType::Spot || obj.light.type == LightType::Area) { drawLightOverlays(obj); } } } - // Toolbar + auto drawArmatureOverlays = [&](const SceneObject& skinnedObj, + const std::unordered_map& idLookup) { + if (!skinnedObj.hasSkeletalAnimation || !skinnedObj.skeletal.enabled) return; + if (skinnedObj.skeletal.boneNodeIds.empty()) return; + + std::unordered_set boneIds; + for (int id : skinnedObj.skeletal.boneNodeIds) { + if (id >= 0) boneIds.insert(id); + } + if (boneIds.empty()) return; + + if (boneIds.size() <= 2 && skinnedObj.skeletal.skeletonRootId >= 0) { + std::vector stack; + stack.push_back(skinnedObj.skeletal.skeletonRootId); + while (!stack.empty()) { + int currentId = stack.back(); + stack.pop_back(); + auto it = idLookup.find(currentId); + if (it == idLookup.end() || !it->second) continue; + const SceneObject* node = it->second; + if (node->type == ObjectType::Empty) { + boneIds.insert(node->id); + } + for (int childId : node->childIds) { + if (childId >= 0) { + stack.push_back(childId); + } + } + } + } + + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImU32 lineCol = ImGui::GetColorU32(ImVec4(0.55f, 0.9f, 0.8f, 0.75f)); + ImU32 nodeCol = ImGui::GetColorU32(ImVec4(0.85f, 0.95f, 0.9f, 0.9f)); + ImU32 rootCol = ImGui::GetColorU32(ImVec4(1.0f, 0.85f, 0.45f, 0.95f)); + + for (int id : boneIds) { + auto it = idLookup.find(id); + if (it == idLookup.end() || !it->second) continue; + const SceneObject* boneObj = it->second; + auto boneScreen = projectToScreen(boneObj->position); + if (!boneScreen) continue; + + bool isRoot = boneObj->parentId < 0 || boneIds.find(boneObj->parentId) == boneIds.end(); + float radius = isRoot ? 4.5f : 3.0f; + dl->AddCircleFilled(*boneScreen, radius, isRoot ? rootCol : nodeCol); + + if (boneObj->parentId >= 0) { + auto parentIt = idLookup.find(boneObj->parentId); + if (parentIt != idLookup.end() && parentIt->second && + boneIds.find(boneObj->parentId) != boneIds.end()) { + auto parentScreen = projectToScreen(parentIt->second->position); + if (parentScreen) { + dl->AddLine(*parentScreen, *boneScreen, lineCol, 2.0f); + } + } + } + } + }; + const float toolbarPadding = 6.0f; const float toolbarSpacing = 5.0f; const ImVec2 gizmoButtonSize(60.0f, 24.0f); - const float toolbarWidthEstimate = 520.0f; - const float toolbarHeightEstimate = 42.0f; // rough height to keep toolbar on-screen when anchoring bottom - ImVec2 desiredBottomLeft = ImVec2(imageMin.x + 12.0f, imageMax.y - 12.0f); - - float minX = imageMin.x + 12.0f; - float maxX = imageMax.x - 12.0f; - float toolbarLeft = desiredBottomLeft.x; - if (toolbarLeft + toolbarWidthEstimate > maxX) toolbarLeft = maxX - toolbarWidthEstimate; - if (toolbarLeft < minX) toolbarLeft = minX; - - float minY = imageMin.y + 12.0f; - float toolbarTop = desiredBottomLeft.y - toolbarHeightEstimate; - if (toolbarTop < minY) toolbarTop = minY; - - ImVec2 toolbarPos = ImVec2(toolbarLeft, toolbarTop); + if (showSceneGizmos && !uiWorldMode) { + std::unordered_map idLookup; + idLookup.reserve(sceneObjects.size()); + for (const auto& obj : sceneObjects) { + idLookup.emplace(obj.id, &obj); + } + for (const auto& obj : sceneObjects) { + drawArmatureOverlays(obj, idLookup); + } + } const ImGuiStyle& style = ImGui::GetStyle(); ImVec4 bgCol = style.Colors[ImGuiCol_PopupBg]; @@ -2157,17 +3867,25 @@ void Engine::renderViewport() { ImU32 toolbarBg = ImGui::GetColorU32(bgCol); ImU32 toolbarOutline = ImGui::GetColorU32(ImVec4(1, 1, 1, 0.0f)); + if (showViewportToolbar) { + ImGui::SetNextWindowPos(toolbarRectMin, ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.0f); + ImGuiWindowFlags toolbarFlags = ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoDocking | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoNav; + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(toolbarPadding, toolbarPadding)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(toolbarSpacing, toolbarSpacing)); + ImGui::Begin("##ViewportToolbarOverlay", nullptr, toolbarFlags); + ImDrawList* toolbarDrawList = ImGui::GetWindowDrawList(); ImDrawListSplitter splitter; splitter.Split(toolbarDrawList, 2); splitter.SetCurrentChannel(toolbarDrawList, 1); - ImVec2 contentStart = ImVec2(toolbarPos.x + toolbarPadding, toolbarPos.y + toolbarPadding); - ImVec2 windowPos = ImGui::GetWindowPos(); - ImVec2 contentStartLocal = ImVec2(contentStart.x - windowPos.x, contentStart.y - windowPos.y); - ImGui::SetCursorPos(contentStartLocal); - ImVec2 contentStartScreen = ImGui::GetCursorScreenPos(); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(toolbarSpacing, toolbarSpacing)); ImGui::BeginGroup(); auto gizmoButton = [&](const char* label, ImGuizmo::OPERATION op, const char* tooltip) { @@ -2277,26 +3995,47 @@ void Engine::renderViewport() { if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Toggle light/camera scene symbols"); } + ImGui::SameLine(0.0f, toolbarSpacing * 0.8f); + if (GizmoToolbar::ModeButton("Grid", showSceneGrid3D, ImVec2(54, 24), baseCol, accentCol, textCol)) { + showSceneGrid3D = !showSceneGrid3D; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Toggle 3D grid"); + } + ImGui::SameLine(0.0f, toolbarSpacing * 0.8f); + if (GizmoToolbar::ModeButton("UI World", uiWorldMode, ImVec2(76, 24), baseCol, accentCol, textCol)) { + uiWorldMode = !uiWorldMode; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Toggle 2D UI world overlay"); + } ImGui::EndGroup(); - ImGui::PopStyleVar(); - - ImVec2 groupMax = ImGui::GetItemRectMax(); splitter.SetCurrentChannel(toolbarDrawList, 0); float rounding = 10.0f; - ImVec2 bgMin = ImVec2(contentStartScreen.x - toolbarPadding, contentStartScreen.y - toolbarPadding); - ImVec2 bgMax = ImVec2(groupMax.x + toolbarPadding, groupMax.y + toolbarPadding); + ImVec2 bgMin = ImGui::GetWindowPos(); + ImVec2 bgMax = ImVec2(bgMin.x + ImGui::GetWindowSize().x, bgMin.y + ImGui::GetWindowSize().y); toolbarDrawList->AddRectFilled(bgMin, bgMax, toolbarBg, rounding, ImDrawFlags_RoundCornersAll); toolbarDrawList->AddRect(bgMin, bgMax, toolbarOutline, rounding, ImDrawFlags_RoundCornersAll, 1.5f); splitter.Merge(toolbarDrawList); - // Prevent viewport picking when clicking on the toolbar overlay. - if (ImGui::IsMouseHoveringRect(bgMin, bgMax)) { + toolbarSizeCache = ImGui::GetWindowSize(); + toolbarRectMin = ImGui::GetWindowPos(); + toolbarRectMax = ImVec2(toolbarRectMin.x + toolbarSizeCache.x, toolbarRectMin.y + toolbarSizeCache.y); + + if (ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) || ImGui::IsAnyItemHovered()) { blockSelection = true; } + ImGui::End(); + ImGui::PopStyleVar(2); + } + + if (uiWorldMode) { + blockSelection = true; + } // Left-click picking inside viewport if (mouseOverViewportImage && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && @@ -2491,6 +4230,9 @@ void Engine::renderViewport() { case ObjectType::PostFXNode: hit = false; break; + case ObjectType::Empty: + hit = false; + break; } if (hit && hitT < closest && hitT >= 0.0f) { @@ -2525,7 +4267,7 @@ void Engine::renderViewport() { if (isPlaying && showViewOutput) { std::vector playerCams; for (const auto& obj : sceneObjects) { - if (obj.type == ObjectType::Camera && obj.camera.type == SceneCameraType::Player) { + if (obj.hasCamera && obj.camera.type == SceneCameraType::Player) { playerCams.push_back(&obj); } } @@ -2607,3 +4349,521 @@ void Engine::renderViewport() { ImGui::End(); } #pragma endregion + +#pragma region Player Viewport +void Engine::renderPlayerViewport() { + ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->Pos); + ImGui::SetNextWindowSize(viewport->Size); + if (playerMode && isPlaying && gameViewCursorLocked) { + ImGui::SetNextWindowFocus(); + } + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoBringToFrontOnFocus | + ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_NoScrollbar; + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + ImGui::Begin("PlayerViewport", nullptr, flags); + ImGui::PopStyleVar(); + + bool windowFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); + ImVec2 imageSize = ImGui::GetContentRegionAvail(); + if (imageSize.x > 0 && imageSize.y > 0) { + viewportWidth = static_cast(imageSize.x); + viewportHeight = static_cast(imageSize.y); + if (rendererInitialized) { + renderer.resize(viewportWidth, viewportHeight); + } + } + + if (rendererInitialized) { + unsigned int tex = renderer.getViewportTexture(); + ImGui::Image((void*)(intptr_t)tex, imageSize, ImVec2(0, 1), ImVec2(1, 0)); + ImVec2 imageMin = ImGui::GetItemRectMin(); + ImVec2 imageMax = ImGui::GetItemRectMax(); + bool imageHovered = ImGui::IsItemHovered(); + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + float uiScaleX = (viewportWidth > 0) ? (imageSize.x / (float)viewportWidth) : 1.0f; + float uiScaleY = (viewportHeight > 0) ? (imageSize.y / (float)viewportHeight) : 1.0f; + + if (showCanvasOverlay) { + ImVec2 pad(8.0f, 8.0f); + ImVec2 tl(imageMin.x + pad.x, imageMin.y + pad.y); + ImVec2 br(imageMax.x - pad.x, imageMax.y - pad.y); + drawList->AddRect(tl, br, IM_COL32(110, 170, 255, 180), 8.0f, 0, 2.0f); + } + + bool uiInteracting = false; + auto isUIType = [](const SceneObject& target) { + return target.hasUI && target.ui.type != UIElementType::None; + }; + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + ImGui::SetCursorScreenPos(imageMin); + ImGui::BeginChild("PlayerUIOverlay", + ImVec2(imageMax.x - imageMin.x, imageMax.y - imageMin.y), + false, + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoBackground); + + auto anchorToPivot = [](UIAnchor anchor, const ImVec2& size) { + switch (anchor) { + case UIAnchor::Center: return ImVec2(size.x * 0.5f, size.y * 0.5f); + case UIAnchor::TopLeft: return ImVec2(0.0f, 0.0f); + case UIAnchor::TopRight: return ImVec2(size.x, 0.0f); + case UIAnchor::BottomLeft: return ImVec2(0.0f, size.y); + case UIAnchor::BottomRight: return ImVec2(size.x, size.y); + default: return ImVec2(size.x * 0.5f, size.y * 0.5f); + } + }; + auto anchorToPoint = [](UIAnchor anchor, const ImVec2& min, const ImVec2& max) { + switch (anchor) { + case UIAnchor::Center: return ImVec2((min.x + max.x) * 0.5f, (min.y + max.y) * 0.5f); + case UIAnchor::TopLeft: return min; + case UIAnchor::TopRight: return ImVec2(max.x, min.y); + case UIAnchor::BottomLeft: return ImVec2(min.x, max.y); + case UIAnchor::BottomRight: return max; + default: return ImVec2((min.x + max.x) * 0.5f, (min.y + max.y) * 0.5f); + } + }; + + auto resolveUIRect = [&](const SceneObject& obj, ImVec2& outMin, ImVec2& outMax) { + std::vector chain; + const SceneObject* current = &obj; + while (current) { + if (isUIType(*current)) { + chain.push_back(current); + } + if (current->parentId < 0) break; + auto pit = std::find_if(sceneObjects.begin(), sceneObjects.end(), + [&](const SceneObject& o) { return o.id == current->parentId; }); + if (pit == sceneObjects.end()) break; + current = &(*pit); + } + std::reverse(chain.begin(), chain.end()); + + ImVec2 regionMin = ImGui::GetWindowPos(); + ImVec2 regionMax = ImVec2(regionMin.x + ImGui::GetWindowWidth(), regionMin.y + ImGui::GetWindowHeight()); + for (const SceneObject* node : chain) { + ImVec2 size = ImVec2(std::max(1.0f, node->ui.size.x * uiScaleX), + std::max(1.0f, node->ui.size.y * uiScaleY)); + ImVec2 anchorPoint = anchorToPoint(node->ui.anchor, regionMin, regionMax); + ImVec2 pivot(anchorPoint.x + node->ui.position.x * uiScaleX, + anchorPoint.y + node->ui.position.y * uiScaleY); + ImVec2 pivotOffset = anchorToPivot(node->ui.anchor, size); + regionMin = ImVec2(pivot.x - pivotOffset.x, pivot.y - pivotOffset.y); + regionMax = ImVec2(regionMin.x + size.x, regionMin.y + size.y); + } + outMin = regionMin; + outMax = regionMax; + }; + + ImVec2 overlayPos = ImGui::GetWindowPos(); + ImVec2 overlaySize = ImGui::GetWindowSize(); + bool useWorldUi = uiWorldMode; + if (!useWorldUi) { + uiWorldPanning = false; + } + if (useWorldUi) { + uiWorldCamera.viewportSize = glm::vec2(overlaySize.x, overlaySize.y); + } + auto worldToScreen = [&](const glm::vec2& world) { + glm::vec2 local = uiWorldCamera.WorldToScreen(world); + return ImVec2(overlayPos.x + local.x, overlayPos.y + local.y); + }; + auto screenToWorld = [&](const ImVec2& screen) { + glm::vec2 local(screen.x - overlayPos.x, screen.y - overlayPos.y); + return uiWorldCamera.ScreenToWorld(local); + }; + auto getWorldParentOffset = [&](const SceneObject& obj) { + glm::vec2 offset(0.0f); + const SceneObject* current = &obj; + while (current && current->parentId >= 0) { + auto pit = std::find_if(sceneObjects.begin(), sceneObjects.end(), + [&](const SceneObject& o) { return o.id == current->parentId; }); + if (pit == sceneObjects.end()) break; + current = &(*pit); + if (current->hasUI && current->ui.type != UIElementType::None) { + offset += glm::vec2(current->ui.position.x, current->ui.position.y); + } + } + return offset; + }; + auto resolveUIRectWorld = [&](const SceneObject& obj, ImVec2& outMin, ImVec2& outMax) { + glm::vec2 parentOffset = getWorldParentOffset(obj); + glm::vec2 worldPos = parentOffset + glm::vec2(obj.ui.position.x, obj.ui.position.y); + glm::vec2 sizeWorld(obj.ui.size.x, obj.ui.size.y); + ImVec2 pivotOffset = anchorToPivot(obj.ui.anchor, ImVec2(sizeWorld.x, sizeWorld.y)); + glm::vec2 worldMin = worldPos - glm::vec2(pivotOffset.x, pivotOffset.y); + glm::vec2 worldMax = worldMin + sizeWorld; + ImVec2 s0 = worldToScreen(worldMin); + ImVec2 s1 = worldToScreen(worldMax); + outMin = ImVec2(std::min(s0.x, s1.x), std::min(s0.y, s1.y)); + outMax = ImVec2(std::max(s0.x, s1.x), std::max(s0.y, s1.y)); + }; + auto rectOutsideOverlay = [&](const ImVec2& min, const ImVec2& max) { + return (max.x < overlayPos.x || min.x > overlayPos.x + overlaySize.x || + max.y < overlayPos.y || min.y > overlayPos.y + overlaySize.y); + }; + + bool uiWorldHover = imageHovered || ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); + bool uiWorldCameraActive = false; + if (useWorldUi) { + ImGuiIO& io = ImGui::GetIO(); + bool panHeld = uiWorldHover && (ImGui::IsMouseDown(ImGuiMouseButton_Middle) || + (ImGui::IsKeyDown(ImGuiKey_Space) && ImGui::IsMouseDown(ImGuiMouseButton_Left))); + if (panHeld) { + uiWorldPanning = true; + } else if (!ImGui::IsMouseDown(ImGuiMouseButton_Middle) && + !(ImGui::IsKeyDown(ImGuiKey_Space) && ImGui::IsMouseDown(ImGuiMouseButton_Left))) { + uiWorldPanning = false; + } + if (uiWorldPanning) { + ImVec2 delta = io.MouseDelta; + if (delta.x != 0.0f || delta.y != 0.0f) { + uiWorldCamera.position.x -= delta.x / uiWorldCamera.zoom; + uiWorldCamera.position.y += delta.y / uiWorldCamera.zoom; + } + uiWorldCameraActive = true; + } + if (uiWorldHover && io.MouseWheel != 0.0f) { + glm::vec2 mouseLocal(io.MousePos.x - overlayPos.x, io.MousePos.y - overlayPos.y); + glm::vec2 worldBefore = uiWorldCamera.ScreenToWorld(mouseLocal); + float zoomFactor = 1.0f + io.MouseWheel * 0.1f; + float newZoom = std::clamp(uiWorldCamera.zoom * zoomFactor, 5.0f, 2000.0f); + if (newZoom != uiWorldCamera.zoom) { + uiWorldCamera.zoom = newZoom; + glm::vec2 worldAfter = uiWorldCamera.ScreenToWorld(mouseLocal); + uiWorldCamera.position += (worldBefore - worldAfter); + uiWorldCameraActive = true; + } + } + if (uiWorldHover) { + glm::vec2 panDir(0.0f); + if (ImGui::IsKeyDown(ImGuiKey_A)) panDir.x -= 1.0f; + if (ImGui::IsKeyDown(ImGuiKey_D)) panDir.x += 1.0f; + if (ImGui::IsKeyDown(ImGuiKey_W)) panDir.y += 1.0f; + if (ImGui::IsKeyDown(ImGuiKey_S)) panDir.y -= 1.0f; + if (panDir.x != 0.0f || panDir.y != 0.0f) { + float panSpeed = 6.0f; + if (ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) { + panSpeed *= 2.5f; + } + uiWorldCamera.position += panDir * (panSpeed * deltaTime); + uiWorldCameraActive = true; + } + } + } + + auto brighten = [](const ImVec4& c, float k) { + return ImVec4(std::clamp(c.x * k, 0.0f, 1.0f), + std::clamp(c.y * k, 0.0f, 1.0f), + std::clamp(c.z * k, 0.0f, 1.0f), + c.w); + }; + float animSpeed = 0.0f; + if (uiAnimationMode == UIAnimationMode::Fluid) { + animSpeed = 8.0f; + } else if (uiAnimationMode == UIAnimationMode::Snappy) { + animSpeed = 18.0f; + } + float animStep = (uiAnimationMode == UIAnimationMode::Off) ? 1.0f + : (1.0f - std::exp(-animSpeed * ImGui::GetIO().DeltaTime)); + auto animateValue = [&](float& current, float target, bool immediate) { + if (uiAnimationMode == UIAnimationMode::Off || immediate) { + current = target; + } else { + current += (target - current) * animStep; + } + return current; + }; + + if (useWorldUi && showUIWorldGrid) { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImVec2 overlayMax(overlayPos.x + overlaySize.x, overlayPos.y + overlaySize.y); + dl->PushClipRect(overlayPos, overlayMax, true); + float step = 1.0f; + float minPx = 30.0f; + float maxPx = 140.0f; + while (step * uiWorldCamera.zoom < minPx) step *= 2.0f; + while (step * uiWorldCamera.zoom > maxPx) step *= 0.5f; + + glm::vec2 worldMin = uiWorldCamera.ScreenToWorld(glm::vec2(0.0f, overlaySize.y)); + glm::vec2 worldMax = uiWorldCamera.ScreenToWorld(glm::vec2(overlaySize.x, 0.0f)); + float startX = std::floor(worldMin.x / step) * step; + float endX = std::ceil(worldMax.x / step) * step; + float startY = std::floor(worldMin.y / step) * step; + float endY = std::ceil(worldMax.y / step) * step; + ImU32 gridColor = IM_COL32(90, 110, 140, 50); + ImU32 axisColorX = IM_COL32(240, 120, 120, 170); + ImU32 axisColorY = IM_COL32(120, 240, 150, 170); + + for (float x = startX; x <= endX; x += step) { + ImVec2 p0 = worldToScreen(glm::vec2(x, worldMin.y)); + ImVec2 p1 = worldToScreen(glm::vec2(x, worldMax.y)); + dl->AddLine(p0, p1, gridColor, 1.0f); + } + for (float y = startY; y <= endY; y += step) { + ImVec2 p0 = worldToScreen(glm::vec2(worldMin.x, y)); + ImVec2 p1 = worldToScreen(glm::vec2(worldMax.x, y)); + dl->AddLine(p0, p1, gridColor, 1.0f); + } + + ImVec2 axisX0 = worldToScreen(glm::vec2(worldMin.x, 0.0f)); + ImVec2 axisX1 = worldToScreen(glm::vec2(worldMax.x, 0.0f)); + ImVec2 axisY0 = worldToScreen(glm::vec2(0.0f, worldMin.y)); + ImVec2 axisY1 = worldToScreen(glm::vec2(0.0f, worldMax.y)); + dl->AddLine(axisX0, axisX1, axisColorX, 2.0f); + dl->AddLine(axisY0, axisY1, axisColorY, 2.0f); + + ImVec2 indicator = ImVec2(overlayPos.x + 36.0f, overlayPos.y + overlaySize.y - 36.0f); + dl->AddLine(indicator, ImVec2(indicator.x + 22.0f, indicator.y), axisColorX, 2.0f); + dl->AddLine(indicator, ImVec2(indicator.x, indicator.y - 22.0f), axisColorY, 2.0f); + dl->AddText(ImVec2(indicator.x + 26.0f, indicator.y - 8.0f), axisColorX, "+X"); + dl->AddText(ImVec2(indicator.x - 16.0f, indicator.y - 30.0f), axisColorY, "+Y"); + dl->PopClipRect(); + } + + for (auto& obj : sceneObjects) { + if (!obj.enabled || !isUIType(obj)) continue; + ImVec2 rectMin, rectMax; + if (useWorldUi) { + resolveUIRectWorld(obj, rectMin, rectMax); + } else { + resolveUIRect(obj, rectMin, rectMax); + } + ImVec2 rectSize(rectMax.x - rectMin.x, rectMax.y - rectMin.y); + if (rectSize.x <= 1.0f || rectSize.y <= 1.0f) continue; + + ImGuiStyle savedStyle = ImGui::GetStyle(); + bool styleApplied = false; + if (!obj.ui.stylePreset.empty()) { + if (const auto* preset = getUIStylePreset(obj.ui.stylePreset)) { + ImGui::GetStyle() = preset->style; + styleApplied = true; + } + } + + if (obj.ui.type == UIElementType::Canvas) { + ImDrawList* dl = ImGui::GetWindowDrawList(); + dl->AddRect(rectMin, rectMax, IM_COL32(110, 170, 255, 140), 6.0f, 0, 1.5f); + if (styleApplied) ImGui::GetStyle() = savedStyle; + continue; + } + + ImVec2 drawMin = rectMin; + ImVec2 drawMax = rectMax; + ImVec2 drawSize(drawMax.x - drawMin.x, drawMax.y - drawMin.y); + ImVec2 localMin(drawMin.x - overlayPos.x, drawMin.y - overlayPos.y); + + ImGui::PushID(obj.id); + UIAnimationState& animState = uiAnimationStates[obj.id]; + if (!animState.initialized) { + animState.sliderValue = obj.ui.sliderValue; + animState.initialized = true; + } + if (obj.ui.type == UIElementType::Image || obj.ui.type == UIElementType::Sprite2D) { + unsigned int texId = 0; + if (!obj.albedoTexturePath.empty()) { + if (auto* tex = renderer.getTexture(obj.albedoTexturePath)) { + texId = tex->GetID(); + } + } + ImVec4 tint(obj.ui.color.r, obj.ui.color.g, obj.ui.color.b, obj.ui.color.a); + float angle = glm::radians(obj.ui.rotation); + if (std::abs(angle) > 1e-4f) { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImVec2 center = ImVec2((drawMin.x + drawMax.x) * 0.5f, (drawMin.y + drawMax.y) * 0.5f); + ImVec2 half = ImVec2(drawSize.x * 0.5f, drawSize.y * 0.5f); + float c = std::cos(angle); + float s = std::sin(angle); + auto rotPt = [&](float x, float y) { + return ImVec2(center.x + x * c - y * s, center.y + x * s + y * c); + }; + ImVec2 p0 = rotPt(-half.x, -half.y); + ImVec2 p1 = rotPt( half.x, -half.y); + ImVec2 p2 = rotPt( half.x, half.y); + ImVec2 p3 = rotPt(-half.x, half.y); + if (texId != 0) { + dl->AddImageQuad((ImTextureID)(intptr_t)texId, p0, p1, p2, p3, + ImVec2(0, 1), ImVec2(1, 1), ImVec2(1, 0), ImVec2(0, 0), + ImGui::GetColorU32(tint)); + } else { + ImU32 fill = ImGui::GetColorU32(tint); + ImU32 border = ImGui::GetColorU32(brighten(tint, 0.85f)); + dl->AddQuadFilled(p0, p1, p2, p3, fill); + dl->AddQuad(p0, p1, p2, p3, border, 2.0f); + ImVec2 textSize = ImGui::CalcTextSize(obj.ui.label.c_str()); + ImVec2 textPos(center.x - textSize.x * 0.5f, center.y - textSize.y * 0.5f); + dl->AddText(textPos, IM_COL32(210, 210, 220, 220), obj.ui.label.c_str()); + } + ImGui::Dummy(drawSize); + } else { + ImGui::SetCursorPos(localMin); + if (texId != 0) { + ImGui::Image((ImTextureID)(intptr_t)texId, drawSize, ImVec2(0, 1), ImVec2(1, 0), tint, ImVec4(0, 0, 0, 0)); + } else { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImU32 fill = ImGui::GetColorU32(tint); + ImU32 border = ImGui::GetColorU32(brighten(tint, 0.85f)); + dl->AddRectFilled(drawMin, drawMax, fill, 6.0f); + dl->AddRect(drawMin, drawMax, border, 6.0f); + ImVec2 textSize = ImGui::CalcTextSize(obj.ui.label.c_str()); + ImVec2 textPos(drawMin.x + (drawSize.x - textSize.x) * 0.5f, + drawMin.y + (drawSize.y - textSize.y) * 0.5f); + dl->AddText(textPos, IM_COL32(210, 210, 220, 220), obj.ui.label.c_str()); + ImGui::Dummy(drawSize); + } + } + } else if (obj.ui.type == UIElementType::Slider) { + ImGui::SetCursorPos(localMin); + ImVec4 tint(obj.ui.color.r, obj.ui.color.g, obj.ui.color.b, obj.ui.color.a); + if (obj.ui.sliderStyle == UISliderStyle::ImGui) { + ImGui::PushItemWidth(drawSize.x); + ImGui::BeginDisabled(!obj.ui.interactable || uiWorldCameraActive); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(tint.x * 0.2f, tint.y * 0.2f, tint.z * 0.2f, tint.w * 0.6f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, brighten(tint, 0.5f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, brighten(tint, 0.7f)); + ImGui::PushStyleColor(ImGuiCol_SliderGrab, brighten(tint, 0.9f)); + ImGui::PushStyleColor(ImGuiCol_SliderGrabActive, brighten(tint, 1.1f)); + if (ImGui::SliderFloat(obj.ui.label.c_str(), &obj.ui.sliderValue, obj.ui.sliderMin, obj.ui.sliderMax)) { + projectManager.currentProject.hasUnsavedChanges = true; + } + ImGui::PopStyleColor(5); + ImGui::EndDisabled(); + ImGui::PopItemWidth(); + } else { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImU32 bg = ImGui::GetColorU32(ImVec4(tint.x * 0.2f, tint.y * 0.2f, tint.z * 0.2f, tint.w * 0.6f)); + ImU32 fill = ImGui::GetColorU32(tint); + ImU32 border = ImGui::GetColorU32(brighten(tint, 0.85f)); + float minValue = obj.ui.sliderMin; + float maxValue = obj.ui.sliderMax; + float range = (maxValue - minValue); + if (range <= 1e-6f) range = 1.0f; + ImGui::BeginDisabled(!obj.ui.interactable || uiWorldCameraActive); + ImGui::InvisibleButton("##UISlider", drawSize); + bool held = obj.ui.interactable && ImGui::IsItemActive(); + if (held && ImGui::IsMouseDown(ImGuiMouseButton_Left) && drawSize.x > 1.0f) { + float mouseT = (ImGui::GetIO().MousePos.x - drawMin.x) / drawSize.x; + mouseT = std::clamp(mouseT, 0.0f, 1.0f); + float newValue = minValue + mouseT * range; + if (newValue != obj.ui.sliderValue) { + obj.ui.sliderValue = newValue; + projectManager.currentProject.hasUnsavedChanges = true; + } + } + ImGui::EndDisabled(); + + animateValue(animState.sliderValue, obj.ui.sliderValue, held); + float displayValue = (uiAnimationMode == UIAnimationMode::Off) ? obj.ui.sliderValue : animState.sliderValue; + float t = (displayValue - minValue) / range; + t = std::clamp(t, 0.0f, 1.0f); + + if (obj.ui.sliderStyle == UISliderStyle::Fill) { + float rounding = 6.0f; + ImVec2 fillMax(drawMin.x + drawSize.x * t, drawMax.y); + dl->AddRectFilled(drawMin, drawMax, bg, rounding); + if (fillMax.x > drawMin.x) { + dl->AddRectFilled(drawMin, fillMax, fill, rounding); + } + dl->AddRect(drawMin, drawMax, border, rounding); + ImVec2 textSize = ImGui::CalcTextSize(obj.ui.label.c_str()); + ImVec2 textPos(drawMin.x + (drawSize.x - textSize.x) * 0.5f, + drawMin.y + (drawSize.y - textSize.y) * 0.5f); + dl->AddText(textPos, IM_COL32(240, 240, 245, 220), obj.ui.label.c_str()); + } else if (obj.ui.sliderStyle == UISliderStyle::Circle) { + ImVec2 center((drawMin.x + drawMax.x) * 0.5f, (drawMin.y + drawMax.y) * 0.5f); + float radius = std::max(2.0f, std::min(drawSize.x, drawSize.y) * 0.5f - 2.0f); + dl->AddCircleFilled(center, radius, bg, 32); + float start = -IM_PI * 0.5f; + float end = start + t * IM_PI * 2.0f; + dl->PathClear(); + dl->PathArcTo(center, radius, start, end, 32); + dl->PathLineTo(center); + dl->PathFillConvex(fill); + dl->AddCircle(center, radius, border, 32, 2.0f); + ImVec2 textSize = ImGui::CalcTextSize(obj.ui.label.c_str()); + ImVec2 textPos(center.x - textSize.x * 0.5f, center.y - textSize.y * 0.5f); + dl->AddText(textPos, IM_COL32(240, 240, 245, 220), obj.ui.label.c_str()); + } + } + } else if (obj.ui.type == UIElementType::Button) { + ImGui::SetCursorPos(localMin); + ImVec4 tint(obj.ui.color.r, obj.ui.color.g, obj.ui.color.b, obj.ui.color.a); + obj.ui.buttonPressed = false; + if (obj.ui.buttonStyle == UIButtonStyle::ImGui) { + ImGui::PushStyleColor(ImGuiCol_Button, tint); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, brighten(tint, 1.1f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, brighten(tint, 1.2f)); + ImGui::BeginDisabled(!obj.ui.interactable || uiWorldCameraActive); + obj.ui.buttonPressed = ImGui::Button(obj.ui.label.c_str(), drawSize); + ImGui::EndDisabled(); + ImGui::PopStyleColor(3); + } else if (obj.ui.buttonStyle == UIButtonStyle::Outline) { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImU32 border = ImGui::GetColorU32(tint); + ImGui::BeginDisabled(!obj.ui.interactable || uiWorldCameraActive); + if (ImGui::InvisibleButton("##UIButton", drawSize)) { + obj.ui.buttonPressed = obj.ui.interactable; + } + bool hovered = ImGui::IsItemHovered(); + bool active = ImGui::IsItemActive(); + ImGui::EndDisabled(); + float hoverT = animateValue(animState.hover, hovered ? 1.0f : 0.0f, false); + float activeT = animateValue(animState.active, active ? 1.0f : 0.0f, false); + if (hoverT > 0.001f) { + ImVec4 hoverCol = brighten(tint, 0.45f); + hoverCol.w *= std::clamp(hoverT, 0.0f, 1.0f); + dl->AddRectFilled(drawMin, drawMax, ImGui::GetColorU32(hoverCol), 6.0f); + } + if (activeT > 0.001f) { + ImVec4 activeCol = brighten(tint, 0.65f); + activeCol.w *= std::clamp(activeT, 0.0f, 1.0f); + dl->AddRectFilled(drawMin, drawMax, ImGui::GetColorU32(activeCol), 6.0f); + } + dl->AddRect(drawMin, drawMax, border, 6.0f, 0, 2.0f); + ImVec2 textSize = ImGui::CalcTextSize(obj.ui.label.c_str()); + ImVec2 textPos(drawMin.x + (drawSize.x - textSize.x) * 0.5f, + drawMin.y + (drawSize.y - textSize.y) * 0.5f); + dl->AddText(textPos, IM_COL32(240, 240, 245, 220), obj.ui.label.c_str()); + } + } else if (obj.ui.type == UIElementType::Text) { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImVec4 tint(obj.ui.color.r, obj.ui.color.g, obj.ui.color.b, obj.ui.color.a); + float scale = std::max(0.1f, obj.ui.textScale); + float scaleFactor = useWorldUi ? std::max(0.01f, uiWorldCamera.zoom / 100.0f) + : std::min(uiScaleX, uiScaleY); + float fontSize = std::max(1.0f, ImGui::GetFontSize() * scale * scaleFactor); + ImVec2 textPos = ImVec2(drawMin.x + 4.0f, drawMin.y + 2.0f); + ImGui::PushClipRect(drawMin, drawMax, true); + dl->AddText(ImGui::GetFont(), fontSize, textPos, ImGui::GetColorU32(tint), obj.ui.label.c_str()); + ImGui::PopClipRect(); + } + ImGui::PopID(); + if (styleApplied) ImGui::GetStyle() = savedStyle; + } + + uiInteracting = ImGui::IsAnyItemActive() || uiWorldCameraActive; + ImGui::EndChild(); + ImGui::PopStyleVar(); + + bool clicked = imageHovered && isPlaying && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !uiInteracting; + if (clicked && !gameViewCursorLocked) { + gameViewCursorLocked = true; + } + if (gameViewCursorLocked && (!isPlaying || ImGui::IsKeyPressed(ImGuiKey_Escape))) { + gameViewCursorLocked = false; + } + gameViewportFocused = windowFocused || gameViewCursorLocked; + } + + ImGui::End(); +} +#pragma endregion diff --git a/src/Engine.cpp b/src/Engine.cpp index bc9cd2d..b75da92 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -4,8 +4,14 @@ #include #include #include +#include +#include +#include +#include +#include #include #include +#include #include "ThirdParty/glm/gtc/constants.hpp" #pragma region Material File IO Helpers @@ -20,6 +26,57 @@ struct MaterialFileData { std::string fragmentShader; }; +bool IsDefaultTransform(const SceneObject& obj) { + auto nearZero = [](float v) { return std::abs(v) < 1e-4f; }; + auto nearOne = [](float v) { return std::abs(v - 1.0f) < 1e-4f; }; + return nearZero(obj.localPosition.x) && + nearZero(obj.localPosition.y) && + nearZero(obj.localPosition.z) && + nearZero(obj.localRotation.x) && + nearZero(obj.localRotation.y) && + nearZero(obj.localRotation.z) && + nearOne(obj.localScale.x) && + nearOne(obj.localScale.y) && + nearOne(obj.localScale.z); +} + +void ApplyModelRootTransform(SceneObject& obj, const ModelSceneData& sceneData) { + if (sceneData.nodes.empty()) return; + if (obj.localInitialized && !IsDefaultTransform(obj)) return; + const auto& root = sceneData.nodes.front(); + obj.localPosition = root.localPosition; + obj.localRotation = root.localRotation; + obj.localScale = root.localScale; + obj.localInitialized = true; + obj.position = obj.localPosition; + obj.rotation = obj.localRotation; + obj.scale = obj.localScale; +} + +std::string sanitizeMaterialName(const std::string& name) { + std::string out; + out.reserve(name.size()); + for (char c : name) { + if ((c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || + c == '_' || c == '-') { + out.push_back(c); + } else if (c == ' ' || c == '.') { + out.push_back('_'); + } + } + if (out.empty()) out = "Material"; + return out; +} + +std::string resolveTexturePath(const std::string& texPath, const fs::path& modelPath) { + if (texPath.empty() || texPath[0] == '*') return ""; + fs::path p(texPath); + if (p.is_absolute()) return p.string(); + return (modelPath.parent_path() / p).string(); +} + bool readMaterialFile(const std::string& path, MaterialFileData& outData) { std::ifstream f(path); if (!f.is_open()) { @@ -81,6 +138,136 @@ bool writeMaterialFile(const MaterialFileData& data, const std::string& path) { return true; } +void ApplyObjectPreset(SceneObject& obj, ObjectType preset) { + obj.type = preset; + obj.hasRenderer = false; + obj.renderType = RenderType::None; + obj.hasLight = false; + obj.hasCamera = false; + obj.hasPostFX = false; + obj.hasUI = false; + obj.ui.type = UIElementType::None; + + switch (preset) { + case ObjectType::Cube: + obj.hasRenderer = true; + obj.renderType = RenderType::Cube; + break; + case ObjectType::Sphere: + obj.hasRenderer = true; + obj.renderType = RenderType::Sphere; + break; + case ObjectType::Capsule: + obj.hasRenderer = true; + obj.renderType = RenderType::Capsule; + break; + case ObjectType::OBJMesh: + obj.hasRenderer = true; + obj.renderType = RenderType::OBJMesh; + break; + case ObjectType::Model: + obj.hasRenderer = true; + obj.renderType = RenderType::Model; + break; + case ObjectType::Mirror: + obj.hasRenderer = true; + obj.renderType = RenderType::Mirror; + obj.useOverlay = true; + obj.material.textureMix = 1.0f; + obj.material.color = glm::vec3(1.0f); + obj.scale = glm::vec3(2.0f, 2.0f, 0.05f); + break; + case ObjectType::Plane: + obj.hasRenderer = true; + obj.renderType = RenderType::Plane; + obj.scale = glm::vec3(2.0f, 2.0f, 0.05f); + break; + case ObjectType::Torus: + obj.hasRenderer = true; + obj.renderType = RenderType::Torus; + break; + case ObjectType::Sprite: + obj.hasRenderer = true; + obj.renderType = RenderType::Sprite; + obj.scale = glm::vec3(1.0f, 1.0f, 0.05f); + obj.material.ambientStrength = 1.0f; + break; + case ObjectType::DirectionalLight: + obj.hasLight = true; + obj.light.type = LightType::Directional; + break; + case ObjectType::PointLight: + obj.hasLight = true; + obj.light.type = LightType::Point; + obj.light.range = 12.0f; + obj.light.intensity = 2.0f; + break; + case ObjectType::SpotLight: + obj.hasLight = true; + obj.light.type = LightType::Spot; + obj.light.range = 15.0f; + obj.light.intensity = 2.5f; + break; + case ObjectType::AreaLight: + obj.hasLight = true; + obj.light.type = LightType::Area; + obj.light.range = 10.0f; + obj.light.intensity = 3.0f; + obj.light.size = glm::vec2(2.0f, 2.0f); + break; + case ObjectType::Camera: + obj.hasCamera = true; + obj.camera.type = SceneCameraType::Player; + obj.camera.fov = 60.0f; + break; + case ObjectType::PostFXNode: + obj.hasPostFX = true; + obj.postFx.enabled = true; + obj.postFx.bloomEnabled = true; + obj.postFx.colorAdjustEnabled = true; + break; + case ObjectType::Canvas: + obj.hasUI = true; + obj.ui.type = UIElementType::Canvas; + obj.ui.label = "Canvas"; + obj.ui.size = glm::vec2(600.0f, 400.0f); + break; + case ObjectType::UIImage: + obj.hasUI = true; + obj.ui.type = UIElementType::Image; + obj.ui.label = "Image"; + obj.ui.size = glm::vec2(200.0f, 200.0f); + break; + case ObjectType::UISlider: + obj.hasUI = true; + obj.ui.type = UIElementType::Slider; + obj.ui.label = "Slider"; + obj.ui.size = glm::vec2(240.0f, 32.0f); + break; + case ObjectType::UIButton: + obj.hasUI = true; + obj.ui.type = UIElementType::Button; + obj.ui.label = "Button"; + obj.ui.size = glm::vec2(160.0f, 40.0f); + break; + case ObjectType::UIText: + obj.hasUI = true; + obj.ui.type = UIElementType::Text; + obj.ui.label = "Text"; + obj.ui.size = glm::vec2(240.0f, 32.0f); + break; + case ObjectType::Sprite2D: + obj.hasUI = true; + obj.ui.type = UIElementType::Sprite2D; + obj.ui.label = "Sprite2D"; + obj.ui.size = glm::vec2(128.0f, 128.0f); + break; + case ObjectType::Empty: + default: + break; + } +} + RawMeshAsset buildCubeRMesh() { RawMeshAsset mesh; mesh.positions.reserve(24); @@ -88,38 +275,56 @@ RawMeshAsset buildCubeRMesh() { mesh.uvs.reserve(24); mesh.faces.reserve(12); - struct Face { - glm::vec3 n; - glm::vec3 v[4]; - }; - const float h = 0.5f; - Face faces[] = { - { glm::vec3(0, 0, 1), { {-h,-h, h}, { h,-h, h}, { h, h, h}, {-h, h, h} } }, // +Z - { glm::vec3(0, 0,-1), { { h,-h,-h}, {-h,-h,-h}, {-h, h,-h}, { h, h,-h} } }, // -Z - { glm::vec3(1, 0, 0), { { h,-h, h}, { h,-h,-h}, { h, h,-h}, { h, h, h} } }, // +X - { glm::vec3(-1,0, 0), { {-h,-h,-h}, {-h,-h, h}, {-h, h, h}, {-h, h,-h} } }, // -X - { glm::vec3(0, 1, 0), { {-h, h, h}, { h, h, h}, { h, h,-h}, {-h, h,-h} } }, // +Y - { glm::vec3(0,-1, 0), { {-h,-h,-h}, { h,-h,-h}, { h,-h, h}, {-h,-h, h} } }, // -Y - }; - - glm::vec2 uvs[4] = { - glm::vec2(0, 0), - glm::vec2(1, 0), - glm::vec2(1, 1), - glm::vec2(0, 1), - }; - - for (const auto& f : faces) { + auto pushFace = [&](const glm::vec3& n, const glm::vec3& uAxis, const glm::vec3& vAxis, + const glm::vec3& v0, const glm::vec3& v1, + const glm::vec3& v2, const glm::vec3& v3) { uint32_t base = static_cast(mesh.positions.size()); - for (int i = 0; i < 4; ++i) { - mesh.positions.push_back(f.v[i]); - mesh.normals.push_back(f.n); - mesh.uvs.push_back(uvs[i]); - } + mesh.positions.push_back(v0); + mesh.positions.push_back(v1); + mesh.positions.push_back(v2); + mesh.positions.push_back(v3); + mesh.normals.push_back(n); + mesh.normals.push_back(n); + mesh.normals.push_back(n); + mesh.normals.push_back(n); + auto toUv = [&](const glm::vec3& p) -> glm::vec2 { + float u = glm::dot(p, uAxis) / (2.0f * h) + 0.5f; + float v = glm::dot(p, vAxis) / (2.0f * h) + 0.5f; + return glm::vec2(u, v); + }; + mesh.uvs.push_back(toUv(v0)); + mesh.uvs.push_back(toUv(v1)); + mesh.uvs.push_back(toUv(v2)); + mesh.uvs.push_back(toUv(v3)); mesh.faces.push_back(glm::u32vec3(base, base + 1, base + 2)); mesh.faces.push_back(glm::u32vec3(base, base + 2, base + 3)); - } + }; + + // +Z (front) + pushFace(glm::vec3(0.0f, 0.0f, 1.0f), glm::vec3(1.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f), + glm::vec3(-h, -h, h), glm::vec3( h, -h, h), + glm::vec3( h, h, h), glm::vec3(-h, h, h)); + // -Z (back) + pushFace(glm::vec3(0.0f, 0.0f, -1.0f), glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f), + glm::vec3( h, -h, -h), glm::vec3(-h, -h, -h), + glm::vec3(-h, h, -h), glm::vec3( h, h, -h)); + // -X (left) + pushFace(glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f), glm::vec3(0.0f, 1.0f, 0.0f), + glm::vec3(-h, -h, -h), glm::vec3(-h, -h, h), + glm::vec3(-h, h, h), glm::vec3(-h, h, -h)); + // +X (right) + pushFace(glm::vec3(1.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f), glm::vec3(0.0f, 1.0f, 0.0f), + glm::vec3( h, -h, h), glm::vec3( h, -h, -h), + glm::vec3( h, h, -h), glm::vec3( h, h, h)); + // +Y (top) + pushFace(glm::vec3(0.0f, 1.0f, 0.0f), glm::vec3(1.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f), + glm::vec3(-h, h, h), glm::vec3( h, h, h), + glm::vec3( h, h, -h), glm::vec3(-h, h, -h)); + // -Y (bottom) + pushFace(glm::vec3(0.0f, -1.0f, 0.0f), glm::vec3(1.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f), + glm::vec3(-h, -h, -h), glm::vec3( h, -h, -h), + glm::vec3( h, -h, h), glm::vec3(-h, -h, h)); mesh.boundsMin = glm::vec3(-h); mesh.boundsMax = glm::vec3(h); @@ -199,8 +404,298 @@ RawMeshAsset buildSphereRMesh(int slices = 24, int stacks = 16) { } // namespace #pragma endregion +#pragma region Build Helpers +namespace { +bool runCommandCapture(const std::string& command, std::string& output) { + std::array buffer{}; +#ifdef _WIN32 + FILE* pipe = _popen(command.c_str(), "r"); +#else + FILE* pipe = popen(command.c_str(), "r"); +#endif + if (!pipe) { + output = "Failed to spawn process: " + command; + return false; + } + + while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != nullptr) { + output += buffer.data(); + } + +#ifdef _WIN32 + int returnCode = _pclose(pipe); +#else + int returnCode = pclose(pipe); +#endif + if (returnCode != 0) { + return false; + } + return true; +} + +bool runCommandStreaming(const std::string& command, + const std::function& onChunk, + int* exitCodeOut) { + std::array buffer{}; +#ifdef _WIN32 + FILE* pipe = _popen(command.c_str(), "r"); +#else + FILE* pipe = popen(command.c_str(), "r"); +#endif + if (!pipe) { + if (exitCodeOut) *exitCodeOut = -1; + return false; + } + + while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != nullptr) { + if (onChunk) { + onChunk(buffer.data()); + } + } + +#ifdef _WIN32 + int returnCode = _pclose(pipe); +#else + int returnCode = pclose(pipe); +#endif + if (exitCodeOut) *exitCodeOut = returnCode; + return returnCode == 0; +} + +fs::path resolveExecutablePath(const fs::path& buildRoot, const char* exeBaseName) { +#ifdef _WIN32 + std::string exeName = std::string(exeBaseName) + ".exe"; +#else + std::string exeName = exeBaseName; +#endif + + std::vector candidates; + candidates.push_back(buildRoot / exeName); + candidates.push_back(buildRoot / "Release" / exeName); + candidates.push_back(buildRoot / "RelWithDebInfo" / exeName); + candidates.push_back(buildRoot / "MinSizeRel" / exeName); + candidates.push_back(buildRoot / "Debug" / exeName); + + for (const auto& path : candidates) { + if (fs::exists(path)) return path; + } + + for (const auto& entry : fs::recursive_directory_iterator(buildRoot)) { + if (!entry.is_regular_file()) continue; + if (entry.path().filename() == exeName) return entry.path(); + } + return {}; +} + +fs::path findCMakeSourceRoot(const fs::path& start) { + std::error_code ec; + fs::path cur = fs::absolute(start, ec); + if (ec) return {}; + while (!cur.empty()) { + fs::path candidate = cur / "CMakeLists.txt"; + if (fs::exists(candidate)) return cur; + if (!cur.has_parent_path()) break; + fs::path parent = cur.parent_path(); + if (parent == cur) break; + cur = parent; + } + return {}; +} + +bool copyDirectoryRecursive(const fs::path& from, const fs::path& to, std::string& error) { + std::error_code ec; + if (!fs::exists(from)) return true; + fs::create_directories(to, ec); + if (ec) { + error = "Failed to create directory: " + to.string(); + return false; + } + + for (const auto& entry : fs::recursive_directory_iterator(from)) { + const auto& src = entry.path(); + fs::path rel = fs::relative(src, from, ec); + if (ec) { + error = "Failed to resolve relative path: " + src.string(); + return false; + } + fs::path dst = to / rel; + if (entry.is_directory()) { + fs::create_directories(dst, ec); + if (ec) { + error = "Failed to create directory: " + dst.string(); + return false; + } + } else if (entry.is_regular_file()) { + fs::create_directories(dst.parent_path(), ec); + if (ec) { + error = "Failed to create directory: " + dst.parent_path().string(); + return false; + } + fs::copy_file(src, dst, fs::copy_options::overwrite_existing, ec); + if (ec) { + error = "Failed to copy file: " + src.string(); + return false; + } + } + } + return true; +} + +bool copyPrecompiledPackages(const fs::path& buildRoot, const fs::path& outDir, std::string& error) { + std::error_code ec; + if (!fs::exists(buildRoot)) return true; + + if (fs::exists(outDir)) { + fs::remove_all(outDir, ec); + if (ec) { + error = "Failed to clear package cache: " + outDir.string(); + return false; + } + } + fs::create_directories(outDir, ec); + if (ec) { + error = "Failed to create package folder: " + outDir.string(); + return false; + } + + const std::vector exts = { ".a", ".so", ".dylib", ".lib", ".dll" }; + for (const auto& entry : fs::recursive_directory_iterator(buildRoot)) { + if (!entry.is_regular_file()) continue; + fs::path src = entry.path(); + std::string ext = src.extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); + if (std::find(exts.begin(), exts.end(), ext) == exts.end()) { + continue; + } + + fs::path dst = outDir / src.filename(); + fs::copy_file(src, dst, fs::copy_options::overwrite_existing, ec); + if (ec) { + error = "Failed to copy package binary: " + src.string(); + return false; + } + } + return true; +} + +bool copyPrecompiledEnginePackages(const fs::path& buildRoot, const fs::path& outDir, std::string& error) { + std::error_code ec; + if (!fs::exists(buildRoot)) return true; + + if (fs::exists(outDir)) { + fs::remove_all(outDir, ec); + if (ec) { + error = "Failed to clear engine package cache: " + outDir.string(); + return false; + } + } + fs::create_directories(outDir, ec); + if (ec) { + error = "Failed to create engine package folder: " + outDir.string(); + return false; + } + + auto isEngineLib = [](const std::string& filename) { + std::string name = filename; + std::transform(name.begin(), name.end(), name.begin(), ::tolower); + return name.rfind("libcore", 0) == 0 || + name.rfind("core_player", 0) == 0 || + name.rfind("core.", 0) == 0 || + name.rfind("core_player.", 0) == 0; + }; + + const std::vector exts = { ".a", ".so", ".dylib", ".lib", ".dll" }; + for (const auto& entry : fs::recursive_directory_iterator(buildRoot)) { + if (!entry.is_regular_file()) continue; + fs::path src = entry.path(); + std::string ext = src.extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); + if (std::find(exts.begin(), exts.end(), ext) == exts.end()) { + continue; + } + if (!isEngineLib(src.filename().string())) { + continue; + } + + fs::path dst = outDir / src.filename(); + fs::copy_file(src, dst, fs::copy_options::overwrite_existing, ec); + if (ec) { + error = "Failed to copy engine binary: " + src.string(); + return false; + } + } + return true; +} + +void cleanExportOutput(const fs::path& exportRoot, const char* exeBaseName, std::string& error) { + std::error_code ec; +#ifdef _WIN32 + fs::path exePath = exportRoot / (std::string(exeBaseName) + ".exe"); +#else + fs::path exePath = exportRoot / exeBaseName; +#endif + if (fs::exists(exePath)) { + fs::remove(exePath, ec); + if (ec) { + error = "Failed to remove existing executable."; + return; + } + } + + fs::path projectDir = exportRoot / "Project"; + if (fs::exists(projectDir)) { + fs::remove_all(projectDir, ec); + if (ec) { + error = "Failed to remove existing project files."; + return; + } + } + + fs::path resourcesDir = exportRoot / "Resources"; + if (fs::exists(resourcesDir)) { + fs::remove_all(resourcesDir, ec); + if (ec) { + error = "Failed to remove existing resources."; + return; + } + } + + fs::path packagesDir = exportRoot / "Packages"; + if (fs::exists(packagesDir)) { + fs::remove_all(packagesDir, ec); + if (ec) { + error = "Failed to remove existing packages."; + return; + } + } + + fs::path autostart = exportRoot / "autostart.modu"; + if (fs::exists(autostart)) { + fs::remove(autostart, ec); + if (ec) { + error = "Failed to remove existing autostart.modu."; + return; + } + } +} + +void cleanEditorExecutable(const fs::path& buildRoot) { + std::error_code ec; +#ifdef _WIN32 + fs::path editorExe = buildRoot / "Modularity.exe"; +#else + fs::path editorExe = buildRoot / "Modularity"; +#endif + if (fs::exists(editorExe)) { + fs::remove(editorExe, ec); + } +} +} // namespace +#pragma endregion + #pragma region Window + Selection Utilities void window_size_callback(GLFWwindow* window, int width, int height) { + (void)window; glViewport(0, 0, width, height); } @@ -286,6 +781,19 @@ glm::quat QuatFromEulerXYZ(const glm::vec3& deg) { m = glm::rotate(m, r.z, glm::vec3(0.0f, 0.0f, 1.0f)); return glm::quat_cast(glm::mat3(m)); } + +fs::path findManagedProjectRoot(const fs::path& start) { + std::error_code ec; + fs::path current = start; + for (int depth = 0; depth < 6 && !current.empty(); ++depth) { + fs::path candidate = current / "Scripts" / "Managed" / "ModuCPP.csproj"; + if (fs::exists(candidate, ec)) { + return current; + } + current = current.parent_path(); + } + return {}; +} } void Engine::DecomposeMatrix(const glm::mat4& matrix, glm::vec3& pos, glm::vec3& rot, glm::vec3& scale) { @@ -373,6 +881,18 @@ void Engine::redo() { } #pragma endregion +fs::path resolveScriptsConfigPath(const Project& project) { + std::error_code ec; + if (!project.scriptsConfigPath.empty() && fs::exists(project.scriptsConfigPath, ec)) { + return project.scriptsConfigPath; + } + fs::path lower = project.projectPath / "scripts.modu"; + if (fs::exists(lower, ec)) { + return lower; + } + return project.projectPath / "Scripts.modu"; +} + #pragma region Engine Lifecycle bool Engine::init() { std::cerr << "[DEBUG] Creating window..." << std::endl; @@ -409,6 +929,14 @@ bool Engine::init() { } logToConsole("Engine initialized - Waiting for project selection"); + loadAutoStartConfig(); +#ifdef MODULARITY_PLAYER + playerMode = true; + autoStartPlayerMode = true; +#endif + if (autoStartRequested && !autoStartProjectPath.empty()) { + startProjectLoad(autoStartProjectPath); + } return true; } @@ -441,6 +969,7 @@ void Engine::run() { glfwPollEvents(); pollProjectLoad(); + pollSceneLoad(); if (!showLauncher) { handleKeyboardShortcuts(); @@ -487,6 +1016,9 @@ void Engine::run() { updateRigidbody2D(deltaTime); } + updateCameraFollow2D(deltaTime); + + updateSkeletalAnimations(deltaTime); updateHierarchyWorldTransforms(); bool simulatePhysics = physics.isReady() && ((isPlaying && !isPaused) || (!isPlaying && specMode)); @@ -495,11 +1027,16 @@ void Engine::run() { } updateHierarchyWorldTransforms(); + updateSkinningMatrices(); + + if (playerMode) { + syncPlayerCamera(); + } bool audioShouldPlay = isPlaying || specMode || testMode; Camera listenerCamera = camera; for (const auto& obj : sceneObjects) { - if (obj.type == ObjectType::Camera && obj.camera.type == SceneCameraType::Player) { + if (obj.enabled && obj.hasCamera && obj.camera.type == SceneCameraType::Player) { listenerCamera = makeCameraFromObject(obj); listenerCamera.position = obj.position; break; @@ -510,6 +1047,20 @@ void Engine::run() { updateCompileJob(); updateAutoCompileScripts(); processAutoCompileQueue(); + pollExportBuild(); + + if (playerMode && !showLauncher) { + int displayW = 0; + int displayH = 0; + glfwGetFramebufferSize(editorWindow, &displayW, &displayH); + if (displayW > 0 && displayH > 0) { + viewportWidth = displayW; + viewportHeight = displayH; + if (rendererInitialized) { + renderer.resize(viewportWidth, viewportHeight); + } + } + } if (!showLauncher && projectManager.currentProject.isLoaded && rendererInitialized) { glm::mat4 view = camera.getViewMatrix(); @@ -530,6 +1081,15 @@ void Engine::run() { ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); + if (pendingWorkspaceReload) { + ImGuiID dockspaceId = ImGui::GetID("MainDockspace"); + ImGui::DockBuilderRemoveNode(dockspaceId); + if (!pendingWorkspaceIniPath.empty() && fs::exists(pendingWorkspaceIniPath)) { + ImGui::LoadIniSettingsFromDisk(pendingWorkspaceIniPath.string().c_str()); + } + pendingWorkspaceReload = false; + } + if (firstFrame) { std::cerr << "[DEBUG] First frame: ImGui NewFrame complete, rendering UI..." << std::endl; } @@ -538,8 +1098,12 @@ void Engine::run() { if (firstFrame) { std::cerr << "[DEBUG] First frame: calling renderLauncher()" << std::endl; } + #ifdef MODULARITY_PLAYER + renderPlayerViewport(); + #else renderLauncher(); - } else { + #endif + } else if (!playerMode) { setupDockspace([this]() { renderPlayControlsBar(); }); renderMainMenuBar(); @@ -549,15 +1113,20 @@ void Engine::run() { if (showFileBrowser) renderFileBrowserPanel(); if (showMeshBuilder) renderMeshBuilderPanel(); if (showConsole) renderConsolePanel(); + if (showScriptingWindow) renderScriptingWindow(); if (showEnvironmentWindow) renderEnvironmentWindow(); if (showCameraWindow) renderCameraWindow(); + if (showAnimationWindow) renderAnimationWindow(); if (showProjectBrowser) renderProjectBrowserPanel(); } + if (showBuildSettings) renderBuildSettingsWindow(); renderScriptEditorWindows(); renderViewport(); if (showGameViewport) renderGameViewportWindow(); renderDialogs(); + } else { + renderPlayerViewport(); } if (firstFrame) { @@ -649,7 +1218,10 @@ void Engine::importOBJToScene(const std::string& filepath, const std::string& ob int id = nextObjectId++; std::string name = objectName.empty() ? fs::path(filepath).stem().string() : objectName; - SceneObject obj(name, ObjectType::OBJMesh, id); + SceneObject obj(name, ObjectType::Empty, id); + obj.hasRenderer = true; + obj.renderType = RenderType::OBJMesh; + obj.type = ObjectType::OBJMesh; obj.meshPath = filepath; obj.meshId = meshId; @@ -674,32 +1246,270 @@ void Engine::importOBJToScene(const std::string& filepath, const std::string& ob void Engine::importModelToScene(const std::string& filepath, const std::string& objectName) { recordState("importModel"); auto& modelLoader = getModelLoader(); - ModelLoadResult result = modelLoader.loadModel(filepath); - - if (!result.success) { - addConsoleMessage("Failed to load model: " + result.errorMessage, ConsoleMessageType::Error); + ModelSceneData sceneData; + std::string error; + if (!modelLoader.loadModelScene(filepath, sceneData, error)) { + ModelLoadResult fallback = modelLoader.loadModel(filepath); + if (!fallback.success) { + addConsoleMessage("Failed to load model: " + error, ConsoleMessageType::Error); + return; + } + int id = nextObjectId++; + std::string name = objectName.empty() ? fs::path(filepath).stem().string() : objectName; + SceneObject obj(name, ObjectType::Empty, id); + obj.hasRenderer = true; + obj.renderType = RenderType::Model; + obj.type = ObjectType::Model; + obj.meshPath = filepath; + obj.meshId = fallback.meshIndex; + sceneObjects.push_back(obj); + setPrimarySelection(id); + if (projectManager.currentProject.isLoaded) { + projectManager.currentProject.hasUnsavedChanges = true; + } + addConsoleMessage( + "Imported model: " + name + " (" + + std::to_string(fallback.vertexCount) + " verts, " + + std::to_string(fallback.faceCount) + " faces, " + + std::to_string(fallback.meshCount) + " meshes)", + ConsoleMessageType::Success + ); return; } - int id = nextObjectId++; - std::string name = objectName.empty() ? fs::path(filepath).stem().string() : objectName; + std::string baseName = objectName.empty() ? fs::path(filepath).stem().string() : objectName; + std::vector materialPaths(sceneData.materials.size()); - SceneObject obj(name, ObjectType::Model, id); - obj.meshPath = filepath; - obj.meshId = result.meshIndex; + if (projectManager.currentProject.isLoaded && !sceneData.materials.empty()) { + fs::path materialsDir = projectManager.currentProject.assetsPath / "Materials" / "Imported" / baseName; + std::error_code ec; + fs::create_directories(materialsDir, ec); + if (ec) { + addConsoleMessage("Failed to create materials folder: " + materialsDir.string(), ConsoleMessageType::Warning); + } else { + for (size_t i = 0; i < sceneData.materials.size(); ++i) { + const auto& mat = sceneData.materials[i]; + std::string matName = sanitizeMaterialName(mat.name); + fs::path matPath = materialsDir / (matName + ".mat"); + MaterialFileData data; + data.props = mat.props; + data.albedo = resolveTexturePath(mat.albedoPath, filepath); + data.overlay.clear(); + data.normal = resolveTexturePath(mat.normalPath, filepath); + data.useOverlay = false; + data.vertexShader.clear(); + data.fragmentShader.clear(); + if (writeMaterialFile(data, matPath.string())) { + materialPaths[i] = matPath.string(); + } + } + } + } - sceneObjects.push_back(obj); - setPrimarySelection(id); + constexpr size_t kStaticBatchMeshThreshold = 16; + size_t validMeshCount = 0; + for (int meshId : sceneData.meshIndices) { + if (meshId >= 0) { + ++validMeshCount; + } + } + + bool hasSkinnedMeshes = false; + for (int meshId : sceneData.meshIndices) { + if (meshId < 0) continue; + const auto* info = modelLoader.getMeshInfo(meshId); + if (info && info->isSkinned) { + hasSkinnedMeshes = true; + break; + } + } + + bool hasAnimations = !sceneData.animations.empty(); + bool singleMaterial = sceneData.materials.size() <= 1; + if (!singleMaterial && !sceneData.meshMaterialIndices.empty()) { + int firstMat = -2; + bool mixed = false; + for (int matIndex : sceneData.meshMaterialIndices) { + if (matIndex < 0) continue; + if (firstMat == -2) { + firstMat = matIndex; + } else if (matIndex != firstMat) { + mixed = true; + break; + } + } + singleMaterial = !mixed; + } + + if (validMeshCount >= kStaticBatchMeshThreshold && + !hasSkinnedMeshes && !hasAnimations && singleMaterial) { + RawMeshAsset raw; + glm::vec3 rootPos(0.0f); + glm::vec3 rootRot(0.0f); + glm::vec3 rootScale(1.0f); + if (modelLoader.buildRawMeshFromScene(filepath, raw, error, &rootPos, &rootRot, &rootScale)) { + std::string batchName = baseName + "_StaticBatch"; + int batchMeshId = modelLoader.addRawMesh(raw, filepath, batchName, error); + if (batchMeshId >= 0) { + int id = nextObjectId++; + SceneObject obj(baseName, ObjectType::Empty, id); + obj.hasRenderer = true; + obj.renderType = RenderType::Model; + obj.type = ObjectType::Model; + obj.meshPath = filepath; + obj.meshId = batchMeshId; + obj.meshSourceIndex = -1; + obj.localPosition = rootPos; + obj.localRotation = rootRot; + obj.localScale = rootScale; + obj.localInitialized = true; + obj.position = obj.localPosition; + obj.rotation = obj.localRotation; + obj.scale = obj.localScale; + + if (!materialPaths.empty() && !materialPaths[0].empty()) { + obj.materialPath = materialPaths[0]; + loadMaterialFromFile(obj); + } else if (!sceneData.materials.empty()) { + const auto& mat = sceneData.materials.front(); + obj.material = mat.props; + obj.albedoTexturePath = resolveTexturePath(mat.albedoPath, filepath); + obj.normalMapPath = resolveTexturePath(mat.normalPath, filepath); + } + + sceneObjects.push_back(obj); + setPrimarySelection(id); + if (projectManager.currentProject.isLoaded) { + projectManager.currentProject.hasUnsavedChanges = true; + } + addConsoleMessage( + "Imported model (static batch): " + baseName + " (" + + std::to_string(validMeshCount) + " meshes)", + ConsoleMessageType::Success + ); + return; + } + } + } + + std::vector nodeObjectIds(sceneData.nodes.size(), -1); + int rootSelectionId = -1; + + for (size_t i = 0; i < sceneData.nodes.size(); ++i) { + const auto& node = sceneData.nodes[i]; + std::string nodeName = node.name.empty() + ? (baseName + "_Node" + std::to_string(i)) + : node.name; + SceneObject obj(nodeName, ObjectType::Empty, nextObjectId++); + obj.localPosition = node.localPosition; + obj.localRotation = node.localRotation; + obj.localScale = node.localScale; + obj.localInitialized = true; + obj.position = obj.localPosition; + obj.rotation = obj.localRotation; + obj.scale = obj.localScale; + sceneObjects.push_back(obj); + nodeObjectIds[i] = obj.id; + if (rootSelectionId == -1) rootSelectionId = obj.id; + } + + for (size_t i = 0; i < sceneData.nodes.size(); ++i) { + int parentIndex = sceneData.nodes[i].parentIndex; + if (parentIndex < 0 || parentIndex >= (int)nodeObjectIds.size()) continue; + int parentId = nodeObjectIds[parentIndex]; + int childId = nodeObjectIds[i]; + if (parentId < 0 || childId < 0) continue; + SceneObject* parentObj = findObjectById(parentId); + SceneObject* childObj = findObjectById(childId); + if (!parentObj || !childObj) continue; + childObj->parentId = parentId; + parentObj->childIds.push_back(childId); + } + + for (size_t nodeIndex = 0; nodeIndex < sceneData.nodes.size(); ++nodeIndex) { + const auto& node = sceneData.nodes[nodeIndex]; + int parentId = nodeObjectIds[nodeIndex]; + if (parentId < 0) continue; + for (int meshSourceIndex : node.meshIndices) { + if (meshSourceIndex < 0 || meshSourceIndex >= (int)sceneData.meshIndices.size()) continue; + int meshId = sceneData.meshIndices[meshSourceIndex]; + if (meshId < 0) continue; + + const auto* meshInfo = modelLoader.getMeshInfo(meshId); + std::string meshName = meshInfo && !meshInfo->name.empty() + ? meshInfo->name + : (baseName + "_Mesh"); + + SceneObject meshObj(meshName, ObjectType::Empty, nextObjectId++); + meshObj.hasRenderer = true; + meshObj.renderType = RenderType::Model; + meshObj.type = ObjectType::Model; + meshObj.meshPath = filepath; + meshObj.meshId = meshId; + meshObj.meshSourceIndex = meshSourceIndex; + meshObj.localPosition = glm::vec3(0.0f); + meshObj.localRotation = glm::vec3(0.0f); + meshObj.localScale = glm::vec3(1.0f); + meshObj.localInitialized = true; + meshObj.position = meshObj.localPosition; + meshObj.rotation = meshObj.localRotation; + meshObj.scale = meshObj.localScale; + meshObj.parentId = parentId; + + if (meshInfo && meshInfo->isSkinned) { + meshObj.hasSkeletalAnimation = true; + meshObj.skeletal = SkeletalAnimationComponent{}; + meshObj.skeletal.skeletonRootId = rootSelectionId; + meshObj.skeletal.boneNames = meshInfo->boneNames; + meshObj.skeletal.inverseBindMatrices = meshInfo->inverseBindMatrices; + meshObj.skeletal.finalMatrices.assign(meshInfo->boneNames.size(), glm::mat4(1.0f)); + meshObj.skeletal.boneNodeIds.assign(meshInfo->boneNames.size(), -1); + for (size_t b = 0; b < meshInfo->boneNames.size(); ++b) { + for (size_t n = 0; n < sceneData.nodes.size(); ++n) { + if (sceneData.nodes[n].name == meshInfo->boneNames[b]) { + int nodeId = nodeObjectIds[n]; + if (nodeId >= 0) { + meshObj.skeletal.boneNodeIds[b] = nodeId; + } + break; + } + } + } + } + + int matIndex = (meshSourceIndex >= 0 && meshSourceIndex < (int)sceneData.meshMaterialIndices.size()) + ? sceneData.meshMaterialIndices[meshSourceIndex] + : -1; + if (matIndex >= 0 && matIndex < (int)materialPaths.size() && !materialPaths[matIndex].empty()) { + meshObj.materialPath = materialPaths[matIndex]; + loadMaterialFromFile(meshObj); + } else if (matIndex >= 0 && matIndex < (int)sceneData.materials.size()) { + const auto& mat = sceneData.materials[matIndex]; + meshObj.material = mat.props; + meshObj.albedoTexturePath = resolveTexturePath(mat.albedoPath, filepath); + meshObj.normalMapPath = resolveTexturePath(mat.normalPath, filepath); + } + + sceneObjects.push_back(meshObj); + if (SceneObject* parentObj = findObjectById(parentId)) { + parentObj->childIds.push_back(meshObj.id); + } + } + } + + updateHierarchyWorldTransforms(); + if (rootSelectionId != -1) { + setPrimarySelection(rootSelectionId); + } if (projectManager.currentProject.isLoaded) { projectManager.currentProject.hasUnsavedChanges = true; } addConsoleMessage( - "Imported model: " + name + " (" + - std::to_string(result.vertexCount) + " verts, " + - std::to_string(result.faceCount) + " faces, " + - std::to_string(result.meshCount) + " meshes)", + "Imported model: " + baseName + " (" + + std::to_string(sceneData.meshIndices.size()) + " meshes, " + + std::to_string(sceneData.nodes.size()) + " nodes)", ConsoleMessageType::Success ); } @@ -746,15 +1556,23 @@ void Engine::createRMeshPrimitive(const std::string& primitiveName) { } fs::path filePath = root / (primitiveName + ".rmesh"); - if (!fs::exists(filePath)) { - std::string error; - if (!getModelLoader().saveRawMesh(asset, filePath.string(), error)) { - addConsoleMessage("Failed to save RMesh primitive: " + error, ConsoleMessageType::Error); - return; - } - fileBrowser.needsRefresh = true; + if (fs::exists(filePath)) { + int suffix = 1; + fs::path candidate; + do { + candidate = root / (primitiveName + "_" + std::to_string(suffix) + ".rmesh"); + ++suffix; + } while (fs::exists(candidate)); + filePath = candidate; } + std::string error; + if (!getModelLoader().saveRawMesh(asset, filePath.string(), error)) { + addConsoleMessage("Failed to save RMesh primitive: " + error, ConsoleMessageType::Error); + return; + } + fileBrowser.needsRefresh = true; + importModelToScene(filePath.string(), primitiveName); } #pragma endregion @@ -919,6 +1737,8 @@ void Engine::handleKeyboardShortcuts() { static bool ctrlSPressed = false; bool ctrlDown = glfwGetKey(editorWindow, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS || glfwGetKey(editorWindow, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS; + bool shiftDown = glfwGetKey(editorWindow, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS || + glfwGetKey(editorWindow, GLFW_KEY_RIGHT_SHIFT) == GLFW_PRESS; if (ctrlDown && glfwGetKey(editorWindow, GLFW_KEY_S) == GLFW_PRESS && !ctrlSPressed) { if (projectManager.currentProject.isLoaded) { @@ -988,7 +1808,7 @@ void Engine::handleKeyboardShortcuts() { } static bool undoPressed = false; - if (ctrlDown && glfwGetKey(editorWindow, GLFW_KEY_Z) == GLFW_PRESS && !undoPressed) { + if (ctrlDown && !shiftDown && glfwGetKey(editorWindow, GLFW_KEY_Z) == GLFW_PRESS && !undoPressed) { undo(); undoPressed = true; } @@ -997,11 +1817,17 @@ void Engine::handleKeyboardShortcuts() { } static bool redoPressed = false; - if (ctrlDown && glfwGetKey(editorWindow, GLFW_KEY_Y) == GLFW_PRESS && !redoPressed) { + if (ctrlDown && + ((glfwGetKey(editorWindow, GLFW_KEY_Y) == GLFW_PRESS) || + (shiftDown && glfwGetKey(editorWindow, GLFW_KEY_Z) == GLFW_PRESS)) && + !redoPressed) + { redo(); redoPressed = true; } - if (glfwGetKey(editorWindow, GLFW_KEY_Y) == GLFW_RELEASE) { + if (glfwGetKey(editorWindow, GLFW_KEY_Y) == GLFW_RELEASE && + glfwGetKey(editorWindow, GLFW_KEY_Z) == GLFW_RELEASE) + { redoPressed = false; } @@ -1020,14 +1846,19 @@ void Engine::updateScripts(float delta) { for (auto& sc : obj.scripts) { if (!sc.enabled) continue; if (sc.path.empty()) continue; - fs::path binary = resolveScriptBinary(sc.path); - if (binary.empty() || !fs::exists(binary)) continue; ScriptContext ctx; ctx.engine = this; ctx.object = &obj; ctx.script = ≻ - - scriptRuntime.tickModule(binary, ctx, delta, specMode, testMode); + if (sc.language == ScriptLanguage::CSharp) { + fs::path assembly = resolveManagedAssembly(sc.path); + if (assembly.empty() || !fs::exists(assembly)) continue; + managedRuntime.tickModule(assembly, sc.managedType, ctx, delta, specMode, testMode); + } else { + fs::path binary = resolveScriptBinary(sc.path); + if (binary.empty() || !fs::exists(binary)) continue; + scriptRuntime.tickModule(binary, ctx, delta, specMode, testMode); + } } } } @@ -1051,10 +1882,7 @@ void Engine::updateAutoCompileScripts() { if (now - scriptAutoCompileLastCheck < scriptAutoCompileInterval) return; scriptAutoCompileLastCheck = now; - fs::path configPath = projectManager.currentProject.scriptsConfigPath; - if (configPath.empty()) { - configPath = projectManager.currentProject.projectPath / "Scripts.modu"; - } + fs::path configPath = resolveScriptsConfigPath(projectManager.currentProject); ScriptBuildConfig config; std::string error; @@ -1072,8 +1900,13 @@ void Engine::updateAutoCompileScripts() { sources.insert(absPath.lexically_normal().string()); }; + bool hasManagedScripts = false; for (const auto& obj : sceneObjects) { for (const auto& sc : obj.scripts) { + if (sc.language == ScriptLanguage::CSharp) { + hasManagedScripts = true; + continue; + } if (sc.path.empty()) continue; addSource(sc.path); } @@ -1122,6 +1955,48 @@ void Engine::updateAutoCompileScripts() { queueAutoCompile(sourcePath, sourceTime); } + + if (hasManagedScripts) { + fs::path managedProject = getManagedProjectPath(); + fs::path managedOutput = getManagedOutputDll(); + std::error_code managedEc; + if (fs::exists(managedProject, managedEc)) { + fs::file_time_type newestSource{}; + bool hasSource = false; + fs::path managedDir = managedProject.parent_path(); + if (fs::exists(managedDir, managedEc)) { + for (auto it = fs::recursive_directory_iterator(managedDir, managedEc); + it != fs::recursive_directory_iterator(); ++it) { + if (it->is_directory()) continue; + if (it->path().extension() != ".cs") continue; + auto sourceTime = fs::last_write_time(it->path(), managedEc); + if (managedEc) continue; + if (!hasSource || sourceTime > newestSource) { + newestSource = sourceTime; + hasSource = true; + } + } + } + + bool needsManaged = false; + if (!fs::exists(managedOutput, managedEc)) { + needsManaged = true; + } else if (hasSource && !managedEc) { + auto binaryTime = fs::last_write_time(managedOutput, managedEc); + if (!managedEc && newestSource > binaryTime) { + needsManaged = true; + } + } + + if (needsManaged) { + if (!compileInProgress) { + compileManagedScripts(); + } else { + managedAutoCompileQueued = true; + } + } + } + } } void Engine::processAutoCompileQueue() { @@ -1160,10 +2035,12 @@ void Engine::updatePlayerController(float delta) { pc.pitch = player->rotation.x; pc.yaw = player->rotation.y; } + glm::vec3 capsuleSize(pc.radius * 2.0f, pc.height, pc.radius * 2.0f); player->hasCollider = true; player->collider.type = ColliderType::Capsule; player->collider.convex = true; - player->collider.boxSize = glm::vec3(pc.radius * 2.0f, pc.height, pc.radius * 2.0f); + player->collider.boxSize = capsuleSize; + player->scale = capsuleSize; player->hasRigidbody = true; player->rigidbody.enabled = true; player->rigidbody.useGravity = true; @@ -1251,32 +2128,493 @@ void Engine::updatePlayerController(float delta) { void Engine::updateRigidbody2D(float delta) { if (delta <= 0.0f) return; - const float gravityPx = -980.0f; - auto isUIType = [](ObjectType type) { - return type == ObjectType::Canvas || - type == ObjectType::UIImage || - type == ObjectType::UISlider || - type == ObjectType::UIButton || - type == ObjectType::UIText || - type == ObjectType::Sprite2D; + const float gravity = -9.81f; + const float minEdgeThickness = 0.01f; + auto getParentOffset = [&](const SceneObject& obj) { + glm::vec2 offset(0.0f); + const SceneObject* current = &obj; + while (current && current->parentId >= 0) { + auto pit = std::find_if(sceneObjects.begin(), sceneObjects.end(), + [&](const SceneObject& o) { return o.id == current->parentId; }); + if (pit == sceneObjects.end()) break; + current = &(*pit); + if (current->hasUI && current->ui.type != UIElementType::None) { + offset += glm::vec2(current->ui.position.x, current->ui.position.y); + } + } + return offset; }; + auto rotatePoint = [](const glm::vec2& p, float c, float s) { + return glm::vec2(p.x * c - p.y * s, p.x * s + p.y * c); + }; + auto buildHexagon = [](float radius, std::vector& out) { + out.clear(); + for (int i = 0; i < 6; ++i) { + float ang = static_cast(i) * (2.0f * PI / 6.0f); + out.emplace_back(std::cos(ang) * radius, std::sin(ang) * radius); + } + }; + auto computeAabb = [](const std::vector& pts, glm::vec2& outMin, glm::vec2& outMax) { + if (pts.empty()) { + outMin = glm::vec2(0.0f); + outMax = glm::vec2(0.0f); + return; + } + outMin = pts[0]; + outMax = pts[0]; + for (const auto& p : pts) { + outMin.x = std::min(outMin.x, p.x); + outMin.y = std::min(outMin.y, p.y); + outMax.x = std::max(outMax.x, p.x); + outMax.y = std::max(outMax.y, p.y); + } + }; + auto polyCenter = [](const std::vector& pts) { + glm::vec2 c(0.0f); + if (pts.empty()) return c; + for (const auto& p : pts) c += p; + return c / static_cast(pts.size()); + }; + auto satOverlap = [&](const std::vector& a, const std::vector& b, glm::vec2& outAxis, float& outDepth) { + if (a.size() < 3 || b.size() < 3) return false; + auto testAxes = [&](const std::vector& poly, glm::vec2& axis, float& depth) { + for (size_t i = 0; i < poly.size(); ++i) { + glm::vec2 p0 = poly[i]; + glm::vec2 p1 = poly[(i + 1) % poly.size()]; + glm::vec2 edge = p1 - p0; + glm::vec2 n = glm::normalize(glm::vec2(-edge.y, edge.x)); + float minA = FLT_MAX, maxA = -FLT_MAX; + float minB = FLT_MAX, maxB = -FLT_MAX; + for (const auto& p : a) { + float d = glm::dot(p, n); + minA = std::min(minA, d); + maxA = std::max(maxA, d); + } + for (const auto& p : b) { + float d = glm::dot(p, n); + minB = std::min(minB, d); + maxB = std::max(maxB, d); + } + float overlap = std::min(maxA, maxB) - std::max(minA, minB); + if (overlap <= 0.0f) return false; + if (overlap < depth) { + depth = overlap; + axis = n; + } + } + return true; + }; + glm::vec2 axis(0.0f); + float depth = FLT_MAX; + if (!testAxes(a, axis, depth)) return false; + if (!testAxes(b, axis, depth)) return false; + glm::vec2 dir = polyCenter(b) - polyCenter(a); + if (glm::dot(axis, dir) < 0.0f) axis = -axis; + outAxis = axis; + outDepth = depth; + return true; + }; + auto segmentRect = [minEdgeThickness](const glm::vec2& a, const glm::vec2& b, float thickness, std::vector& out) { + glm::vec2 dir = b - a; + float len = glm::length(dir); + if (len < 1e-4f) { + out.clear(); + return; + } + glm::vec2 n = glm::vec2(-dir.y, dir.x) / len; + float half = std::max(minEdgeThickness, thickness) * 0.5f; + out.clear(); + out.push_back(a + n * half); + out.push_back(b + n * half); + out.push_back(b - n * half); + out.push_back(a - n * half); + }; + struct Body2DRef { + int index = -1; + bool dynamic = false; + glm::vec2 parentOffset = glm::vec2(0.0f); + glm::vec2 pivotWorld = glm::vec2(0.0f); + float rotationRad = 0.0f; + std::vector poly; + std::vector> segments; + float edgeThickness = 0.01f; + glm::vec2 aabbMin = glm::vec2(0.0f); + glm::vec2 aabbMax = glm::vec2(0.0f); + bool isEdge = false; + }; + std::vector bodies; + bodies.reserve(sceneObjects.size()); for (auto& obj : sceneObjects) { - if (!obj.enabled || !obj.hasRigidbody2D || !obj.rigidbody2D.enabled) continue; - if (!isUIType(obj.type)) continue; - glm::vec2 vel = obj.rigidbody2D.velocity; - if (obj.rigidbody2D.useGravity) { - vel.y += gravityPx * obj.rigidbody2D.gravityScale * delta; + if (!obj.enabled || !HasUIComponent(obj)) continue; + bool hasDynamic = obj.hasRigidbody2D && obj.rigidbody2D.enabled; + bool hasCollider2D = obj.hasCollider2D && obj.collider2D.enabled; + if (!hasDynamic && !hasCollider2D) continue; + + if (hasDynamic) { + glm::vec2 vel = obj.rigidbody2D.velocity; + if (obj.rigidbody2D.useGravity) { + vel.y += gravity * obj.rigidbody2D.gravityScale * delta; + } + float damping = std::max(0.0f, obj.rigidbody2D.linearDamping); + if (damping > 0.0f) { + vel -= vel * std::min(1.0f, damping * delta); + } + obj.ui.position += vel * delta; + obj.rigidbody2D.velocity = vel; } - float damping = std::max(0.0f, obj.rigidbody2D.linearDamping); - if (damping > 0.0f) { - vel -= vel * std::min(1.0f, damping * delta); + + Body2DRef body; + body.index = static_cast(&obj - &sceneObjects[0]); + body.dynamic = hasDynamic; + body.parentOffset = getParentOffset(obj); + body.pivotWorld = body.parentOffset + obj.ui.position; + body.rotationRad = glm::radians(obj.ui.rotation); + float c = std::cos(body.rotationRad); + float s = std::sin(body.rotationRad); + glm::vec2 size = glm::vec2(std::max(1.0f, obj.ui.size.x), std::max(1.0f, obj.ui.size.y)); + Collider2DType type = Collider2DType::Box; + glm::vec2 boxSize = size; + std::vector localPoints; + bool closed = false; + float edgeThickness = minEdgeThickness; + if (hasCollider2D) { + type = obj.collider2D.type; + boxSize = obj.collider2D.boxSize; + if (boxSize.x <= 0.0f || boxSize.y <= 0.0f) { + boxSize = size; + } + localPoints = obj.collider2D.points; + closed = obj.collider2D.closed; + edgeThickness = obj.collider2D.edgeThickness; + } + if (type == Collider2DType::Box) { + glm::vec2 half = boxSize * 0.5f; + localPoints = { + glm::vec2(-half.x, -half.y), + glm::vec2( half.x, -half.y), + glm::vec2( half.x, half.y), + glm::vec2(-half.x, half.y) + }; + } else if (type == Collider2DType::Polygon) { + if (localPoints.empty()) { + float radius = 0.5f * std::min(boxSize.x, boxSize.y); + buildHexagon(radius, localPoints); + } + } else if (type == Collider2DType::Edge) { + if (localPoints.size() < 2) { + float half = boxSize.x * 0.5f; + localPoints = { glm::vec2(-half, 0.0f), glm::vec2(half, 0.0f) }; + } + } + + if (type == Collider2DType::Edge) { + body.isEdge = true; + body.edgeThickness = edgeThickness; + for (size_t i = 0; i + 1 < localPoints.size(); ++i) { + glm::vec2 a = rotatePoint(localPoints[i], c, s) + body.pivotWorld; + glm::vec2 b = rotatePoint(localPoints[i + 1], c, s) + body.pivotWorld; + body.segments.emplace_back(a, b); + } + if (closed && localPoints.size() > 2) { + glm::vec2 a = rotatePoint(localPoints.back(), c, s) + body.pivotWorld; + glm::vec2 b = rotatePoint(localPoints.front(), c, s) + body.pivotWorld; + body.segments.emplace_back(a, b); + } + } else { + body.poly.reserve(localPoints.size()); + for (const auto& p : localPoints) { + body.poly.push_back(rotatePoint(p, c, s) + body.pivotWorld); + } + computeAabb(body.poly, body.aabbMin, body.aabbMax); + } + bodies.push_back(body); + } + + auto applySeparation = [&](Body2DRef& body, const glm::vec2& sep, const glm::vec2& normal) { + SceneObject& obj = sceneObjects[body.index]; + if (body.dynamic) { + obj.ui.position += sep; + body.pivotWorld += sep; + if (!body.poly.empty()) { + for (auto& p : body.poly) p += sep; + body.aabbMin += sep; + body.aabbMax += sep; + } + if (!body.segments.empty()) { + for (auto& seg : body.segments) { + seg.first += sep; + seg.second += sep; + } + } + float vn = glm::dot(obj.rigidbody2D.velocity, normal); + if (vn < 0.0f) { + obj.rigidbody2D.velocity -= normal * vn; + } + } + }; + + for (size_t i = 0; i < bodies.size(); ++i) { + for (size_t j = i + 1; j < bodies.size(); ++j) { + Body2DRef& a = bodies[i]; + Body2DRef& b = bodies[j]; + if (!a.dynamic && !b.dynamic) continue; + + auto polyVsPoly = [&](Body2DRef& pA, Body2DRef& pB) { + if (pA.poly.empty() || pB.poly.empty()) return; + if (pA.aabbMax.x <= pB.aabbMin.x || pA.aabbMin.x >= pB.aabbMax.x || + pA.aabbMax.y <= pB.aabbMin.y || pA.aabbMin.y >= pB.aabbMax.y) { + return; + } + glm::vec2 axis(0.0f); + float depth = 0.0f; + if (!satOverlap(pA.poly, pB.poly, axis, depth)) return; + glm::vec2 sep = axis * depth; + if (pA.dynamic && pB.dynamic) { + applySeparation(pA, -sep * 0.5f, -axis); + applySeparation(pB, sep * 0.5f, axis); + } else if (pA.dynamic) { + applySeparation(pA, -sep, -axis); + } else if (pB.dynamic) { + applySeparation(pB, sep, axis); + } + }; + + auto polyVsEdge = [&](Body2DRef& polyBody, Body2DRef& edgeBody) { + if (polyBody.poly.empty() || edgeBody.segments.empty()) return; + std::vector rect; + for (const auto& seg : edgeBody.segments) { + segmentRect(seg.first, seg.second, edgeBody.edgeThickness, rect); + if (rect.size() < 3) continue; + glm::vec2 axis(0.0f); + float depth = 0.0f; + if (!satOverlap(polyBody.poly, rect, axis, depth)) continue; + glm::vec2 sep = axis * depth; + if (polyBody.dynamic && edgeBody.dynamic) { + applySeparation(polyBody, -sep * 0.5f, -axis); + applySeparation(edgeBody, sep * 0.5f, axis); + } else if (polyBody.dynamic) { + applySeparation(polyBody, -sep, -axis); + } else if (edgeBody.dynamic) { + applySeparation(edgeBody, sep, axis); + } + } + }; + + if (!a.isEdge && !b.isEdge) { + polyVsPoly(a, b); + } else if (!a.isEdge && b.isEdge) { + polyVsEdge(a, b); + } else if (a.isEdge && !b.isEdge) { + polyVsEdge(b, a); + } + } + } +} + +void Engine::updateCameraFollow2D(float delta) { + if (sceneObjects.empty()) return; + + std::unordered_map indexById; + indexById.reserve(sceneObjects.size()); + for (size_t i = 0; i < sceneObjects.size(); ++i) { + indexById[sceneObjects[i].id] = i; + } + + auto getUiWorldPosition = [&](const SceneObject& target) { + glm::vec2 pos(target.ui.position.x, target.ui.position.y); + int parentId = target.parentId; + while (parentId >= 0) { + auto it = indexById.find(parentId); + if (it == indexById.end()) break; + const SceneObject& parent = sceneObjects[it->second]; + if (parent.hasUI && parent.ui.type != UIElementType::None) { + pos += glm::vec2(parent.ui.position.x, parent.ui.position.y); + } + parentId = parent.parentId; + } + return pos; + }; + + for (auto& obj : sceneObjects) { + if (!obj.enabled || !obj.hasCamera || !obj.hasCameraFollow2D || !obj.cameraFollow2D.enabled) continue; + if (obj.cameraFollow2D.targetId < 0) continue; + auto targetIt = indexById.find(obj.cameraFollow2D.targetId); + if (targetIt == indexById.end()) continue; + + const SceneObject& target = sceneObjects[targetIt->second]; + glm::vec2 desired2D = (target.hasUI && target.ui.type != UIElementType::None) + ? getUiWorldPosition(target) + : glm::vec2(target.position.x, target.position.y); + desired2D += obj.cameraFollow2D.offset; + glm::vec3 desired(desired2D.x, desired2D.y, obj.position.z); + + if (obj.cameraFollow2D.smoothTime > 0.0001f) { + float alpha = 1.0f - std::exp(-delta / obj.cameraFollow2D.smoothTime); + obj.position = glm::mix(obj.position, desired, alpha); + } else { + obj.position = desired; + } + + if (obj.parentId == -1) { + obj.localPosition = obj.position; + obj.localInitialized = true; + } else { + auto parentIt = indexById.find(obj.parentId); + if (parentIt != indexById.end()) { + const SceneObject& parent = sceneObjects[parentIt->second]; + updateLocalFromWorld(obj, + parent.position, + QuatFromEulerXYZ(parent.rotation), + parent.scale); + } } - obj.ui.position += vel * delta; - obj.rigidbody2D.velocity = vel; } } #pragma endregion +#pragma region Skeletal Animation +namespace { +glm::vec3 sampleVecKeys(const std::vector& keys, float time, const glm::vec3& fallback) { + if (keys.empty()) return fallback; + if (time <= keys.front().time) return keys.front().value; + if (time >= keys.back().time) return keys.back().value; + for (size_t i = 0; i + 1 < keys.size(); ++i) { + if (time >= keys[i].time && time <= keys[i + 1].time) { + float span = keys[i + 1].time - keys[i].time; + float t = span > 0.0f ? (time - keys[i].time) / span : 0.0f; + return glm::mix(keys[i].value, keys[i + 1].value, t); + } + } + return keys.back().value; +} + +glm::quat sampleQuatKeys(const std::vector& keys, float time, const glm::quat& fallback) { + if (keys.empty()) return fallback; + if (time <= keys.front().time) return keys.front().value; + if (time >= keys.back().time) return keys.back().value; + for (size_t i = 0; i + 1 < keys.size(); ++i) { + if (time >= keys[i].time && time <= keys[i + 1].time) { + float span = keys[i + 1].time - keys[i].time; + float t = span > 0.0f ? (time - keys[i].time) / span : 0.0f; + return glm::slerp(keys[i].value, keys[i + 1].value, t); + } + } + return keys.back().value; +} +} + +void Engine::updateSkeletalAnimations(float delta) { + for (auto& obj : sceneObjects) { + if (!obj.enabled || !obj.hasSkeletalAnimation || !obj.skeletal.enabled) continue; + if (!obj.skeletal.useAnimation) continue; + if (obj.meshPath.empty()) continue; + + ModelSceneData sceneData; + std::string err; + if (!getModelLoader().loadModelScene(obj.meshPath, sceneData, err)) continue; + if (obj.skeletal.clipIndex < 0 || obj.skeletal.clipIndex >= (int)sceneData.animations.size()) continue; + + const auto& clip = sceneData.animations[obj.skeletal.clipIndex]; + double tps = clip.ticksPerSecond != 0.0 ? clip.ticksPerSecond : 25.0; + obj.skeletal.time += delta * obj.skeletal.playSpeed; + double timeTicks = obj.skeletal.time * tps; + if (clip.duration > 0.0) { + if (obj.skeletal.loop) { + timeTicks = std::fmod(timeTicks, clip.duration); + if (timeTicks < 0.0) timeTicks += clip.duration; + } else { + timeTicks = std::clamp(timeTicks, 0.0, clip.duration); + } + } + float time = static_cast(timeTicks); + + for (size_t b = 0; b < obj.skeletal.boneNames.size(); ++b) { + int boneId = obj.skeletal.boneNodeIds.size() > b ? obj.skeletal.boneNodeIds[b] : -1; + if (boneId < 0) continue; + SceneObject* boneObj = findObjectById(boneId); + if (!boneObj) continue; + + const ModelSceneData::AnimChannel* channel = nullptr; + for (const auto& ch : clip.channels) { + if (ch.nodeName == obj.skeletal.boneNames[b]) { + channel = &ch; + break; + } + } + if (!channel) continue; + + glm::vec3 pos = sampleVecKeys(channel->positions, time, boneObj->localPosition); + glm::quat rot = sampleQuatKeys(channel->rotations, time, QuatFromEulerXYZ(boneObj->localRotation)); + glm::vec3 scale = sampleVecKeys(channel->scales, time, boneObj->localScale); + + boneObj->localPosition = pos; + boneObj->localRotation = NormalizeEulerDegrees(glm::degrees(glm::eulerAngles(rot))); + boneObj->localScale = scale; + boneObj->localInitialized = true; + } + } +} + +void Engine::updateSkinningMatrices() { + for (auto& obj : sceneObjects) { + if (!obj.enabled || !obj.hasSkeletalAnimation || !obj.skeletal.enabled) continue; + if (obj.skeletal.inverseBindMatrices.empty()) continue; + + glm::mat4 meshWorld = ComposeTransform(obj.position, obj.rotation, obj.scale); + glm::mat4 invMesh = glm::inverse(meshWorld); + + size_t boneCount = obj.skeletal.inverseBindMatrices.size(); + if (obj.skeletal.finalMatrices.size() != boneCount) { + obj.skeletal.finalMatrices.assign(boneCount, glm::mat4(1.0f)); + } + + for (size_t b = 0; b < boneCount; ++b) { + int boneId = obj.skeletal.boneNodeIds.size() > b ? obj.skeletal.boneNodeIds[b] : -1; + if (boneId < 0) { + obj.skeletal.finalMatrices[b] = glm::mat4(1.0f); + continue; + } + SceneObject* boneObj = findObjectById(boneId); + if (!boneObj) continue; + glm::mat4 boneWorld = ComposeTransform(boneObj->position, boneObj->rotation, boneObj->scale); + obj.skeletal.finalMatrices[b] = invMesh * boneWorld * obj.skeletal.inverseBindMatrices[b]; + } + } +} +#pragma endregion + +void Engine::rebuildSkeletalBindings() { + std::unordered_map nameToId; + nameToId.reserve(sceneObjects.size()); + for (const auto& obj : sceneObjects) { + if (!obj.name.empty()) { + nameToId[obj.name] = obj.id; + } + } + + for (auto& obj : sceneObjects) { + if (!obj.hasRenderer || obj.renderType != RenderType::Model || obj.meshId < 0) continue; + const auto* meshInfo = getModelLoader().getMeshInfo(obj.meshId); + if (!meshInfo || !meshInfo->isSkinned) continue; + + if (!obj.hasSkeletalAnimation) { + obj.skeletal = SkeletalAnimationComponent{}; + obj.hasSkeletalAnimation = true; + } + obj.skeletal.skeletonRootId = obj.parentId; + obj.skeletal.boneNames = meshInfo->boneNames; + obj.skeletal.inverseBindMatrices = meshInfo->inverseBindMatrices; + obj.skeletal.finalMatrices.assign(meshInfo->boneNames.size(), glm::mat4(1.0f)); + obj.skeletal.boneNodeIds.assign(meshInfo->boneNames.size(), -1); + for (size_t b = 0; b < meshInfo->boneNames.size(); ++b) { + auto it = nameToId.find(meshInfo->boneNames[b]); + if (it != nameToId.end()) { + obj.skeletal.boneNodeIds[b] = it->second; + } + } + } +} + #pragma region Transform Hierarchy void Engine::updateLocalFromWorld(SceneObject& obj, const glm::vec3& parentPos, const glm::quat& parentRot, const glm::vec3& parentScale) { auto safeDiv = [](float v, float d) { return (std::abs(d) > 1e-6f) ? (v / d) : 0.0f; }; @@ -1489,6 +2827,153 @@ void Engine::pollProjectLoad() { } } +void Engine::beginDeferredSceneLoad(const std::string& sceneName) { + if (sceneLoadInProgress || !projectManager.currentProject.isLoaded) return; + + sceneObjects.clear(); + clearSelection(); + nextObjectId = 0; + undoStack.clear(); + redoStack.clear(); + + sceneLoadInProgress = true; + sceneLoadProgress = 0.0f; + sceneLoadStatus = "Reading scene..."; + sceneLoadSceneName = sceneName; + sceneLoadObjects.clear(); + sceneLoadAssetIndices.clear(); + sceneLoadAssetsDone = 0; + sceneLoadNextId = 0; + sceneLoadVersion = 9; + sceneLoadTimeOfDay = -1.0f; + showLauncher = true; + projectLoadStartTime = glfwGetTime(); + + fs::path scenePath = projectManager.currentProject.getSceneFilePath(sceneName); + if (!fs::exists(scenePath)) { + sceneLoadInProgress = false; + addConsoleMessage("Default scene not found, starting with a new scene.", ConsoleMessageType::Info); + addObject(ObjectType::Cube, "Cube"); + showLauncher = false; + return; + } + + if (!SceneSerializer::loadSceneDeferred(scenePath, sceneLoadObjects, sceneLoadNextId, sceneLoadVersion, &sceneLoadTimeOfDay)) { + sceneLoadInProgress = false; + addConsoleMessage("Error: Failed to load scene: " + sceneName, ConsoleMessageType::Error); + addObject(ObjectType::Cube, "Cube"); + showLauncher = false; + return; + } + + for (size_t i = 0; i < sceneLoadObjects.size(); ++i) { + const auto& obj = sceneLoadObjects[i]; + if (!obj.hasRenderer) continue; + if ((obj.renderType == RenderType::OBJMesh || obj.renderType == RenderType::Model) && + !obj.meshPath.empty()) { + sceneLoadAssetIndices.push_back(i); + } + } + + if (sceneLoadAssetIndices.empty()) { + sceneLoadProgress = 1.0f; + sceneLoadStatus = "Finalizing scene..."; + finalizeDeferredSceneLoad(); + } else { + sceneLoadProgress = 0.0f; + sceneLoadStatus = "Loading scene assets..."; + } +} + +void Engine::pollSceneLoad() { + if (!sceneLoadInProgress) return; + + if (sceneLoadAssetIndices.empty()) { + return; + } + + constexpr size_t kAssetsPerFrame = 1; + size_t processed = 0; + while (sceneLoadAssetsDone < sceneLoadAssetIndices.size() && processed < kAssetsPerFrame) { + size_t objIndex = sceneLoadAssetIndices[sceneLoadAssetsDone]; + SceneObject& obj = sceneLoadObjects[objIndex]; + + if (obj.renderType == RenderType::OBJMesh) { + std::string err; + obj.meshId = g_objLoader.loadOBJ(obj.meshPath, err); + if (obj.meshId < 0 && !err.empty()) { + std::cerr << "Failed to load OBJ: " << err << std::endl; + } + } else if (obj.renderType == RenderType::Model) { + ModelSceneData sceneData; + std::string err; + if (getModelLoader().loadModelScene(obj.meshPath, sceneData, err)) { + int sourceIndex = obj.meshSourceIndex; + if (sourceIndex < 0 || sourceIndex >= (int)sceneData.meshIndices.size()) { + sourceIndex = 0; + } + if (!sceneData.meshIndices.empty() && + sourceIndex >= 0 && sourceIndex < (int)sceneData.meshIndices.size()) { + obj.meshId = sceneData.meshIndices[sourceIndex]; + } + ApplyModelRootTransform(obj, sceneData); + } else { + std::cerr << "Failed to load model from scene: " << err << std::endl; + obj.meshId = -1; + } + } + + ++sceneLoadAssetsDone; + ++processed; + } + + float total = static_cast(sceneLoadAssetIndices.size()); + sceneLoadProgress = total > 0.0f ? (static_cast(sceneLoadAssetsDone) / total) : 1.0f; + sceneLoadStatus = "Loading scene assets (" + std::to_string(sceneLoadAssetsDone) + "/" + + std::to_string(sceneLoadAssetIndices.size()) + ")"; + + if (sceneLoadAssetsDone >= sceneLoadAssetIndices.size()) { + sceneLoadStatus = "Finalizing scene..."; + finalizeDeferredSceneLoad(); + } +} + +void Engine::finalizeDeferredSceneLoad() { + if (!sceneLoadInProgress) return; + + sceneObjects = std::move(sceneLoadObjects); + nextObjectId = sceneLoadNextId; + + initializeLocalTransformsFromWorld(sceneLoadVersion); + rebuildSkeletalBindings(); + + projectManager.currentProject.currentSceneName = sceneLoadSceneName; + projectManager.currentProject.hasUnsavedChanges = false; + projectManager.currentProject.saveProjectFile(); + clearSelection(); + + bool hasAnyLight = std::any_of(sceneObjects.begin(), sceneObjects.end(), [](const SceneObject& o) { + return o.hasLight; + }); + if (!hasAnyLight) { + addObject(ObjectType::DirectionalLight, "Directional Light"); + } + + recordState("sceneLoaded"); + addConsoleMessage("Loaded scene: " + sceneLoadSceneName, ConsoleMessageType::Success); + if (sceneLoadTimeOfDay >= 0.0f) { + if (Skybox* skybox = renderer.getSkybox()) { + skybox->setTimeOfDay(sceneLoadTimeOfDay); + } + } + + sceneLoadInProgress = false; + sceneLoadProgress = 1.0f; + sceneLoadStatus.clear(); + sceneLoadAssetIndices.clear(); + showLauncher = false; +} + void Engine::finishProjectLoad(ProjectLoadResult& result) { if (!result.success) { projectManager.errorMessage = result.error.empty() ? "Failed to load project file" : result.error; @@ -1520,12 +3005,18 @@ void Engine::finishProjectLoad(ProjectLoadResult& result) { addConsoleMessage("Warning: PhysX failed to initialize; physics disabled for this session", ConsoleMessageType::Warning); } - loadRecentScenes(); + loadBuildSettings(); + if (autoStartRequested && !autoStartSceneName.empty()) { + beginDeferredSceneLoad(autoStartSceneName); + } else { + loadRecentScenes(); + } fs::path contentRoot = projectManager.currentProject.usesNewLayout ? projectManager.currentProject.assetsPath : projectManager.currentProject.projectPath; fileBrowser.setProjectRoot(contentRoot); fileBrowser.currentPath = contentRoot; + loadEditorUserSettings(); fileBrowser.needsRefresh = true; scriptEditorWindowsDirty = true; scriptEditorWindows.clear(); @@ -1533,10 +3024,640 @@ void Engine::finishProjectLoad(ProjectLoadResult& result) { autoCompileQueue.clear(); autoCompileQueued.clear(); scriptAutoCompileLastCheck = 0.0; - showLauncher = false; + if (!sceneLoadInProgress) { + showLauncher = false; + } + #ifdef MODULARITY_PLAYER + applyAutoStartMode(); + #else + if (autoStartRequested && autoStartPlayerMode) { + applyAutoStartMode(); + } else { + playerMode = false; + } + #endif + if (playerMode) { + syncPlayerCamera(); + } addConsoleMessage("Opened project: " + projectManager.currentProject.name, ConsoleMessageType::Info); } +void Engine::syncPlayerCamera() { + SceneObject* playerCamObj = nullptr; + for (auto& obj : sceneObjects) { + if (obj.hasCamera && obj.camera.type == SceneCameraType::Player) { + playerCamObj = &obj; + break; + } + } + if (!playerCamObj) { + return; + } + Camera cam = makeCameraFromObject(*playerCamObj); + cam.position = playerCamObj->position; + cam.firstMouse = true; + camera = cam; +} + +void Engine::loadAutoStartConfig() { + autoStartRequested = false; + autoStartPlayerMode = false; + autoStartProjectPath.clear(); + autoStartSceneName.clear(); + + fs::path configPath = fs::current_path() / "autostart.modu"; + if (!fs::exists(configPath)) return; + + std::ifstream file(configPath); + if (!file.is_open()) return; + + auto trim = [](std::string& s) { + auto start = s.find_first_not_of(" \t\r\n"); + auto end = s.find_last_not_of(" \t\r\n"); + if (start == std::string::npos || end == std::string::npos) { + s.clear(); + return; + } + s = s.substr(start, end - start + 1); + }; + + std::string line; + bool modeSpecified = false; + bool sawKey = false; + while (std::getline(file, line)) { + trim(line); + if (line.empty() || line[0] == '#') continue; + auto pos = line.find('='); + if (pos == std::string::npos) { + if (!sawKey && autoStartProjectPath.empty()) { + autoStartProjectPath = line; + } + continue; + } + std::string key = line.substr(0, pos); + std::string value = line.substr(pos + 1); + trim(key); + trim(value); + sawKey = true; + if (key == "project") { + autoStartProjectPath = value; + } else if (key == "scene") { + autoStartSceneName = value; + } else if (key == "mode") { + autoStartPlayerMode = (value == "player"); + modeSpecified = true; + } + } + + if (!autoStartProjectPath.empty()) { + fs::path path = autoStartProjectPath; + if (path.is_relative()) { + path = fs::current_path() / path; + } + autoStartProjectPath = path.lexically_normal().string(); + autoStartRequested = true; + if (!modeSpecified) { + autoStartPlayerMode = true; + } + } +} + +void Engine::applyAutoStartMode() { + playerMode = true; + isPlaying = true; + specMode = false; + testMode = false; + gameViewCursorLocked = true; + gameViewportFocused = true; + showHierarchy = false; + showInspector = false; + showFileBrowser = false; + showConsole = false; + showProjectBrowser = false; + showMeshBuilder = false; + showEnvironmentWindow = false; + showCameraWindow = false; + showAnimationWindow = false; + showViewOutput = false; + showSceneGizmos = false; + showGameViewport = false; + showBuildSettings = false; + viewportFullscreen = true; + physics.onPlayStart(sceneObjects); + audio.onPlayStart(sceneObjects); +} + +void Engine::resetBuildSettings() { +#ifdef _WIN32 + buildSettings.platform = BuildPlatform::Windows; +#else + buildSettings.platform = BuildPlatform::Linux; +#endif + buildSettings.architecture = "x86_64"; + buildSettings.developmentBuild = false; + buildSettings.autoConnectProfiler = false; + buildSettings.scriptDebugging = false; + buildSettings.deepProfiling = false; + buildSettings.scriptsOnlyBuild = false; + buildSettings.serverBuild = false; + buildSettings.compressionMethod = "Default"; + buildSettings.scenes.clear(); + buildSettingsSelectedIndex = -1; + buildSettingsDirty = false; +} + +bool Engine::addSceneToBuildSettings(const std::string& sceneName, bool enabled) { + if (sceneName.empty()) return false; + for (const auto& entry : buildSettings.scenes) { + if (entry.name == sceneName) return false; + } + buildSettings.scenes.push_back({sceneName, enabled}); + buildSettingsDirty = true; + return true; +} + +void Engine::loadBuildSettings() { + resetBuildSettings(); + if (!projectManager.currentProject.isLoaded) return; + + fs::path buildPath = projectManager.currentProject.projectPath / "build.modu"; + if (!fs::exists(buildPath)) { + if (!projectManager.currentProject.currentSceneName.empty()) { + addSceneToBuildSettings(projectManager.currentProject.currentSceneName, true); + } + saveBuildSettings(); + return; + } + + auto trim = [](std::string& s) { + auto start = s.find_first_not_of(" \t\r\n"); + auto end = s.find_last_not_of(" \t\r\n"); + if (start == std::string::npos || end == std::string::npos) { + s.clear(); + return; + } + s = s.substr(start, end - start + 1); + }; + + std::ifstream file(buildPath); + std::string line; + while (std::getline(file, line)) { + trim(line); + if (line.empty() || line[0] == '#') continue; + if (line.rfind("platform=", 0) == 0) { + std::string value = line.substr(9); + trim(value); + if (value == "Windows") buildSettings.platform = BuildPlatform::Windows; + else if (value == "Linux") buildSettings.platform = BuildPlatform::Linux; + else if (value == "Android") buildSettings.platform = BuildPlatform::Android; + } else if (line.rfind("architecture=", 0) == 0) { + buildSettings.architecture = line.substr(13); + trim(buildSettings.architecture); + } else if (line.rfind("developmentBuild=", 0) == 0) { + buildSettings.developmentBuild = line.substr(17) == "1"; + } else if (line.rfind("autoConnectProfiler=", 0) == 0) { + buildSettings.autoConnectProfiler = line.substr(20) == "1"; + } else if (line.rfind("scriptDebugging=", 0) == 0) { + buildSettings.scriptDebugging = line.substr(16) == "1"; + } else if (line.rfind("deepProfiling=", 0) == 0) { + buildSettings.deepProfiling = line.substr(14) == "1"; + } else if (line.rfind("scriptsOnlyBuild=", 0) == 0) { + buildSettings.scriptsOnlyBuild = line.substr(17) == "1"; + } else if (line.rfind("serverBuild=", 0) == 0) { + buildSettings.serverBuild = line.substr(12) == "1"; + } else if (line.rfind("compressionMethod=", 0) == 0) { + buildSettings.compressionMethod = line.substr(18); + trim(buildSettings.compressionMethod); + } else if (line.rfind("scene=", 0) == 0) { + std::string value = line.substr(6); + trim(value); + size_t comma = value.find(','); + if (comma != std::string::npos) { + std::string name = value.substr(0, comma); + std::string enabledStr = value.substr(comma + 1); + trim(name); + trim(enabledStr); + if (!name.empty()) { + buildSettings.scenes.push_back({name, enabledStr == "1"}); + } + } + } + } + + if (buildSettings.scenes.empty() && !projectManager.currentProject.currentSceneName.empty()) { + addSceneToBuildSettings(projectManager.currentProject.currentSceneName, true); + } + buildSettingsDirty = false; +} + +void Engine::saveBuildSettings() { + if (!projectManager.currentProject.isLoaded) return; + fs::path buildPath = projectManager.currentProject.projectPath / "build.modu"; + std::ofstream file(buildPath); + file << "# build.modu\n"; + const char* platformName = "Windows"; + if (buildSettings.platform == BuildPlatform::Linux) platformName = "Linux"; + else if (buildSettings.platform == BuildPlatform::Android) platformName = "Android"; + file << "platform=" << platformName << "\n"; + file << "architecture=" << buildSettings.architecture << "\n"; + file << "developmentBuild=" << (buildSettings.developmentBuild ? "1" : "0") << "\n"; + file << "autoConnectProfiler=" << (buildSettings.autoConnectProfiler ? "1" : "0") << "\n"; + file << "scriptDebugging=" << (buildSettings.scriptDebugging ? "1" : "0") << "\n"; + file << "deepProfiling=" << (buildSettings.deepProfiling ? "1" : "0") << "\n"; + file << "scriptsOnlyBuild=" << (buildSettings.scriptsOnlyBuild ? "1" : "0") << "\n"; + file << "serverBuild=" << (buildSettings.serverBuild ? "1" : "0") << "\n"; + file << "compressionMethod=" << buildSettings.compressionMethod << "\n"; + for (const auto& scene : buildSettings.scenes) { + file << "scene=" << scene.name << "," << (scene.enabled ? "1" : "0") << "\n"; + } + buildSettingsDirty = false; +} + +void Engine::startExportBuild(const fs::path& outputDir, bool runAfter) { + if (!projectManager.currentProject.isLoaded) { + addConsoleMessage("No project loaded for export", ConsoleMessageType::Warning); + return; + } + if (exportJob.active) return; + + if (projectManager.currentProject.hasUnsavedChanges) { + saveCurrentScene(); + } else { + projectManager.currentProject.saveProjectFile(); + } + + std::error_code ec; + fs::path normalizedOut = fs::absolute(outputDir, ec); + if (ec) { + addConsoleMessage("Export failed: invalid output path.", ConsoleMessageType::Error); + return; + } + fs::create_directories(normalizedOut, ec); + if (ec) { + addConsoleMessage("Export failed: unable to create output folder.", ConsoleMessageType::Error); + return; + } + + std::string startScene = projectManager.currentProject.currentSceneName; + if (startScene.empty()) { + for (const auto& scene : buildSettings.scenes) { + if (scene.enabled) { + startScene = scene.name; + break; + } + } + } + + { + std::lock_guard lock(exportMutex); + exportJob = ExportJobState{}; + exportJob.active = true; + exportJob.runAfter = runAfter; + exportJob.progress = 0.02f; + exportJob.status = "Preparing export..."; + exportJob.outputDir = normalizedOut; + } + exportCancelRequested = false; + + fs::path sourceRoot = findCMakeSourceRoot(fs::current_path()); + if (sourceRoot.empty()) { + addConsoleMessage("Export failed: could not locate CMakeLists.txt.", ConsoleMessageType::Error); + return; + } + fs::path projectRoot = projectManager.currentProject.projectPath; + bool usesNewLayout = projectManager.currentProject.usesNewLayout; + fs::path scenesPath = projectManager.currentProject.scenesPath; + fs::path scriptsPath = projectManager.currentProject.scriptsPath; + auto future = std::async(std::launch::async, [this, normalizedOut, sourceRoot, projectRoot, startScene, usesNewLayout, scenesPath, scriptsPath]() { + ExportJobResult result; + result.outputDir = normalizedOut; + + auto setStatus = [this](float value, const std::string& status) { + std::lock_guard lock(exportMutex); + exportJob.progress = value; + exportJob.status = status; + }; + auto appendLog = [this](const std::string& text) { + std::lock_guard lock(exportMutex); + exportJob.log += text; + if (!exportJob.log.empty() && exportJob.log.back() != '\n') { + exportJob.log += '\n'; + } + }; + + std::error_code ec; + if (exportCancelRequested.load()) { + result.message = "Export cancelled."; + result.success = false; + return result; + } + fs::create_directories(normalizedOut, ec); + if (ec) { + result.message = "Failed to create export directory."; + return result; + } + + setStatus(0.05f, "Cleaning export output..."); + std::string cleanError; + cleanExportOutput(normalizedOut, "ModularityPlayer", cleanError); + if (!cleanError.empty()) { + result.message = cleanError; + return result; + } + + fs::path sharedBuildRoot = sourceRoot / "build" / "player-cache"; + bool useSharedBuild = fs::exists(sharedBuildRoot / "CMakeCache.txt"); + fs::path buildRoot = useSharedBuild ? sharedBuildRoot : (normalizedOut / "_build"); + if (!useSharedBuild) { + fs::create_directories(buildRoot, ec); + if (ec) { + result.message = "Failed to create build directory."; + return result; + } + } + cleanEditorExecutable(buildRoot); + + setStatus(0.1f, useSharedBuild ? "Configuring cached build..." : "Configuring build..."); + int configureExit = 0; + std::string configureCmd = "cmake -S \"" + sourceRoot.string() + "\" -B \"" + + buildRoot.string() + "\" -DCMAKE_BUILD_TYPE=Release -DMODULARITY_BUILD_EDITOR=OFF"; + appendLog("Running: " + configureCmd); + if (!runCommandStreaming(configureCmd + " 2>&1", appendLog, &configureExit)) { + result.message = "CMake configure failed (exit code " + std::to_string(configureExit) + ")."; + return result; + } + + if (exportCancelRequested.load()) { + result.message = "Export cancelled."; + result.success = false; + return result; + } + + setStatus(0.45f, "Building..."); + int buildExit = 0; + std::string buildCmd = "cmake --build \"" + buildRoot.string() + "\" --config Release --target ModularityPlayer"; + appendLog("Running: " + buildCmd); + auto onBuildChunk = [this, &appendLog](const std::string& chunk) { + appendLog(chunk); + // Parse lines like: "[ 17%] Building CXX object ..." + size_t open = chunk.find('['); + size_t pct = chunk.find('%'); + if (open != std::string::npos && pct != std::string::npos && pct > open) { + std::string num = chunk.substr(open + 1, pct - open - 1); + num.erase(0, num.find_first_not_of(" \t")); + num.erase(num.find_last_not_of(" \t") + 1); + int value = std::atoi(num.c_str()); + if (value >= 0 && value <= 100) { + float progress = 0.45f + (value / 100.0f) * 0.25f; + std::string label = "Building (" + std::to_string(value) + "%)"; + std::lock_guard lock(exportMutex); + exportJob.progress = progress; + exportJob.status = label; + } + } + }; + if (!runCommandStreaming(buildCmd + " 2>&1", onBuildChunk, &buildExit)) { + result.message = "CMake build failed (exit code " + std::to_string(buildExit) + ")."; + return result; + } + + if (exportCancelRequested.load()) { + result.message = "Export cancelled."; + result.success = false; + return result; + } + + setStatus(0.7f, "Copying runtime..."); + fs::path exePath = resolveExecutablePath(buildRoot, "ModularityPlayer"); + if (exePath.empty()) { + result.message = "Built executable not found."; + return result; + } + + fs::path exportRoot = normalizedOut; + fs::create_directories(exportRoot, ec); + if (ec) { + result.message = "Failed to create export root."; + return result; + } + + fs::path destExe = exportRoot / exePath.filename(); + fs::copy_file(exePath, destExe, fs::copy_options::overwrite_existing, ec); + if (ec) { + result.message = "Failed to copy executable."; + return result; + } + + std::string copyError; + if (!copyDirectoryRecursive(sourceRoot / "Resources", exportRoot / "Resources", copyError)) { + result.message = copyError; + return result; + } + + setStatus(0.78f, "Collecting precompiled packages..."); + if (!copyPrecompiledPackages(buildRoot, exportRoot / "Packages" / "ThirdParty", copyError)) { + result.message = copyError; + return result; + } + + setStatus(0.82f, "Collecting engine cache..."); + if (!copyPrecompiledEnginePackages(buildRoot, exportRoot / "Packages" / "Engine", copyError)) { + result.message = copyError; + return result; + } + + setStatus(0.85f, "Copying project..."); + fs::path projectOut = exportRoot / "Project"; + if (fs::exists(projectRoot / "Assets")) { + if (!copyDirectoryRecursive(projectRoot / "Assets", projectOut / "Assets", copyError)) { + result.message = copyError; + return result; + } + } + if (!usesNewLayout) { + if (fs::exists(scenesPath)) { + if (!copyDirectoryRecursive(scenesPath, projectOut / "Scenes", copyError)) { + result.message = copyError; + return result; + } + } + if (fs::exists(scriptsPath)) { + if (!copyDirectoryRecursive(scriptsPath, projectOut / "Scripts", copyError)) { + result.message = copyError; + return result; + } + } + } + fs::path compiledScriptsSrc; + fs::path compiledScriptsDst; + { + ScriptBuildConfig scriptConfig; + std::string configError; + fs::path configPath = resolveScriptsConfigPath(projectManager.currentProject); + if (scriptCompiler.loadConfig(configPath, scriptConfig, configError)) { + compiledScriptsSrc = scriptConfig.outDir; + if (!compiledScriptsSrc.is_absolute()) { + compiledScriptsSrc = projectRoot / compiledScriptsSrc; + } + std::error_code relEc; + fs::path relOutDir = fs::relative(compiledScriptsSrc, projectRoot, relEc); + if (!relEc && !relOutDir.empty()) { + bool hasDotDot = false; + for (const auto& part : relOutDir) { + if (part == "..") { + hasDotDot = true; + break; + } + } + if (!hasDotDot) { + compiledScriptsDst = projectOut / relOutDir; + } + } + if (compiledScriptsDst.empty()) { + compiledScriptsDst = projectOut / "Library" / "CompiledScripts"; + } + } + } + if (compiledScriptsSrc.empty()) { + compiledScriptsSrc = projectRoot / "Library" / "CompiledScripts"; + compiledScriptsDst = projectOut / "Library" / "CompiledScripts"; + } + if (fs::exists(compiledScriptsSrc)) { + if (!copyDirectoryRecursive(compiledScriptsSrc, compiledScriptsDst, copyError)) { + result.message = copyError; + return result; + } + } + if (fs::exists(projectRoot / "Library" / "InstalledPackages")) { + if (!copyDirectoryRecursive(projectRoot / "Library" / "InstalledPackages", + projectOut / "Library" / "InstalledPackages", copyError)) { + result.message = copyError; + return result; + } + } + + std::vector projectFiles = { + projectRoot / "project.modu", + projectRoot / "scripts.modu", + projectRoot / "packages.modu" + }; + for (const auto& src : projectFiles) { + if (!fs::exists(src)) continue; + fs::path dst = projectOut / src.filename(); + fs::copy_file(src, dst, fs::copy_options::overwrite_existing, ec); + if (ec) { + result.message = "Failed to copy project file: " + src.filename().string(); + return result; + } + } + + if (!startScene.empty()) { + fs::path srcScene = scenesPath / (startScene + ".scene"); + fs::path dstScene = usesNewLayout + ? (projectOut / "Assets" / "Scenes" / (startScene + ".scene")) + : (projectOut / "Scenes" / (startScene + ".scene")); + if (fs::exists(srcScene)) { + fs::create_directories(dstScene.parent_path(), ec); + if (!ec) { + fs::copy_file(srcScene, dstScene, fs::copy_options::overwrite_existing, ec); + } + if (ec) { + result.message = "Failed to copy scene: " + srcScene.filename().string(); + return result; + } + } + } + + fs::path autoStartPath = exportRoot / "autostart.modu"; + std::ofstream autoStart(autoStartPath); + if (!autoStart.is_open()) { + result.message = "Failed to write autostart.modu."; + return result; + } + autoStart << "project=Project/project.modu\n"; + if (!startScene.empty()) { + autoStart << "scene=" << startScene << "\n"; + } + autoStart << "mode=player\n"; + autoStart.close(); + + fs::path buildAutoStartPath = buildRoot / "autostart.modu"; + std::ofstream buildAutoStart(buildAutoStartPath); + if (buildAutoStart.is_open()) { + buildAutoStart << "project=" << (exportRoot / "Project" / "project.modu").string() << "\n"; + if (!startScene.empty()) { + buildAutoStart << "scene=" << startScene << "\n"; + } + buildAutoStart << "mode=player\n"; + buildAutoStart.close(); + } + + setStatus(1.0f, "Export complete."); + result.success = true; + result.message = "Export complete."; + return result; + }); + + { + std::lock_guard lock(exportMutex); + exportJob.future = std::move(future); + } +} + +void Engine::pollExportBuild() { + if (!exportJob.active) return; + if (!exportJob.future.valid()) { + exportJob.active = false; + return; + } + auto state = exportJob.future.wait_for(std::chrono::milliseconds(0)); + if (state != std::future_status::ready) return; + + ExportJobResult result = exportJob.future.get(); + { + std::lock_guard lock(exportMutex); + exportJob.done = true; + exportJob.active = false; + exportJob.success = result.success; + exportJob.status = result.message; + exportJob.outputDir = result.outputDir; + exportJob.cancelled = exportCancelRequested.load() && !result.success; + } + + bool runAfter = false; + { + std::lock_guard lock(exportMutex); + runAfter = exportJob.runAfter; + } + + if (result.success) { + addConsoleMessage("Export finished: " + result.outputDir.string(), ConsoleMessageType::Success); + if (runAfter) { + fs::path exePath = result.outputDir / +#ifdef _WIN32 + "ModularityPlayer.exe"; +#else + "ModularityPlayer"; +#endif + if (fs::exists(exePath)) { +#ifdef _WIN32 + std::string runCmd = "start \"\" \"" + exePath.string() + "\""; +#else + std::string runCmd = "\"" + exePath.string() + "\" &"; +#endif + std::string runOut; + runCommandCapture(runCmd + " 2>&1", runOut); + } else { + addConsoleMessage("Export finished, but executable was not found to run.", ConsoleMessageType::Warning); + } + } + } else if (exportJob.cancelled) { + addConsoleMessage("Export cancelled.", ConsoleMessageType::Warning); + } else { + addConsoleMessage("Export failed: " + result.message, ConsoleMessageType::Error); + } +} + void Engine::createNewProject(const char* name, const char* location) { fs::path basePath(location); fs::create_directories(basePath); @@ -1584,6 +3705,7 @@ void Engine::createNewProject(const char* name, const char* location) { addConsoleMessage("Project location: " + newProject.projectPath.string(), ConsoleMessageType::Info); saveCurrentScene(); + loadBuildSettings(); } else { projectManager.errorMessage = "Failed to create project directory"; } @@ -1600,19 +3722,13 @@ void Engine::loadRecentScenes() { fs::path scenePath = projectManager.currentProject.getSceneFilePath(projectManager.currentProject.currentSceneName); if (fs::exists(scenePath)) { - int sceneVersion = 9; - if (SceneSerializer::loadScene(scenePath, sceneObjects, nextObjectId, sceneVersion)) { - initializeLocalTransformsFromWorld(sceneVersion); - addConsoleMessage("Loaded scene: " + projectManager.currentProject.currentSceneName, ConsoleMessageType::Success); - } else { - addConsoleMessage("Warning: Failed to load scene, starting fresh", ConsoleMessageType::Warning); - addObject(ObjectType::Cube, "Cube"); - } + beginDeferredSceneLoad(projectManager.currentProject.currentSceneName); + return; } else { addConsoleMessage("Default scene not found, starting with a new scene.", ConsoleMessageType::Info); addObject(ObjectType::Cube, "Cube"); + recordState("sceneLoaded"); } - recordState("sceneLoaded"); fs::path contentRoot = projectManager.currentProject.usesNewLayout ? projectManager.currentProject.assetsPath @@ -1626,7 +3742,11 @@ void Engine::saveCurrentScene() { if (!projectManager.currentProject.isLoaded) return; fs::path scenePath = projectManager.currentProject.getSceneFilePath(projectManager.currentProject.currentSceneName); - if (SceneSerializer::saveScene(scenePath, sceneObjects, nextObjectId)) { + float timeOfDay = 0.0f; + if (Skybox* skybox = renderer.getSkybox()) { + timeOfDay = skybox->getTimeOfDay(); + } + if (SceneSerializer::saveScene(scenePath, sceneObjects, nextObjectId, timeOfDay)) { projectManager.currentProject.hasUnsavedChanges = false; projectManager.currentProject.saveProjectFile(); addConsoleMessage("Saved scene: " + projectManager.currentProject.currentSceneName, ConsoleMessageType::Success); @@ -1644,22 +3764,29 @@ void Engine::loadScene(const std::string& sceneName) { fs::path scenePath = projectManager.currentProject.getSceneFilePath(sceneName); int sceneVersion = 9; - if (SceneSerializer::loadScene(scenePath, sceneObjects, nextObjectId, sceneVersion)) { + float loadedTimeOfDay = -1.0f; + if (SceneSerializer::loadScene(scenePath, sceneObjects, nextObjectId, sceneVersion, &loadedTimeOfDay)) { initializeLocalTransformsFromWorld(sceneVersion); + rebuildSkeletalBindings(); undoStack.clear(); redoStack.clear(); projectManager.currentProject.currentSceneName = sceneName; projectManager.currentProject.hasUnsavedChanges = false; projectManager.currentProject.saveProjectFile(); clearSelection(); - bool hasDirLight = std::any_of(sceneObjects.begin(), sceneObjects.end(), [](const SceneObject& o) { - return o.type == ObjectType::DirectionalLight; + bool hasAnyLight = std::any_of(sceneObjects.begin(), sceneObjects.end(), [](const SceneObject& o) { + return o.hasLight; }); - if (!hasDirLight) { + if (!hasAnyLight) { addObject(ObjectType::DirectionalLight, "Directional Light"); } recordState("sceneLoaded"); addConsoleMessage("Loaded scene: " + sceneName, ConsoleMessageType::Success); + if (loadedTimeOfDay >= 0.0f) { + if (Skybox* skybox = renderer.getSkybox()) { + skybox->setTimeOfDay(loadedTimeOfDay); + } + } } else { addConsoleMessage("Error: Failed to load scene: " + sceneName, ConsoleMessageType::Error); } @@ -1695,61 +3822,13 @@ void Engine::addObject(ObjectType type, const std::string& baseName) { recordState("addObject"); int id = nextObjectId++; std::string name = baseName + " " + std::to_string(id); - sceneObjects.push_back(SceneObject(name, type, id)); - // Light defaults - if (type == ObjectType::PointLight) { - sceneObjects.back().light.type = LightType::Point; - sceneObjects.back().light.range = 12.0f; - sceneObjects.back().light.intensity = 2.0f; - } else if (type == ObjectType::SpotLight) { - sceneObjects.back().light.type = LightType::Spot; - sceneObjects.back().light.range = 15.0f; - sceneObjects.back().light.intensity = 2.5f; - } else if (type == ObjectType::AreaLight) { - sceneObjects.back().light.type = LightType::Area; - sceneObjects.back().light.range = 10.0f; - sceneObjects.back().light.intensity = 3.0f; - sceneObjects.back().light.size = glm::vec2(2.0f, 2.0f); - } else if (type == ObjectType::PostFXNode) { - sceneObjects.back().postFx.enabled = true; - sceneObjects.back().postFx.bloomEnabled = true; - sceneObjects.back().postFx.colorAdjustEnabled = true; - } else if (type == ObjectType::Camera) { - sceneObjects.back().camera.type = SceneCameraType::Player; - sceneObjects.back().camera.fov = 60.0f; - } else if (type == ObjectType::Mirror) { - sceneObjects.back().useOverlay = true; - sceneObjects.back().material.textureMix = 1.0f; - sceneObjects.back().material.color = glm::vec3(1.0f); - sceneObjects.back().scale = glm::vec3(2.0f, 2.0f, 0.05f); - } else if (type == ObjectType::Plane) { - sceneObjects.back().scale = glm::vec3(2.0f, 2.0f, 0.05f); - } else if (type == ObjectType::Sprite) { - sceneObjects.back().scale = glm::vec3(1.0f, 1.0f, 0.05f); - sceneObjects.back().material.ambientStrength = 1.0f; - } else if (type == ObjectType::Canvas) { - sceneObjects.back().ui.label = "Canvas"; - sceneObjects.back().ui.size = glm::vec2(600.0f, 400.0f); - } else if (type == ObjectType::UIImage) { - sceneObjects.back().ui.label = "Image"; - sceneObjects.back().ui.size = glm::vec2(200.0f, 200.0f); - } else if (type == ObjectType::UISlider) { - sceneObjects.back().ui.label = "Slider"; - sceneObjects.back().ui.size = glm::vec2(240.0f, 32.0f); - } else if (type == ObjectType::UIButton) { - sceneObjects.back().ui.label = "Button"; - sceneObjects.back().ui.size = glm::vec2(160.0f, 40.0f); - } else if (type == ObjectType::UIText) { - sceneObjects.back().ui.label = "Text"; - sceneObjects.back().ui.size = glm::vec2(240.0f, 32.0f); - } else if (type == ObjectType::Sprite2D) { - sceneObjects.back().ui.label = "Sprite2D"; - sceneObjects.back().ui.size = glm::vec2(128.0f, 128.0f); - } - sceneObjects.back().localPosition = sceneObjects.back().position; - sceneObjects.back().localRotation = NormalizeEulerDegrees(sceneObjects.back().rotation); - sceneObjects.back().localScale = sceneObjects.back().scale; - sceneObjects.back().localInitialized = true; + SceneObject obj(name, ObjectType::Empty, id); + ApplyObjectPreset(obj, type); + obj.localPosition = obj.position; + obj.localRotation = NormalizeEulerDegrees(obj.rotation); + obj.localScale = obj.scale; + obj.localInitialized = true; + sceneObjects.push_back(obj); setPrimarySelection(id); if (projectManager.currentProject.isLoaded) { projectManager.currentProject.hasUnsavedChanges = true; @@ -1764,12 +3843,20 @@ void Engine::duplicateSelected() { if (it != sceneObjects.end()) { recordState("duplicate"); int id = nextObjectId++; - SceneObject newObj(it->name + " (Copy)", it->type, id); + SceneObject newObj(it->name + " (Copy)", ObjectType::Empty, id); + newObj.type = it->type; newObj.position = it->position + glm::vec3(1.0f, 0.0f, 0.0f); newObj.rotation = it->rotation; newObj.scale = it->scale; + newObj.hasRenderer = it->hasRenderer; + newObj.renderType = it->renderType; + newObj.hasLight = it->hasLight; + newObj.hasCamera = it->hasCamera; + newObj.hasPostFX = it->hasPostFX; + newObj.hasUI = it->hasUI; newObj.meshPath = it->meshPath; newObj.meshId = it->meshId; + newObj.meshSourceIndex = it->meshSourceIndex; newObj.material = it->material; newObj.materialPath = it->materialPath; newObj.albedoTexturePath = it->albedoTexturePath; @@ -1785,6 +3872,12 @@ void Engine::duplicateSelected() { newObj.rigidbody = it->rigidbody; newObj.hasRigidbody2D = it->hasRigidbody2D; newObj.rigidbody2D = it->rigidbody2D; + newObj.hasCollider2D = it->hasCollider2D; + newObj.collider2D = it->collider2D; + newObj.hasParallaxLayer2D = it->hasParallaxLayer2D; + newObj.parallaxLayer2D = it->parallaxLayer2D; + newObj.hasCameraFollow2D = it->hasCameraFollow2D; + newObj.cameraFollow2D = it->cameraFollow2D; newObj.hasCollider = it->hasCollider; newObj.collider = it->collider; newObj.hasPlayerController = it->hasPlayerController; @@ -1795,6 +3888,12 @@ void Engine::duplicateSelected() { newObj.localInitialized = true; newObj.hasAudioSource = it->hasAudioSource; newObj.audioSource = it->audioSource; + newObj.hasReverbZone = it->hasReverbZone; + newObj.reverbZone = it->reverbZone; + newObj.hasAnimation = it->hasAnimation; + newObj.animation = it->animation; + newObj.hasSkeletalAnimation = it->hasSkeletalAnimation; + newObj.skeletal = it->skeletal; newObj.ui = it->ui; sceneObjects.push_back(newObj); @@ -1807,13 +3906,64 @@ void Engine::duplicateSelected() { } void Engine::deleteSelected() { + if (selectedObjectId < 0 && selectedObjectIds.empty()) { + return; + } + recordState("delete"); + + std::unordered_map idLookup; + idLookup.reserve(sceneObjects.size()); + for (auto& obj : sceneObjects) { + idLookup.emplace(obj.id, &obj); + } + + std::unordered_set toDelete; + std::vector stack; + if (!selectedObjectIds.empty()) { + for (int id : selectedObjectIds) { + if (id >= 0 && toDelete.insert(id).second) { + stack.push_back(id); + } + } + } else if (selectedObjectId >= 0) { + toDelete.insert(selectedObjectId); + stack.push_back(selectedObjectId); + } + + while (!stack.empty()) { + int currentId = stack.back(); + stack.pop_back(); + auto it = idLookup.find(currentId); + if (it == idLookup.end() || !it->second) continue; + + for (int childId : it->second->childIds) { + if (childId >= 0 && toDelete.insert(childId).second) { + stack.push_back(childId); + } + } + + for (const auto& obj : sceneObjects) { + if (obj.parentId == currentId && toDelete.insert(obj.id).second) { + stack.push_back(obj.id); + } + } + } + auto it = std::remove_if(sceneObjects.begin(), sceneObjects.end(), - [this](const SceneObject& obj) { return obj.id == selectedObjectId; }); + [&toDelete](const SceneObject& obj) { return toDelete.count(obj.id) > 0; }); if (it != sceneObjects.end()) { - logToConsole("Deleted object"); sceneObjects.erase(it, sceneObjects.end()); + for (auto& obj : sceneObjects) { + if (toDelete.count(obj.parentId) > 0) { + obj.parentId = -1; + } + obj.childIds.erase(std::remove_if(obj.childIds.begin(), obj.childIds.end(), + [&toDelete](int id) { return toDelete.count(id) > 0; }), obj.childIds.end()); + } + updateHierarchyWorldTransforms(); + logToConsole("Deleted object"); clearSelection(); if (projectManager.currentProject.isLoaded) { projectManager.currentProject.hasUnsavedChanges = true; @@ -1918,17 +4068,151 @@ SceneObject* Engine::findObjectById(int id) { fs::path Engine::resolveScriptBinary(const fs::path& sourcePath) { ScriptBuildConfig config; std::string error; - fs::path cfg = projectManager.currentProject.scriptsConfigPath.empty() - ? projectManager.currentProject.projectPath / "Scripts.modu" - : projectManager.currentProject.scriptsConfigPath; + fs::path cfg = resolveScriptsConfigPath(projectManager.currentProject); if (!scriptCompiler.loadConfig(cfg, config, error)) { return {}; } - ScriptBuildCommands cmds; - if (!scriptCompiler.makeCommands(config, sourcePath, cmds, error)) { + auto resolveSource = [&](const fs::path& input) -> fs::path { + if (input.empty()) return {}; + std::error_code ec; + fs::path abs = fs::absolute(input, ec); + if (ec) abs = input; + if (fs::exists(abs)) return abs; + + fs::path scriptsDir = config.scriptsDir; + if (!scriptsDir.is_absolute()) { + scriptsDir = projectManager.currentProject.projectPath / scriptsDir; + } + + if (input.is_relative()) { + fs::path candidate = projectManager.currentProject.projectPath / input; + if (fs::exists(candidate)) return candidate; + } + + auto remapSuffix = [&](const fs::path& marker) -> fs::path { + std::vector parts; + for (const auto& p : abs) parts.push_back(p); + std::vector markerParts; + for (const auto& p : marker) markerParts.push_back(p); + if (markerParts.empty()) return {}; + for (size_t i = 0; i + markerParts.size() <= parts.size(); ++i) { + bool match = true; + for (size_t k = 0; k < markerParts.size(); ++k) { + if (parts[i + k] != markerParts[k]) { + match = false; + break; + } + } + if (match) { + fs::path suffix; + for (size_t j = i + markerParts.size(); j < parts.size(); ++j) { + suffix /= parts[j]; + } + if (!suffix.empty()) { + fs::path candidate = scriptsDir / suffix; + if (fs::exists(candidate)) return candidate; + } + break; + } + } + return {}; + }; + + fs::path remapped = remapSuffix(fs::path("Assets") / "Scripts"); + if (!remapped.empty()) return remapped; + remapped = remapSuffix("Scripts"); + if (!remapped.empty()) return remapped; + + if (!abs.filename().empty()) { + fs::path candidate = scriptsDir / abs.filename(); + if (fs::exists(candidate)) return candidate; + } return {}; + }; + + fs::path resolvedSource = resolveSource(sourcePath); + if (!resolvedSource.empty()) { + ScriptBuildCommands cmds; + if (scriptCompiler.makeCommands(config, resolvedSource, cmds, error)) { + return cmds.binaryPath; + } } - return cmds.binaryPath; + + fs::path compiledDir = config.outDir; + if (!compiledDir.is_absolute()) { + compiledDir = projectManager.currentProject.projectPath / compiledDir; + } + std::string stem = sourcePath.stem().string(); + if (!stem.empty() && fs::exists(compiledDir)) { + std::error_code dirEc; + for (auto it = fs::recursive_directory_iterator(compiledDir, dirEc); + it != fs::recursive_directory_iterator(); ++it) { + if (it->is_directory()) continue; + fs::path p = it->path(); +#ifdef _WIN32 + if (p.stem() == stem && p.extension() == ".dll") return p; +#else + if (p.stem() == stem && p.extension() == ".so") return p; +#endif + } + } + + return {}; +} + +fs::path Engine::resolveManagedAssembly(const fs::path& sourcePath) { + if (sourcePath.empty()) return {}; + std::error_code ec; + std::string ext = sourcePath.extension().string(); + if (ext == ".cs" || ext == ".csproj") { + fs::path output = getManagedOutputDll(); + if (fs::exists(output)) return output; + } + fs::path abs = fs::absolute(sourcePath, ec); + if (!ec && fs::exists(abs)) return abs; + fs::path candidate = projectManager.currentProject.projectPath / sourcePath; + if (fs::exists(candidate)) return candidate; + return {}; +} + +fs::path Engine::getManagedProjectPath() const { + fs::path root = findManagedProjectRoot(fs::current_path()); + if (root.empty() && projectManager.currentProject.isLoaded) { + root = findManagedProjectRoot(projectManager.currentProject.projectPath); + } +#if defined(__linux__) + if (root.empty()) { + std::error_code ec; + fs::path exe = fs::read_symlink("/proc/self/exe", ec); + if (!ec) { + root = findManagedProjectRoot(exe.parent_path()); + } + } +#endif + if (root.empty()) { + return fs::current_path() / "Scripts" / "Managed" / "ModuCPP.csproj"; + } + return root / "Scripts" / "Managed" / "ModuCPP.csproj"; +} + +fs::path Engine::getManagedOutputDll() const { + fs::path root = findManagedProjectRoot(fs::current_path()); + if (root.empty() && projectManager.currentProject.isLoaded) { + root = findManagedProjectRoot(projectManager.currentProject.projectPath); + } +#if defined(__linux__) + if (root.empty()) { + std::error_code ec; + fs::path exe = fs::read_symlink("/proc/self/exe", ec); + if (!ec) { + root = findManagedProjectRoot(exe.parent_path()); + } + } +#endif + if (root.empty()) { + root = fs::current_path(); + } + return root / "Scripts" / "Managed" / "bin" / "Debug" / "netstandard2.0" / "ModuCPP.dll"; } void Engine::markProjectDirty() { @@ -2051,6 +4335,12 @@ void Engine::compileScriptFile(const fs::path& scriptPath) { return; } + std::string ext = scriptPath.extension().string(); + if (ext == ".cs" || ext == ".csproj") { + compileManagedScripts(); + return; + } + if (compileInProgress) { showCompilePopup = true; lastCompileStatus = "Compile already in progress"; @@ -2065,15 +4355,22 @@ void Engine::compileScriptFile(const fs::path& scriptPath) { lastCompileLog.clear(); lastCompileStatus = "Compiling " + scriptPath.filename().string(); lastCompileSuccess = false; - - fs::path configPath = projectManager.currentProject.scriptsConfigPath; - if (configPath.empty()) { - configPath = projectManager.currentProject.projectPath / "Scripts.modu"; + { + std::lock_guard lock(compileMutex); + compileProgress = 0.05f; + compileStage = "Preparing"; } + fs::path configPath = resolveScriptsConfigPath(projectManager.currentProject); + compileInProgress = true; compileResultReady = false; compileWorker = std::thread([this, scriptPath, configPath]() { + auto setProgress = [this](float value, const char* stage) { + std::lock_guard lock(compileMutex); + compileProgress = value; + compileStage = stage; + }; ScriptCompileJobResult result; result.scriptPath = scriptPath; std::string error; @@ -2086,17 +4383,20 @@ void Engine::compileScriptFile(const fs::path& scriptPath) { if (!scriptCompiler.makeCommands(config, scriptPath, commands, error)) { result.error = error; } else { + setProgress(0.15f, "Compiling"); ScriptCompileOutput output; if (!scriptCompiler.compile(commands, output, error)) { result.compileLog = output.compileLog; result.linkLog = output.linkLog; result.error = error; + setProgress(0.9f, "Finalizing"); } else { result.success = true; result.compileLog = output.compileLog; result.linkLog = output.linkLog; result.binaryPath = commands.binaryPath; result.compiledSource = fs::absolute(scriptPath).lexically_normal().string(); + setProgress(0.85f, "Reloading"); } } } @@ -2107,6 +4407,97 @@ void Engine::compileScriptFile(const fs::path& scriptPath) { }); } +void Engine::compileManagedScripts() { + if (!projectManager.currentProject.isLoaded) { + addConsoleMessage("No project is loaded", ConsoleMessageType::Warning); + return; + } + + if (compileInProgress) { + showCompilePopup = true; + lastCompileStatus = "Compile already in progress"; + return; + } + if (compileWorker.joinable()) { + compileWorker.join(); + } + + fs::path managedProject = getManagedProjectPath(); + if (!fs::exists(managedProject)) { + addConsoleMessage("Managed project not found: " + managedProject.string(), ConsoleMessageType::Error); + return; + } + + showCompilePopup = true; + compilePopupHideTime = 0.0; + lastCompileLog.clear(); + lastCompileStatus = "Compiling managed scripts"; + lastCompileSuccess = false; + { + std::lock_guard lock(compileMutex); + compileProgress = 0.05f; + compileStage = "Preparing"; + } + + compileInProgress = true; + compileResultReady = false; + compileWorker = std::thread([this, managedProject]() { + auto setProgress = [this](float value, const char* stage) { + std::lock_guard lock(compileMutex); + compileProgress = value; + compileStage = stage; + }; + + ScriptCompileJobResult result; + result.isManaged = true; + result.scriptPath = managedProject; + + setProgress(0.2f, "Building"); + + std::string command = "dotnet build \"" + managedProject.string() + "\" -c Debug 2>&1"; + std::string output; + int exitCode = -1; + + auto runCommand = [&]() -> bool { +#if defined(_WIN32) + FILE* pipe = _popen(command.c_str(), "r"); +#else + FILE* pipe = popen(command.c_str(), "r"); +#endif + if (!pipe) return false; + char buffer[256]; + while (fgets(buffer, sizeof(buffer), pipe)) { + output += buffer; + } +#if defined(_WIN32) + exitCode = _pclose(pipe); +#else + exitCode = pclose(pipe); +#endif + return true; + }; + + if (!runCommand()) { + result.error = "Failed to launch dotnet build"; + result.compileLog = output; + } else if (exitCode != 0) { + result.error = "dotnet build failed"; + result.compileLog = output; + } else { + result.success = true; + result.compileLog = output; + result.binaryPath = getManagedOutputDll(); + result.compiledSource = managedProject.string(); + setProgress(0.85f, "Reloading"); + } + + std::lock_guard lock(compileMutex); + compileResult = std::move(result); + compileResultReady = true; + compileInProgress = false; + }); +} + void Engine::updateCompileJob() { if (compileResultReady) { if (compileWorker.joinable()) { @@ -2131,30 +4522,48 @@ void Engine::updateCompileJob() { if (!result.compileLog.empty()) addConsoleMessage(result.compileLog, ConsoleMessageType::Info); if (!result.linkLog.empty()) addConsoleMessage(result.linkLog, ConsoleMessageType::Info); } else { - scriptRuntime.unloadAll(); + if (result.isManaged) { + managedRuntime.unloadAll(); + } else { + scriptRuntime.unloadAll(); + } lastCompileSuccess = true; - lastCompileStatus = "Reloading ModuCore"; + lastCompileStatus = result.isManaged ? "Reloading ModuCPP" : "Reloading ModuCore"; lastCompileLog = result.compileLog + result.linkLog; addConsoleMessage("Compiled script -> " + result.binaryPath.string(), ConsoleMessageType::Success); if (!result.compileLog.empty()) addConsoleMessage(result.compileLog, ConsoleMessageType::Info); if (!result.linkLog.empty()) addConsoleMessage(result.linkLog, ConsoleMessageType::Info); - for (auto& obj : sceneObjects) { - for (auto& sc : obj.scripts) { - std::error_code ec; - fs::path scAbs = fs::absolute(sc.path, ec); - std::string scPathNorm = (ec ? fs::path(sc.path) : scAbs).lexically_normal().string(); - if (scPathNorm == result.compiledSource) { + if (result.isManaged) { + for (auto& obj : sceneObjects) { + for (auto& sc : obj.scripts) { + if (sc.language != ScriptLanguage::CSharp) continue; sc.lastBinaryPath = result.binaryPath.string(); } } + } else { + for (auto& obj : sceneObjects) { + for (auto& sc : obj.scripts) { + std::error_code ec; + fs::path scAbs = fs::absolute(sc.path, ec); + std::string scPathNorm = (ec ? fs::path(sc.path) : scAbs).lexically_normal().string(); + if (scPathNorm == result.compiledSource) { + sc.lastBinaryPath = result.binaryPath.string(); + } + } + } } scriptEditorWindowsDirty = true; refreshScriptEditorWindows(); } + { + std::lock_guard lock(compileMutex); + compileProgress = 1.0f; + compileStage = lastCompileSuccess ? "Done" : "Failed"; + } compilePopupHideTime = glfwGetTime() + 1.0; showCompilePopup = true; } @@ -2165,6 +4574,11 @@ void Engine::updateCompileJob() { compilePopupOpened = false; compilePopupHideTime = 0.0; } + + if (!compileInProgress && managedAutoCompileQueued) { + managedAutoCompileQueued = false; + compileManagedScripts(); + } } void Engine::refreshScriptEditorWindows() { @@ -2213,10 +4627,7 @@ void Engine::refreshScriptEditorWindows() { } // Also scan the configured script output directory for standalone editor tabs. - fs::path configPath = projectManager.currentProject.scriptsConfigPath; - if (configPath.empty()) { - configPath = projectManager.currentProject.projectPath / "Scripts.modu"; - } + fs::path configPath = resolveScriptsConfigPath(projectManager.currentProject); ScriptBuildConfig config; std::string error; if (scriptCompiler.loadConfig(configPath, config, error)) { @@ -2287,6 +4698,7 @@ void Engine::setupImGui() { #ifndef __linux__ io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; #endif + io.IniFilename = nullptr; std::cerr << "[DEBUG] setupImGui: applying theme..." << std::endl; applyModernTheme(); @@ -2323,20 +4735,33 @@ void Engine::initUIStylePresets() { current.builtin = true; uiStylePresets.push_back(current); - UIStylePreset editor; - editor.name = "Editor Style"; - editor.style = ImGui::GetStyle(); - editor.builtin = true; - uiStylePresets.push_back(editor); - UIStylePreset imguiDefault; - imguiDefault.name = "ImGui Default"; + imguiDefault.name = "Imgui Default"; imguiDefault.style = ImGui::GetStyle(); ImGui::StyleColorsDark(&imguiDefault.style); + applyEditorLayoutPreset(imguiDefault.style); imguiDefault.builtin = true; uiStylePresets.push_back(imguiDefault); - uiStylePresetIndex = 0; + UIStylePreset pixel; + pixel.name = "Pixel"; + pixel.style = ImGui::GetStyle(); + applyPixelStyle(pixel.style); + pixel.builtin = true; + uiStylePresets.push_back(pixel); + + UIStylePreset superRound; + superRound.name = "Super Round"; + superRound.style = ImGui::GetStyle(); + applySuperRoundStyle(superRound.style); + superRound.builtin = true; + uiStylePresets.push_back(superRound); + + uiStylePresetIndex = findUIStylePreset(uiStylePresetName); + if (uiStylePresetIndex < 0) { + uiStylePresetIndex = 0; + uiStylePresetName = uiStylePresets[0].name; + } } int Engine::findUIStylePreset(const std::string& name) const { @@ -2371,3 +4796,256 @@ void Engine::registerUIStylePreset(const std::string& name, const ImGuiStyle& st void Engine::registerUIStylePresetFromScript(const std::string& name, const ImGuiStyle& style, bool replace) { registerUIStylePreset(name, style, replace); } + +bool Engine::applyUIStylePresetByName(const std::string& name) { + int idx = findUIStylePreset(name); + if (idx < 0) { + return false; + } + ImVec4 preservedColors[ImGuiCol_COUNT]; + ImGuiStyle& currentStyle = ImGui::GetStyle(); + for (int i = 0; i < ImGuiCol_COUNT; ++i) { + preservedColors[i] = currentStyle.Colors[i]; + } + uiStylePresetIndex = idx; + uiStylePresetName = uiStylePresets[idx].name; + currentStyle = uiStylePresets[idx].style; + for (int i = 0; i < ImGuiCol_COUNT; ++i) { + currentStyle.Colors[i] = preservedColors[i]; + } + return true; +} + +fs::path Engine::getEditorUserSettingsPath() const { + if (!projectManager.currentProject.isLoaded) { + return fs::path(); + } + fs::path settingsDir = projectManager.currentProject.projectPath / "ProjectUserSettings"; + return settingsDir / "EditorUI.ini"; +} + +fs::path Engine::getEditorLayoutPath() const { + return getWorkspaceLayoutPath(WorkspaceMode::Default); +} + +fs::path Engine::getWorkspaceLayoutPath(WorkspaceMode mode) const { + fs::path settingsDir = fs::path("Resources"); + const char* filename = "imgui.ini"; + if (mode == WorkspaceMode::Animation) { + filename = "anim.ini"; + } else if (mode == WorkspaceMode::Scripting) { + filename = "scripter.ini"; + } + return settingsDir / filename; +} + +void Engine::loadEditorUserSettings() { + if (!projectManager.currentProject.isLoaded) { + return; + } + fs::path settingsPath = getEditorUserSettingsPath(); + if (settingsPath.empty() || !fs::exists(settingsPath)) { + return; + } + + auto trim = [](std::string& s) { + size_t start = s.find_first_not_of(" \t\r\n"); + size_t end = s.find_last_not_of(" \t\r\n"); + if (start == std::string::npos || end == std::string::npos) { + s.clear(); + return; + } + s = s.substr(start, end - start + 1); + }; + + fileBrowserFavorites.clear(); + std::vector loadedColors(ImGuiCol_COUNT); + std::vector hasColor(ImGuiCol_COUNT, false); + static std::unordered_map colorIndex; + if (colorIndex.empty()) { + for (int i = 0; i < ImGuiCol_COUNT; ++i) { + colorIndex.emplace(ImGui::GetStyleColorName(i), i); + } + } + + std::ifstream file(settingsPath); + std::string line; + while (std::getline(file, line)) { + trim(line); + if (line.empty() || line[0] == '#') { + continue; + } + size_t eq = line.find('='); + if (eq == std::string::npos) { + continue; + } + std::string key = line.substr(0, eq); + std::string value = line.substr(eq + 1); + trim(key); + trim(value); + if (key == "uiStyle") { + uiStylePresetName = value; + } else if (key == "uiAnimationMode") { + if (value == "Fluid") { + uiAnimationMode = UIAnimationMode::Fluid; + } else if (value == "Snappy") { + uiAnimationMode = UIAnimationMode::Snappy; + } else { + uiAnimationMode = UIAnimationMode::Off; + } + } else if (key == "workspace") { + if (value == "Animation") { + currentWorkspace = WorkspaceMode::Animation; + } else if (value == "Scripting") { + currentWorkspace = WorkspaceMode::Scripting; + } else { + currentWorkspace = WorkspaceMode::Default; + } + } else if (key == "fileBrowserIconScale") { + try { + fileBrowserIconScale = std::stof(value); + } catch (...) { + } + } else if (key == "fileBrowserViewMode") { + if (value == "List") { + fileBrowser.viewMode = FileBrowserViewMode::List; + } else { + fileBrowser.viewMode = FileBrowserViewMode::Grid; + } + } else if (key == "fileBrowserSidebarWidth") { + try { + fileBrowserSidebarWidth = std::stof(value); + } catch (...) { + } + } else if (key == "fileBrowserSidebarVisible") { + showFileBrowserSidebar = (value == "1" || value == "true" || value == "yes"); + } else if (key == "consoleWrapText") { + consoleWrapText = (value == "1" || value == "true" || value == "yes"); + } else if (key == "showAnimationWindow") { + showAnimationWindow = (value == "1" || value == "true" || value == "yes"); + } else if (key.rfind("color.", 0) == 0) { + std::string name = key.substr(6); + auto it = colorIndex.find(name); + if (it != colorIndex.end()) { + std::string parseValue = value; + std::replace(parseValue.begin(), parseValue.end(), ',', ' '); + std::stringstream ss(parseValue); + float r = 0.0f; + float g = 0.0f; + float b = 0.0f; + float a = 1.0f; + if (ss >> r >> g >> b >> a) { + loadedColors[it->second] = ImVec4(r, g, b, a); + hasColor[it->second] = true; + } + } + } else if (key == "favorite") { + if (value.empty()) { + continue; + } + fs::path favPath = fs::path(value); + fs::path baseRoot = fileBrowser.projectRoot.empty() + ? projectManager.currentProject.projectPath + : fileBrowser.projectRoot; + if (favPath.is_relative()) { + favPath = baseRoot / favPath; + } + std::error_code ec; + fs::path canonical = fs::weakly_canonical(favPath, ec); + if (!ec) { + favPath = canonical; + } + fileBrowserFavorites.push_back(favPath); + } + } + + fileBrowserIconScale = std::clamp(fileBrowserIconScale, 0.6f, 2.0f); + fileBrowserSidebarWidth = std::clamp(fileBrowserSidebarWidth, 160.0f, 360.0f); + + applyUIStylePresetByName(uiStylePresetName); + ImGuiStyle& style = ImGui::GetStyle(); + for (int i = 0; i < ImGuiCol_COUNT; ++i) { + if (hasColor[i]) { + style.Colors[i] = loadedColors[i]; + } + } + + applyWorkspacePreset(currentWorkspace, false); + scriptingFilesDirty = true; +} + +void Engine::saveEditorUserSettings() const { + if (!projectManager.currentProject.isLoaded) { + return; + } + fs::path settingsPath = getEditorUserSettingsPath(); + if (settingsPath.empty()) { + return; + } + fs::create_directories(settingsPath.parent_path()); + + std::ofstream file(settingsPath); + if (!file.is_open()) { + return; + } + + file << "# Editor UI settings\n"; + file << std::fixed << std::setprecision(4); + file << "uiStyle=" << uiStylePresetName << "\n"; + const char* animMode = "Off"; + if (uiAnimationMode == UIAnimationMode::Fluid) { + animMode = "Fluid"; + } else if (uiAnimationMode == UIAnimationMode::Snappy) { + animMode = "Snappy"; + } + file << "uiAnimationMode=" << animMode << "\n"; + const char* workspaceName = "Default"; + if (currentWorkspace == WorkspaceMode::Animation) { + workspaceName = "Animation"; + } else if (currentWorkspace == WorkspaceMode::Scripting) { + workspaceName = "Scripting"; + } + file << "workspace=" << workspaceName << "\n"; + file << "fileBrowserIconScale=" << fileBrowserIconScale << "\n"; + file << "fileBrowserViewMode=" << (fileBrowser.viewMode == FileBrowserViewMode::List ? "List" : "Grid") << "\n"; + file << "fileBrowserSidebarWidth=" << fileBrowserSidebarWidth << "\n"; + file << "fileBrowserSidebarVisible=" << (showFileBrowserSidebar ? "1" : "0") << "\n"; + file << "consoleWrapText=" << (consoleWrapText ? "1" : "0") << "\n"; + file << "showAnimationWindow=" << (showAnimationWindow ? "1" : "0") << "\n"; + const ImGuiStyle& style = ImGui::GetStyle(); + for (int i = 0; i < ImGuiCol_COUNT; ++i) { + const ImVec4& c = style.Colors[i]; + file << "color." << ImGui::GetStyleColorName(i) << "=" + << c.x << "," << c.y << "," << c.z << "," << c.w << "\n"; + } + + fs::path baseRoot = fileBrowser.projectRoot.empty() + ? projectManager.currentProject.projectPath + : fileBrowser.projectRoot; + for (const auto& fav : fileBrowserFavorites) { + fs::path stored = fav; + std::error_code ec; + fs::path rel = fs::relative(fav, baseRoot, ec); + std::string relStr = rel.generic_string(); + if (!ec && !rel.empty() && relStr.find("..") != 0) { + stored = rel; + } + file << "favorite=" << stored.generic_string() << "\n"; + } +} + +void Engine::exportEditorThemeLayout() { + if (!projectManager.currentProject.isLoaded) { + addConsoleMessage("No project loaded to export UI settings", ConsoleMessageType::Warning); + return; + } + saveEditorUserSettings(); + fs::path layoutPath = getEditorLayoutPath(); + if (layoutPath.empty()) { + addConsoleMessage("Failed to resolve layout export path", ConsoleMessageType::Error); + return; + } + fs::create_directories(layoutPath.parent_path()); + ImGui::SaveIniSettingsToDisk(layoutPath.string().c_str()); + addConsoleMessage("Exported UI layout to: " + layoutPath.string(), ConsoleMessageType::Success); +} diff --git a/src/Engine.h b/src/Engine.h index 9f8f28e..20027dd 100644 --- a/src/Engine.h +++ b/src/Engine.h @@ -12,6 +12,8 @@ #include "PhysicsSystem.h" #include "AudioSystem.h" #include "PackageManager.h" +#include "ManagedScriptRuntime.h" +#include "ThirdParty/ImGuiColorTextEdit/TextEditor.h" #include "../include/Window/Window.h" #include #include @@ -22,6 +24,7 @@ #include void window_size_callback(GLFWwindow* window, int width, int height); +fs::path resolveScriptsConfigPath(const Project& project); class Engine { private: @@ -74,7 +77,15 @@ private: bool showConsole = true; bool showProjectBrowser = true; // Now merged into file browser bool showMeshBuilder = false; + bool showBuildSettings = false; + bool showStyleEditor = false; + bool showScriptingWindow = false; bool firstFrame = true; + bool playerMode = false; + bool autoStartRequested = false; + bool autoStartPlayerMode = false; + std::string autoStartProjectPath; + std::string autoStartSceneName; std::vector consoleLog; int draggedObjectId = -1; @@ -103,8 +114,34 @@ private: char fileBrowserSearch[256] = ""; float fileBrowserIconScale = 1.0f; // 0.5 to 2.0 range + float fileBrowserSidebarWidth = 220.0f; + bool showFileBrowserSidebar = true; + std::vector fileBrowserFavorites; + std::string uiStylePresetName = "Current"; + enum class UIAnimationMode { + Off = 0, + Snappy = 1, + Fluid = 2 + }; + enum class WorkspaceMode { + Default = 0, + Animation = 1, + Scripting = 2 + }; + UIAnimationMode uiAnimationMode = UIAnimationMode::Off; + WorkspaceMode currentWorkspace = WorkspaceMode::Default; + bool workspaceLayoutDirty = false; + bool pendingWorkspaceReload = false; + fs::path pendingWorkspaceIniPath; + bool editorSettingsDirty = false; bool showEnvironmentWindow = true; bool showCameraWindow = true; + bool showAnimationWindow = false; + int animationTargetId = -1; + int animationSelectedKey = -1; + float animationCurrentTime = 0.0f; + bool animationIsPlaying = false; + float animationLastAppliedTime = -1.0f; bool hierarchyShowTexturePreview = false; bool hierarchyPreviewNearest = false; std::unordered_map texturePreviewFilterOverrides; @@ -121,6 +158,8 @@ private: bool gameViewportFocused = false; bool showGameProfiler = true; bool showCanvasOverlay = false; + bool showUIWorldGrid = true; + bool showSceneGrid3D = false; int gameViewportResolutionIndex = 0; int gameViewportCustomWidth = 1920; int gameViewportCustomHeight = 1080; @@ -139,10 +178,41 @@ private: std::vector meshEditSelectedVertices; std::vector meshEditSelectedEdges; // indices into generated edge list std::vector meshEditSelectedFaces; // indices into mesh faces + struct UIAnimationState { + float hover = 0.0f; + float active = 0.0f; + float sliderValue = 0.0f; + bool initialized = false; + }; + std::unordered_map uiAnimationStates; + struct UIWorldCamera2D { + glm::vec2 position = glm::vec2(0.0f); + float zoom = 100.0f; // pixels per world unit + glm::vec2 viewportSize = glm::vec2(0.0f); + + glm::vec2 WorldToScreen(const glm::vec2& world) const { + return glm::vec2( + (world.x - position.x) * zoom + viewportSize.x * 0.5f, + (position.y - world.y) * zoom + viewportSize.y * 0.5f + ); + } + + glm::vec2 ScreenToWorld(const glm::vec2& screen) const { + return glm::vec2( + (screen.x - viewportSize.x * 0.5f) / zoom + position.x, + position.y - (screen.y - viewportSize.y * 0.5f) / zoom + ); + } + }; + bool uiWorldMode = false; + bool uiWorldPanning = false; + UIWorldCamera2D uiWorldCamera; + bool consoleWrapText = true; enum class MeshEditSelectionMode { Vertex = 0, Edge = 1, Face = 2 }; MeshEditSelectionMode meshEditSelectionMode = MeshEditSelectionMode::Vertex; ScriptCompiler scriptCompiler; ScriptRuntime scriptRuntime; + ManagedScriptRuntime managedRuntime; PhysicsSystem physics; AudioSystem audio; bool showCompilePopup = false; @@ -151,8 +221,58 @@ private: bool lastCompileSuccess = false; std::string lastCompileStatus; std::string lastCompileLog; + float compileProgress = 0.0f; + std::string compileStage; + enum class BuildPlatform { + Windows = 0, + Linux = 1, + Android = 2 + }; + struct BuildSceneEntry { + std::string name; + bool enabled = true; + }; + struct BuildSettings { + BuildPlatform platform = BuildPlatform::Windows; + std::string architecture = "x86_64"; + bool developmentBuild = false; + bool autoConnectProfiler = false; + bool scriptDebugging = false; + bool deepProfiling = false; + bool scriptsOnlyBuild = false; + bool serverBuild = false; + std::string compressionMethod = "Default"; + std::vector scenes; + }; + BuildSettings buildSettings; + int buildSettingsSelectedIndex = -1; + bool buildSettingsDirty = false; + struct ExportJobResult { + bool success = false; + std::string message; + fs::path outputDir; + }; + struct ExportJobState { + bool active = false; + bool done = false; + bool success = false; + bool cancelled = false; + float progress = 0.0f; + std::string status; + std::string log; + fs::path outputDir; + bool runAfter = false; + std::future future; + }; + ExportJobState exportJob; + std::atomic exportCancelRequested = false; + std::mutex exportMutex; + bool showExportDialog = false; + bool exportRunAfter = false; + char exportOutputPath[512] = ""; struct ScriptCompileJobResult { bool success = false; + bool isManaged = false; fs::path scriptPath; fs::path binaryPath; std::string compiledSource; @@ -168,6 +288,7 @@ private: std::unordered_map scriptLastAutoCompileTime; std::deque autoCompileQueue; std::unordered_set autoCompileQueued; + bool managedAutoCompileQueued = false; double scriptAutoCompileLastCheck = 0.0; double scriptAutoCompileInterval = 0.5; struct ProjectLoadResult { @@ -180,6 +301,16 @@ private: double projectLoadStartTime = 0.0; std::string projectLoadPath; std::future projectLoadFuture; + bool sceneLoadInProgress = false; + float sceneLoadProgress = 0.0f; + std::string sceneLoadStatus; + std::string sceneLoadSceneName; + std::vector sceneLoadObjects; + std::vector sceneLoadAssetIndices; + size_t sceneLoadAssetsDone = 0; + int sceneLoadNextId = 0; + int sceneLoadVersion = 9; + float sceneLoadTimeOfDay = -1.0f; bool specMode = false; bool testMode = false; bool collisionWireframe = false; @@ -192,6 +323,21 @@ private: }; std::vector uiStylePresets; int uiStylePresetIndex = 0; + struct ScriptEditorState { + fs::path filePath; + std::string buffer; + bool dirty = false; + bool autoCompileOnSave = true; + bool hasWriteTime = false; + fs::file_time_type lastWriteTime; + }; + ScriptEditorState scriptEditorState; + std::vector scriptingFileList; + std::vector scriptingCompletions; + TextEditor scriptTextEditor; + bool scriptTextEditorReady = false; + char scriptingFilter[128] = ""; + bool scriptingFilesDirty = true; // Private methods SceneObject* getSelectedObject(); glm::vec3 getSelectionCenterWorld(bool worldSpace) const; @@ -222,6 +368,7 @@ private: void renderPlayControlsBar(); void renderEnvironmentWindow(); void renderCameraWindow(); + void renderAnimationWindow(); void renderHierarchyPanel(); void renderObjectNode(SceneObject& obj, const std::string& filter, std::vector& ancestorHasNext, bool isLast, int depth); @@ -230,12 +377,16 @@ private: void renderInspectorPanel(); void renderConsolePanel(); void renderViewport(); + void renderPlayerViewport(); void renderGameViewportWindow(); + void renderBuildSettingsWindow(); + void renderScriptingWindow(); void renderDialogs(); void updateCompileJob(); void renderProjectBrowserPanel(); void renderScriptEditorWindows(); void refreshScriptEditorWindows(); + void refreshScriptingFileList(); Camera makeCameraFromObject(const SceneObject& obj) const; void compileScriptFile(const fs::path& scriptPath); void updateAutoCompileScripts(); @@ -244,13 +395,38 @@ private: void startProjectLoad(const std::string& path); void pollProjectLoad(); void finishProjectLoad(ProjectLoadResult& result); + void beginDeferredSceneLoad(const std::string& sceneName); + void pollSceneLoad(); + void finalizeDeferredSceneLoad(); + void syncPlayerCamera(); void updateScripts(float delta); void updatePlayerController(float delta); void updateRigidbody2D(float delta); + void updateCameraFollow2D(float delta); + void updateSkeletalAnimations(float delta); + void updateSkinningMatrices(); + void rebuildSkeletalBindings(); void initUIStylePresets(); int findUIStylePreset(const std::string& name) const; const UIStylePreset* getUIStylePreset(const std::string& name) const; void registerUIStylePreset(const std::string& name, const ImGuiStyle& style, bool replace); + bool applyUIStylePresetByName(const std::string& name); + void applyWorkspacePreset(WorkspaceMode mode, bool rebuildLayout); + void buildWorkspaceLayout(WorkspaceMode mode); + fs::path getEditorUserSettingsPath() const; + fs::path getEditorLayoutPath() const; + fs::path getWorkspaceLayoutPath(WorkspaceMode mode) const; + void loadEditorUserSettings(); + void saveEditorUserSettings() const; + void exportEditorThemeLayout(); + void resetBuildSettings(); + void loadBuildSettings(); + void saveBuildSettings(); + bool addSceneToBuildSettings(const std::string& sceneName, bool enabled); + void loadAutoStartConfig(); + void applyAutoStartMode(); + void startExportBuild(const fs::path& outputDir, bool runAfter); + void pollExportBuild(); void renderFileBrowserToolbar(); void renderFileBrowserBreadcrumb(); @@ -258,6 +434,7 @@ private: void renderFileBrowserListView(); void renderFileContextMenu(const fs::directory_entry& entry); void handleFileDoubleClick(const fs::directory_entry& entry); + void openScriptInEditor(const fs::path& path); ImVec4 getFileCategoryColor(FileCategory category) const; const char* getFileCategoryIconText(FileCategory category) const; @@ -308,6 +485,10 @@ public: SceneObject* findObjectByName(const std::string& name); SceneObject* findObjectById(int id); fs::path resolveScriptBinary(const fs::path& sourcePath); + fs::path resolveManagedAssembly(const fs::path& sourcePath); + fs::path getManagedProjectPath() const; + fs::path getManagedOutputDll() const; + void compileManagedScripts(); void markProjectDirty(); // Script-accessible logging wrapper void addConsoleMessageFromScript(const std::string& message, ConsoleMessageType type); diff --git a/src/ManagedBindings.cpp b/src/ManagedBindings.cpp new file mode 100644 index 0000000..e5871da --- /dev/null +++ b/src/ManagedBindings.cpp @@ -0,0 +1,145 @@ +#include "ManagedBindings.h" +#include +#include +#include + +int modu_ctx_get_object_id(ScriptContext* ctx) { + return (ctx && ctx->object) ? ctx->object->id : -1; +} + +void modu_ctx_get_position(ScriptContext* ctx, float* x, float* y, float* z) { + if (!ctx || !ctx->object || !x || !y || !z) return; + *x = ctx->object->position.x; + *y = ctx->object->position.y; + *z = ctx->object->position.z; +} + +void modu_ctx_set_position(ScriptContext* ctx, float x, float y, float z) { + if (!ctx) return; + ctx->SetPosition(glm::vec3(x, y, z)); +} + +void modu_ctx_get_rotation(ScriptContext* ctx, float* x, float* y, float* z) { + if (!ctx || !ctx->object || !x || !y || !z) return; + *x = ctx->object->rotation.x; + *y = ctx->object->rotation.y; + *z = ctx->object->rotation.z; +} + +void modu_ctx_set_rotation(ScriptContext* ctx, float x, float y, float z) { + if (!ctx) return; + ctx->SetRotation(glm::vec3(x, y, z)); +} + +void modu_ctx_get_scale(ScriptContext* ctx, float* x, float* y, float* z) { + if (!ctx || !ctx->object || !x || !y || !z) return; + *x = ctx->object->scale.x; + *y = ctx->object->scale.y; + *z = ctx->object->scale.z; +} + +void modu_ctx_set_scale(ScriptContext* ctx, float x, float y, float z) { + if (!ctx) return; + ctx->SetScale(glm::vec3(x, y, z)); +} + +int modu_ctx_has_rigidbody(ScriptContext* ctx) { + return (ctx && ctx->HasRigidbody()) ? 1 : 0; +} + +int modu_ctx_ensure_rigidbody(ScriptContext* ctx, int useGravity, int kinematic) { + if (!ctx) return 0; + return ctx->EnsureRigidbody(useGravity != 0, kinematic != 0) ? 1 : 0; +} + +int modu_ctx_set_rigidbody_velocity(ScriptContext* ctx, float x, float y, float z) { + if (!ctx) return 0; + return ctx->SetRigidbodyVelocity(glm::vec3(x, y, z)) ? 1 : 0; +} + +int modu_ctx_get_rigidbody_velocity(ScriptContext* ctx, float* x, float* y, float* z) { + if (!ctx || !x || !y || !z) return 0; + glm::vec3 velocity(0.0f); + if (!ctx->GetRigidbodyVelocity(velocity)) return 0; + *x = velocity.x; + *y = velocity.y; + *z = velocity.z; + return 1; +} + +int modu_ctx_add_rigidbody_force(ScriptContext* ctx, float x, float y, float z) { + if (!ctx) return 0; + return ctx->AddRigidbodyForce(glm::vec3(x, y, z)) ? 1 : 0; +} + +int modu_ctx_add_rigidbody_impulse(ScriptContext* ctx, float x, float y, float z) { + if (!ctx) return 0; + return ctx->AddRigidbodyImpulse(glm::vec3(x, y, z)) ? 1 : 0; +} + +float modu_ctx_get_setting_float(ScriptContext* ctx, const char* key, float fallback) { + if (!ctx || !key) return fallback; + return ctx->GetSettingFloat(key, fallback); +} + +int modu_ctx_get_setting_bool(ScriptContext* ctx, const char* key, int fallback) { + if (!ctx || !key) return fallback ? 1 : 0; + return ctx->GetSettingBool(key, fallback != 0) ? 1 : 0; +} + +void modu_ctx_get_setting_string(ScriptContext* ctx, const char* key, const char* fallback, + char* outBuffer, int outBufferSize) { + if (!outBuffer || outBufferSize <= 0) return; + std::string value; + if (!ctx || !key) { + value = fallback ? fallback : ""; + } else { + value = ctx->GetSetting(key, fallback ? fallback : ""); + } + std::snprintf(outBuffer, static_cast(outBufferSize), "%s", value.c_str()); +} + +void modu_ctx_set_setting_float(ScriptContext* ctx, const char* key, float value) { + if (!ctx || !key) return; + ctx->SetSettingFloat(key, value); +} + +void modu_ctx_set_setting_bool(ScriptContext* ctx, const char* key, int value) { + if (!ctx || !key) return; + ctx->SetSettingBool(key, value != 0); +} + +void modu_ctx_set_setting_string(ScriptContext* ctx, const char* key, const char* value) { + if (!ctx || !key) return; + ctx->SetSetting(key, value ? value : ""); +} + +void modu_ctx_add_console_message(ScriptContext* ctx, const char* message, int type) { + if (!ctx || !message) return; + ctx->AddConsoleMessage(message, static_cast(type)); +} + +ManagedNativeApi BuildManagedNativeApi() { + ManagedNativeApi api; + api.getObjectId = modu_ctx_get_object_id; + api.getPosition = modu_ctx_get_position; + api.setPosition = modu_ctx_set_position; + api.getRotation = modu_ctx_get_rotation; + api.setRotation = modu_ctx_set_rotation; + api.getScale = modu_ctx_get_scale; + api.setScale = modu_ctx_set_scale; + api.hasRigidbody = modu_ctx_has_rigidbody; + api.ensureRigidbody = modu_ctx_ensure_rigidbody; + api.setRigidbodyVelocity = modu_ctx_set_rigidbody_velocity; + api.getRigidbodyVelocity = modu_ctx_get_rigidbody_velocity; + api.addRigidbodyForce = modu_ctx_add_rigidbody_force; + api.addRigidbodyImpulse = modu_ctx_add_rigidbody_impulse; + api.getSettingFloat = modu_ctx_get_setting_float; + api.getSettingBool = modu_ctx_get_setting_bool; + api.getSettingString = modu_ctx_get_setting_string; + api.setSettingFloat = modu_ctx_set_setting_float; + api.setSettingBool = modu_ctx_set_setting_bool; + api.setSettingString = modu_ctx_set_setting_string; + api.addConsoleMessage = modu_ctx_add_console_message; + return api; +} diff --git a/src/ManagedBindings.h b/src/ManagedBindings.h new file mode 100644 index 0000000..4d7a6c1 --- /dev/null +++ b/src/ManagedBindings.h @@ -0,0 +1,55 @@ +#pragma once + +#include "ScriptRuntime.h" +#include + +extern "C" { +int modu_ctx_get_object_id(ScriptContext* ctx); +void modu_ctx_get_position(ScriptContext* ctx, float* x, float* y, float* z); +void modu_ctx_set_position(ScriptContext* ctx, float x, float y, float z); +void modu_ctx_get_rotation(ScriptContext* ctx, float* x, float* y, float* z); +void modu_ctx_set_rotation(ScriptContext* ctx, float x, float y, float z); +void modu_ctx_get_scale(ScriptContext* ctx, float* x, float* y, float* z); +void modu_ctx_set_scale(ScriptContext* ctx, float x, float y, float z); +int modu_ctx_has_rigidbody(ScriptContext* ctx); +int modu_ctx_ensure_rigidbody(ScriptContext* ctx, int useGravity, int kinematic); +int modu_ctx_set_rigidbody_velocity(ScriptContext* ctx, float x, float y, float z); +int modu_ctx_get_rigidbody_velocity(ScriptContext* ctx, float* x, float* y, float* z); +int modu_ctx_add_rigidbody_force(ScriptContext* ctx, float x, float y, float z); +int modu_ctx_add_rigidbody_impulse(ScriptContext* ctx, float x, float y, float z); +float modu_ctx_get_setting_float(ScriptContext* ctx, const char* key, float fallback); +int modu_ctx_get_setting_bool(ScriptContext* ctx, const char* key, int fallback); +void modu_ctx_get_setting_string(ScriptContext* ctx, const char* key, const char* fallback, + char* outBuffer, int outBufferSize); +void modu_ctx_set_setting_float(ScriptContext* ctx, const char* key, float value); +void modu_ctx_set_setting_bool(ScriptContext* ctx, const char* key, int value); +void modu_ctx_set_setting_string(ScriptContext* ctx, const char* key, const char* value); +void modu_ctx_add_console_message(ScriptContext* ctx, const char* message, int type); +} + +struct ManagedNativeApi { + uint32_t version = 1; + int (*getObjectId)(ScriptContext* ctx) = nullptr; + void (*getPosition)(ScriptContext* ctx, float* x, float* y, float* z) = nullptr; + void (*setPosition)(ScriptContext* ctx, float x, float y, float z) = nullptr; + void (*getRotation)(ScriptContext* ctx, float* x, float* y, float* z) = nullptr; + void (*setRotation)(ScriptContext* ctx, float x, float y, float z) = nullptr; + void (*getScale)(ScriptContext* ctx, float* x, float* y, float* z) = nullptr; + void (*setScale)(ScriptContext* ctx, float x, float y, float z) = nullptr; + int (*hasRigidbody)(ScriptContext* ctx) = nullptr; + int (*ensureRigidbody)(ScriptContext* ctx, int useGravity, int kinematic) = nullptr; + int (*setRigidbodyVelocity)(ScriptContext* ctx, float x, float y, float z) = nullptr; + int (*getRigidbodyVelocity)(ScriptContext* ctx, float* x, float* y, float* z) = nullptr; + int (*addRigidbodyForce)(ScriptContext* ctx, float x, float y, float z) = nullptr; + int (*addRigidbodyImpulse)(ScriptContext* ctx, float x, float y, float z) = nullptr; + float (*getSettingFloat)(ScriptContext* ctx, const char* key, float fallback) = nullptr; + int (*getSettingBool)(ScriptContext* ctx, const char* key, int fallback) = nullptr; + void (*getSettingString)(ScriptContext* ctx, const char* key, const char* fallback, + char* outBuffer, int outBufferSize) = nullptr; + void (*setSettingFloat)(ScriptContext* ctx, const char* key, float value) = nullptr; + void (*setSettingBool)(ScriptContext* ctx, const char* key, int value) = nullptr; + void (*setSettingString)(ScriptContext* ctx, const char* key, const char* value) = nullptr; + void (*addConsoleMessage)(ScriptContext* ctx, const char* message, int type) = nullptr; +}; + +ManagedNativeApi BuildManagedNativeApi(); diff --git a/src/ManagedScriptRuntime.cpp b/src/ManagedScriptRuntime.cpp new file mode 100644 index 0000000..80ff80d --- /dev/null +++ b/src/ManagedScriptRuntime.cpp @@ -0,0 +1,452 @@ +#include "ManagedScriptRuntime.h" +#include +#include +#include +#include +#include + +#if MODULARITY_USE_MONO +#include +#include +#include +#include +#include +#endif + +#if MODULARITY_USE_MONO +namespace { +std::string trim(std::string value) { + auto is_space = [](unsigned char c) { return std::isspace(c) != 0; }; + while (!value.empty() && is_space(static_cast(value.front()))) { + value.erase(value.begin()); + } + while (!value.empty() && is_space(static_cast(value.back()))) { + value.pop_back(); + } + return value; +} + +std::string stripAssemblyQualifier(const std::string& typeName) { + auto comma = typeName.find(','); + if (comma == std::string::npos) { + return trim(typeName); + } + return trim(typeName.substr(0, comma)); +} + +bool splitNamespaceAndName(const std::string& fullName, std::string& nameSpace, std::string& name) { + auto dot = fullName.rfind('.'); + if (dot == std::string::npos) { + nameSpace.clear(); + name = fullName; + return !name.empty(); + } + nameSpace = fullName.substr(0, dot); + name = fullName.substr(dot + 1); + return !name.empty(); +} + +std::string monoExceptionToString(MonoObject* exc) { + if (!exc) return "Unknown managed exception"; + MonoString* strObj = mono_object_to_string(exc, nullptr); + if (!strObj) return "Managed exception (no ToString)"; + char* utf8 = mono_string_to_utf8(strObj); + std::string out = utf8 ? utf8 : "Managed exception (utf8 conversion failed)"; + mono_free(utf8); + return out; +} + +fs::path resolveMonoRoot() { + const char* env = std::getenv("MODU_MONO_ROOT"); + if (env && env[0] != '\0') { + return fs::path(env); + } + + std::vector candidates; + fs::path cwd = fs::current_path(); + candidates.push_back(cwd / "src" / "ThirdParty" / "mono"); + candidates.push_back(cwd / ".." / "src" / "ThirdParty" / "mono"); + candidates.push_back(cwd / "ThirdParty" / "mono"); + candidates.push_back(cwd / ".." / "ThirdParty" / "mono"); + + for (const auto& path : candidates) { + std::error_code ec; + if (fs::exists(path, ec)) { + return path; + } + } + + return {}; +} + +void configureMonoFromRoot(const fs::path& root) { + fs::path libDir = root / "lib"; + fs::path etcDir = root / "etc"; + mono_set_dirs(libDir.string().c_str(), etcDir.string().c_str()); + + fs::path assembliesDir = root / "lib" / "mono" / "4.5"; + if (fs::exists(assembliesDir)) { + mono_set_assemblies_path(assembliesDir.string().c_str()); + } + mono_config_parse(nullptr); +} + +std::string toKey(const fs::path& assemblyPath, const std::string& typeName) { + return assemblyPath.lexically_normal().string() + "|" + typeName; +} +} // namespace + +struct ManagedScriptRuntime::MonoState { + MonoDomain* rootDomain = nullptr; + MonoDomain* scriptDomain = nullptr; + fs::path monoRoot; +}; + +ManagedScriptRuntime::~ManagedScriptRuntime() { + unloadAll(); + monoState.reset(); +} + +void ManagedScriptRuntime::MonoStateDeleter::operator()(MonoState* state) const { + if (!state) return; + if (state->rootDomain) { + if (state->scriptDomain) { + mono_domain_set(state->rootDomain, false); + mono_domain_unload(state->scriptDomain); + state->scriptDomain = nullptr; + } + mono_jit_cleanup(state->rootDomain); + } + delete state; +} + +bool ManagedScriptRuntime::ensureHost(const fs::path& assemblyPath) { + (void)assemblyPath; + if (monoState && monoState->rootDomain && monoState->scriptDomain) return true; + + if (!monoState) { + fs::path monoRoot = resolveMonoRoot(); + if (monoRoot.empty()) { + lastError = "Mono root not found. Set MODU_MONO_ROOT or vendor Mono in src/ThirdParty/mono."; + return false; + } + + auto* state = new MonoState(); + state->monoRoot = monoRoot; + configureMonoFromRoot(monoRoot); + state->rootDomain = mono_jit_init_version("Modularity", "v4.0.30319"); + if (!state->rootDomain) { + delete state; + lastError = "Failed to initialize Mono JIT"; + return false; + } + monoState.reset(state); + + std::cerr << "[Managed] Mono root: " << monoRoot << std::endl; + } + + if (!monoState->scriptDomain) { + monoState->scriptDomain = mono_domain_create_appdomain(const_cast("ModularityScripts"), nullptr); + if (!monoState->scriptDomain) { + lastError = "Failed to create Mono appdomain"; + return false; + } + } + + mono_domain_set(monoState->scriptDomain, false); + mono_thread_attach(monoState->scriptDomain); + return true; +} + +static MonoAssembly* loadAssembly(MonoDomain* domain, const fs::path& assemblyPath, MonoImage** outImage, + std::string& error) { + if (!outImage) { + error = "Internal error: missing image output"; + return nullptr; + } + *outImage = nullptr; + + std::error_code ec; + if (!fs::exists(assemblyPath, ec)) { + error = "Missing managed assembly: " + assemblyPath.string(); + return nullptr; + } + + mono_domain_set(domain, false); + MonoAssembly* assembly = mono_domain_assembly_open(domain, assemblyPath.string().c_str()); + if (!assembly) { + error = "Mono failed to load assembly: " + assemblyPath.string(); + return nullptr; + } + MonoImage* image = mono_assembly_get_image(assembly); + if (!image) { + error = "Mono failed to get image: " + assemblyPath.string(); + return nullptr; + } + + *outImage = image; + return assembly; +} + +bool ManagedScriptRuntime::ensureApiInjected(const fs::path& assemblyPath) { + if (apiInjected) return true; + if (!monoState || !monoState->scriptDomain) return false; + + std::string error; + MonoImage* image = nullptr; + MonoAssembly* assembly = loadAssembly(monoState->scriptDomain, assemblyPath, &image, error); + if (!assembly || !image) { + lastError = error; + return false; + } + + MonoClass* hostClass = mono_class_from_name(image, "ModuCPP", "Host"); + if (!hostClass) { + lastError = "Managed class ModuCPP.Host not found"; + return false; + } + + MonoMethod* setApiMethod = mono_class_get_method_from_name(hostClass, "SetNativeApi", 1); + if (!setApiMethod) { + lastError = "Managed method ModuCPP.Host.SetNativeApi not found"; + return false; + } + + mono_domain_set(monoState->scriptDomain, false); + mono_thread_attach(monoState->scriptDomain); + intptr_t apiPtr = reinterpret_cast(&api); + void* args[1] = { &apiPtr }; + MonoObject* exc = nullptr; + mono_runtime_invoke(setApiMethod, nullptr, args, &exc); + if (exc) { + lastError = monoExceptionToString(exc); + return false; + } + + apiInjected = true; + return true; +} + +bool ManagedScriptRuntime::loadModuleMethods(Module& mod, const fs::path& assemblyPath, + const std::string& typeName) { + if (!monoState || !monoState->scriptDomain || typeName.empty()) { + lastError = "Managed script type is required"; + return false; + } + + std::string error; + MonoImage* image = nullptr; + MonoAssembly* assembly = loadAssembly(monoState->scriptDomain, assemblyPath, &image, error); + if (!assembly || !image) { + lastError = error; + return false; + } + + std::string normalized = stripAssemblyQualifier(typeName); + std::string nameSpace; + std::string className; + if (!splitNamespaceAndName(normalized, nameSpace, className)) { + lastError = "Managed script type name is invalid"; + return false; + } + + MonoClass* klass = mono_class_from_name(image, nameSpace.c_str(), className.c_str()); + if (!klass) { + lastError = "Managed type not found: " + normalized; + return false; + } + + MonoMethod* inspector = mono_class_get_method_from_name(klass, "Script_OnInspector", 1); + MonoMethod* begin = mono_class_get_method_from_name(klass, "Script_Begin", 2); + MonoMethod* spec = mono_class_get_method_from_name(klass, "Script_Spec", 2); + MonoMethod* testEditor = mono_class_get_method_from_name(klass, "Script_TestEditor", 2); + MonoMethod* update = mono_class_get_method_from_name(klass, "Script_Update", 2); + MonoMethod* tickUpdate = mono_class_get_method_from_name(klass, "Script_TickUpdate", 2); + + mod.inspectorMethod = inspector; + mod.beginMethod = begin; + mod.specMethod = spec; + mod.testEditorMethod = testEditor; + mod.updateMethod = update; + mod.tickUpdateMethod = tickUpdate; + + if (!inspector && !begin && !spec && !testEditor && !update && !tickUpdate) { + lastError = "No managed script exports found"; + return false; + } + + return true; +} + +ManagedScriptRuntime::Module* ManagedScriptRuntime::getModule(const fs::path& assemblyPath, + const std::string& typeName) { + lastError.clear(); + if (assemblyPath.empty()) { + lastError = "Managed assembly path is empty"; + return nullptr; + } + + std::string key = toKey(assemblyPath, typeName); + auto it = modules.find(key); + if (it != modules.end()) return &it->second; + + if (!ensureHost(assemblyPath)) return nullptr; + if (!ensureApiInjected(assemblyPath)) return nullptr; + + Module mod; + mod.assemblyPath = assemblyPath; + mod.typeName = typeName; + if (!loadModuleMethods(mod, assemblyPath, typeName)) return nullptr; + + modules[key] = mod; + return &modules[key]; +} + +static bool invokeMonoMethod(MonoDomain* domain, MonoMethod* method, ScriptContext* ctx, + float deltaTime, bool hasDelta, std::string& error) { + if (!method) return false; + mono_domain_set(domain, false); + mono_thread_attach(domain); + + intptr_t ctxPtr = reinterpret_cast(ctx); + void* argsWithDelta[2] = { &ctxPtr, &deltaTime }; + void* argsNoDelta[1] = { &ctxPtr }; + + MonoObject* exc = nullptr; + mono_runtime_invoke(method, nullptr, hasDelta ? argsWithDelta : argsNoDelta, &exc); + if (exc) { + error = monoExceptionToString(exc); + return false; + } + return true; +} + +bool ManagedScriptRuntime::invokeInspector(const fs::path& assemblyPath, const std::string& typeName, + ScriptContext& ctx) { + Module* mod = getModule(assemblyPath, typeName); + if (!mod) return false; + MonoMethod* inspector = reinterpret_cast(mod->inspectorMethod); + if (!inspector) { + lastError.clear(); + return false; + } + std::string error; + bool ok = invokeMonoMethod(monoState->scriptDomain, inspector, &ctx, 0.0f, false, error); + if (!ok) { + lastError = error; + } + return ok; +} + +bool ManagedScriptRuntime::hasInspector(const fs::path& assemblyPath, const std::string& typeName) { + Module* mod = getModule(assemblyPath, typeName); + if (!mod) return false; + if (!mod->inspectorMethod) { + lastError.clear(); + return false; + } + return true; +} + +void ManagedScriptRuntime::tickModule(const fs::path& assemblyPath, const std::string& typeName, + ScriptContext& ctx, float deltaTime, + bool runSpec, bool runTest) { + Module* mod = getModule(assemblyPath, typeName); + if (!mod) return; + + int objId = ctx.object ? ctx.object->id : -1; + MonoMethod* begin = reinterpret_cast(mod->beginMethod); + if (objId >= 0 && begin && mod->beginCalledObjects.find(objId) == mod->beginCalledObjects.end()) { + std::string error; + if (!invokeMonoMethod(monoState->scriptDomain, begin, &ctx, deltaTime, true, error)) { + lastError = error; + return; + } + mod->beginCalledObjects.insert(objId); + } + + MonoMethod* tickUpdate = reinterpret_cast(mod->tickUpdateMethod); + MonoMethod* update = reinterpret_cast(mod->updateMethod); + if (tickUpdate || update) { + std::string error; + if (!invokeMonoMethod(monoState->scriptDomain, tickUpdate ? tickUpdate : update, + &ctx, deltaTime, true, error)) { + lastError = error; + return; + } + } + + if (runSpec) { + MonoMethod* spec = reinterpret_cast(mod->specMethod); + if (spec) { + std::string error; + if (!invokeMonoMethod(monoState->scriptDomain, spec, &ctx, deltaTime, true, error)) { + lastError = error; + return; + } + } + } + if (runTest) { + MonoMethod* test = reinterpret_cast(mod->testEditorMethod); + if (test) { + std::string error; + if (!invokeMonoMethod(monoState->scriptDomain, test, &ctx, deltaTime, true, error)) { + lastError = error; + return; + } + } + } +} + +void ManagedScriptRuntime::unloadAll() { + modules.clear(); + apiInjected = false; + lastError.clear(); + if (monoState && monoState->rootDomain && monoState->scriptDomain) { + mono_domain_set(monoState->rootDomain, false); + mono_domain_unload(monoState->scriptDomain); + monoState->scriptDomain = nullptr; + } +} +#else +ManagedScriptRuntime::~ManagedScriptRuntime() = default; + +struct ManagedScriptRuntime::MonoState {}; + +void ManagedScriptRuntime::MonoStateDeleter::operator()(MonoState* state) const { + delete state; +} + +bool ManagedScriptRuntime::hasInspector(const fs::path& assemblyPath, const std::string& typeName) { + (void)assemblyPath; + (void)typeName; + lastError = "Managed scripts disabled (Mono not built)."; + return false; +} + +bool ManagedScriptRuntime::invokeInspector(const fs::path& assemblyPath, const std::string& typeName, ScriptContext& ctx) { + (void)assemblyPath; + (void)typeName; + (void)ctx; + lastError = "Managed scripts disabled (Mono not built)."; + return false; +} + +void ManagedScriptRuntime::tickModule(const fs::path& assemblyPath, const std::string& typeName, + ScriptContext& ctx, float deltaTime, bool runSpec, bool runTest) { + (void)assemblyPath; + (void)typeName; + (void)ctx; + (void)deltaTime; + (void)runSpec; + (void)runTest; + lastError = "Managed scripts disabled (Mono not built)."; +} + +void ManagedScriptRuntime::unloadAll() { + modules.clear(); + monoState.reset(); + apiInjected = false; + lastError.clear(); +} +#endif diff --git a/src/ManagedScriptRuntime.h b/src/ManagedScriptRuntime.h new file mode 100644 index 0000000..9b8c03c --- /dev/null +++ b/src/ManagedScriptRuntime.h @@ -0,0 +1,50 @@ +#pragma once + +#include "ManagedBindings.h" +#include "ScriptRuntime.h" +#include +#include +#include +#include +#include + +class ManagedScriptRuntime { +public: + ~ManagedScriptRuntime(); + + bool hasInspector(const fs::path& assemblyPath, const std::string& typeName); + bool invokeInspector(const fs::path& assemblyPath, const std::string& typeName, ScriptContext& ctx); + void tickModule(const fs::path& assemblyPath, const std::string& typeName, + ScriptContext& ctx, float deltaTime, bool runSpec, bool runTest); + void unloadAll(); + const std::string& getLastError() const { return lastError; } + + struct Module { + fs::path assemblyPath; + std::string typeName; + void* inspectorMethod = nullptr; + void* beginMethod = nullptr; + void* specMethod = nullptr; + void* testEditorMethod = nullptr; + void* updateMethod = nullptr; + void* tickUpdateMethod = nullptr; + std::unordered_set beginCalledObjects; + }; + + struct MonoState; + struct MonoStateDeleter { + void operator()(MonoState* state) const; + }; + +private: + Module* getModule(const fs::path& assemblyPath, const std::string& typeName); + bool ensureHost(const fs::path& assemblyPath); + bool ensureApiInjected(const fs::path& assemblyPath); + bool loadModuleMethods(Module& mod, const fs::path& assemblyPath, const std::string& typeName); + + std::unordered_map modules; + std::string lastError; + std::unique_ptr monoState; + bool apiInjected = false; + ManagedNativeApi api = BuildManagedNativeApi(); +}; diff --git a/src/ModelLoader.cpp b/src/ModelLoader.cpp index 7eec0bf..99b8a2f 100644 --- a/src/ModelLoader.cpp +++ b/src/ModelLoader.cpp @@ -4,6 +4,10 @@ #include #include #include +#include +#include +#include +#include "ThirdParty/glm/gtc/quaternion.hpp" ModelLoader& ModelLoader::getInstance() { static ModelLoader instance; @@ -11,6 +15,13 @@ ModelLoader& ModelLoader::getInstance() { } static void collectRawMeshData(aiNode* node, const aiScene* scene, const aiMatrix4x4& parentTransform, RawMeshAsset& out); +static bool buildSceneMeshes(const std::string& filepath, const aiScene* scene, + std::vector& loadedMeshes, + ModelSceneData& out, std::string& errorMsg); +static void buildSceneNodes(const aiScene* scene, + const std::vector& meshIndices, + ModelSceneData& out); +static glm::mat4 aiToGlm(const aiMatrix4x4& m); ModelLoader& getModelLoader() { return ModelLoader::getInstance(); @@ -91,6 +102,7 @@ ModelLoadResult ModelLoader::loadModel(const std::string& filepath) { if (loadedMeshes[i].path == filepath) { result.success = true; result.meshIndex = static_cast(i); + result.meshIndices.push_back(result.meshIndex); const auto& mesh = loadedMeshes[i]; result.vertexCount = mesh.vertexCount; result.faceCount = mesh.faceCount; @@ -273,6 +285,92 @@ ModelLoadResult ModelLoader::loadModel(const std::string& filepath) { return result; } +bool ModelLoader::loadModelScene(const std::string& filepath, ModelSceneData& out, std::string& errorMsg) { + out = ModelSceneData(); + + if (!isSupported(filepath)) { + errorMsg = "Unsupported file format: " + fs::path(filepath).extension().string(); + return false; + } + + auto cached = cachedScenes.find(filepath); + if (cached != cachedScenes.end()) { + out = cached->second; + return true; + } + + unsigned int importFlags = + aiProcess_Triangulate | + aiProcess_GenSmoothNormals | + aiProcess_FlipUVs | + aiProcess_CalcTangentSpace | + aiProcess_JoinIdenticalVertices | + aiProcess_SortByPType | + aiProcess_ValidateDataStructure; + + const aiScene* scene = importer.ReadFile(filepath, importFlags); + if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) { + errorMsg = "Assimp error: " + std::string(importer.GetErrorString()); + return false; + } + + if (!buildSceneMeshes(filepath, scene, loadedMeshes, out, errorMsg)) { + return false; + } + + buildSceneNodes(scene, out.meshIndices, out); + + out.animations.clear(); + if (scene->mNumAnimations > 0) { + out.animations.reserve(scene->mNumAnimations); + for (unsigned int i = 0; i < scene->mNumAnimations; ++i) { + aiAnimation* anim = scene->mAnimations[i]; + ModelSceneData::AnimationClip clip; + clip.name = anim->mName.C_Str(); + if (clip.name.empty()) { + clip.name = "Clip_" + std::to_string(i); + } + clip.duration = anim->mDuration; + clip.ticksPerSecond = anim->mTicksPerSecond != 0.0 ? anim->mTicksPerSecond : 25.0; + clip.channels.reserve(anim->mNumChannels); + for (unsigned int c = 0; c < anim->mNumChannels; ++c) { + aiNodeAnim* ch = anim->mChannels[c]; + ModelSceneData::AnimChannel channel; + channel.nodeName = ch->mNodeName.C_Str(); + channel.positions.reserve(ch->mNumPositionKeys); + for (unsigned int k = 0; k < ch->mNumPositionKeys; ++k) { + const auto& key = ch->mPositionKeys[k]; + ModelSceneData::AnimVecKey vk; + vk.time = static_cast(key.mTime); + vk.value = glm::vec3(key.mValue.x, key.mValue.y, key.mValue.z); + channel.positions.push_back(vk); + } + channel.rotations.reserve(ch->mNumRotationKeys); + for (unsigned int k = 0; k < ch->mNumRotationKeys; ++k) { + const auto& key = ch->mRotationKeys[k]; + ModelSceneData::AnimQuatKey qk; + qk.time = static_cast(key.mTime); + qk.value = glm::quat(key.mValue.w, key.mValue.x, key.mValue.y, key.mValue.z); + channel.rotations.push_back(qk); + } + channel.scales.reserve(ch->mNumScalingKeys); + for (unsigned int k = 0; k < ch->mNumScalingKeys; ++k) { + const auto& key = ch->mScalingKeys[k]; + ModelSceneData::AnimVecKey sk; + sk.time = static_cast(key.mTime); + sk.value = glm::vec3(key.mValue.x, key.mValue.y, key.mValue.z); + channel.scales.push_back(sk); + } + clip.channels.push_back(std::move(channel)); + } + out.animations.push_back(std::move(clip)); + } + } + + cachedScenes[filepath] = out; + return true; +} + bool ModelLoader::exportRawMesh(const std::string& inputFile, const std::string& outputFile, std::string& errorMsg) { fs::path inPath(inputFile); if (!fs::exists(inPath)) { @@ -350,17 +448,51 @@ bool ModelLoader::loadRawMesh(const std::string& filepath, RawMeshAsset& out, st return false; } + in.seekg(0, std::ios::end); + std::streamoff fileSize = in.tellg(); + in.seekg(sizeof(header), std::ios::beg); + in.read(reinterpret_cast(&out.boundsMin.x), sizeof(float) * 3); in.read(reinterpret_cast(&out.boundsMax.x), sizeof(float) * 3); + const std::streamoff payloadSize = fileSize - sizeof(header) - sizeof(float) * 6; + const std::streamoff positionsSize = static_cast(sizeof(glm::vec3)) * header.vertexCount; + const std::streamoff normalsSize = static_cast(sizeof(glm::vec3)) * header.vertexCount; + const std::streamoff uvsSize = static_cast(sizeof(glm::vec2)) * header.vertexCount; + const std::streamoff facesSize = static_cast(sizeof(glm::u32vec3)) * header.faceCount; + + bool hasNormals = false; + bool hasUVs = false; + if (payloadSize == positionsSize + normalsSize + uvsSize + facesSize) { + hasNormals = true; + hasUVs = true; + } else if (payloadSize == positionsSize + normalsSize + facesSize) { + hasNormals = true; + } else if (payloadSize == positionsSize + uvsSize + facesSize) { + hasUVs = true; + } else if (payloadSize == positionsSize + facesSize) { + // legacy raw meshes without normals/uvs + } else if (payloadSize < positionsSize + facesSize) { + errorMsg = "Raw mesh data is truncated"; + return false; + } + out.positions.resize(header.vertexCount); - out.normals.resize(header.vertexCount); - out.uvs.resize(header.vertexCount); out.faces.resize(header.faceCount); in.read(reinterpret_cast(out.positions.data()), sizeof(glm::vec3) * out.positions.size()); - in.read(reinterpret_cast(out.normals.data()), sizeof(glm::vec3) * out.normals.size()); - in.read(reinterpret_cast(out.uvs.data()), sizeof(glm::vec2) * out.uvs.size()); + if (hasNormals) { + out.normals.resize(header.vertexCount); + in.read(reinterpret_cast(out.normals.data()), sizeof(glm::vec3) * out.normals.size()); + } else { + out.normals.assign(header.vertexCount, glm::vec3(0.0f)); + } + if (hasUVs) { + out.uvs.resize(header.vertexCount); + in.read(reinterpret_cast(out.uvs.data()), sizeof(glm::vec2) * out.uvs.size()); + } else { + out.uvs.assign(header.vertexCount, glm::vec2(0.0f)); + } in.read(reinterpret_cast(out.faces.data()), sizeof(glm::u32vec3) * out.faces.size()); if (!in.good()) { @@ -435,6 +567,18 @@ bool ModelLoader::saveRawMesh(const RawMeshAsset& asset, const std::string& file outPath.replace_extension(".rmesh"); } + std::vector normalsData; + normalsData.resize(asset.positions.size(), glm::vec3(0.0f)); + if (asset.normals.size() == asset.positions.size()) { + normalsData = asset.normals; + } + + std::vector uvsData; + uvsData.resize(asset.positions.size(), glm::vec2(0.0f)); + if (asset.uvs.size() == asset.positions.size()) { + uvsData = asset.uvs; + } + struct Header { char magic[6] = {'R','M','E','S','H','\0'}; uint32_t version = 1; @@ -455,8 +599,8 @@ bool ModelLoader::saveRawMesh(const RawMeshAsset& asset, const std::string& file out.write(reinterpret_cast(&asset.boundsMin.x), sizeof(float) * 3); out.write(reinterpret_cast(&asset.boundsMax.x), sizeof(float) * 3); out.write(reinterpret_cast(asset.positions.data()), sizeof(glm::vec3) * asset.positions.size()); - out.write(reinterpret_cast(asset.normals.data()), sizeof(glm::vec3) * asset.normals.size()); - out.write(reinterpret_cast(asset.uvs.data()), sizeof(glm::vec2) * asset.uvs.size()); + out.write(reinterpret_cast(normalsData.data()), sizeof(glm::vec3) * normalsData.size()); + out.write(reinterpret_cast(uvsData.data()), sizeof(glm::vec2) * uvsData.size()); out.write(reinterpret_cast(asset.faces.data()), sizeof(glm::u32vec3) * asset.faces.size()); if (!out.good()) { @@ -544,6 +688,84 @@ bool ModelLoader::updateRawMesh(int meshIndex, const RawMeshAsset& asset, std::s return true; } +int ModelLoader::addRawMesh(const RawMeshAsset& asset, const std::string& sourcePath, + const std::string& name, std::string& errorMsg) { + if (asset.positions.empty() || asset.faces.empty()) { + errorMsg = "Raw mesh is empty"; + return -1; + } + + std::vector vertices; + vertices.reserve(asset.faces.size() * 3 * 8); + std::vector triPositions; + triPositions.reserve(asset.faces.size() * 3); + + auto getPos = [&](uint32_t idx) -> const glm::vec3& { return asset.positions[idx]; }; + auto getNorm = [&](uint32_t idx) -> glm::vec3 { + if (idx < asset.normals.size()) return asset.normals[idx]; + return glm::vec3(0.0f); + }; + auto getUV = [&](uint32_t idx) -> glm::vec2 { + if (idx < asset.uvs.size()) return asset.uvs[idx]; + return glm::vec2(0.0f); + }; + + for (const auto& face : asset.faces) { + const uint32_t idx[3] = { face.x, face.y, face.z }; + glm::vec3 faceNormal(0.0f); + if (!asset.hasNormals) { + const glm::vec3& a = getPos(idx[0]); + const glm::vec3& b = getPos(idx[1]); + const glm::vec3& c = getPos(idx[2]); + faceNormal = glm::normalize(glm::cross(b - a, c - a)); + } + for (int i = 0; i < 3; i++) { + glm::vec3 pos = getPos(idx[i]); + glm::vec3 n = asset.hasNormals ? getNorm(idx[i]) : faceNormal; + glm::vec2 uv = asset.hasUVs ? getUV(idx[i]) : glm::vec2(0.0f); + + triPositions.push_back(pos); + vertices.push_back(pos.x); + vertices.push_back(pos.y); + vertices.push_back(pos.z); + vertices.push_back(n.x); + vertices.push_back(n.y); + vertices.push_back(n.z); + vertices.push_back(uv.x); + vertices.push_back(uv.y); + } + } + + if (vertices.empty()) { + errorMsg = "No vertices generated for GPU upload"; + return -1; + } + + OBJLoader::LoadedMesh loaded; + loaded.path = sourcePath; + loaded.name = name.empty() ? "StaticBatch" : name; + loaded.mesh = std::make_unique(vertices.data(), vertices.size() * sizeof(float)); + loaded.vertexCount = static_cast(vertices.size() / 8); + loaded.faceCount = static_cast(asset.faces.size()); + loaded.hasNormals = asset.hasNormals; + loaded.hasTexCoords = asset.hasUVs; + loaded.boundsMin = asset.boundsMin; + loaded.boundsMax = asset.boundsMax; + loaded.triangleVertices = std::move(triPositions); + loaded.positions = asset.positions; + loaded.triangleIndices.clear(); + loaded.triangleIndices.reserve(asset.faces.size() * 3); + for (const auto& face : asset.faces) { + loaded.triangleIndices.push_back(face.x); + loaded.triangleIndices.push_back(face.y); + loaded.triangleIndices.push_back(face.z); + } + + int newIndex = static_cast(loadedMeshes.size()); + loadedMeshes.push_back(std::move(loaded)); + return newIndex; +} + static glm::mat4 aiToGlm(const aiMatrix4x4& m) { return glm::mat4( m.a1, m.b1, m.c1, m.d1, @@ -553,6 +775,303 @@ static glm::mat4 aiToGlm(const aiMatrix4x4& m) { ); } +static glm::vec3 quatToEulerDegrees(const aiQuaternion& q) { + glm::quat gq(q.w, q.x, q.y, q.z); + glm::vec3 euler = glm::degrees(glm::eulerAngles(gq)); + return euler; +} + +static bool buildSceneMeshes(const std::string& filepath, const aiScene* scene, + std::vector& loadedMeshes, + ModelSceneData& out, std::string& errorMsg) { + out.meshIndices.assign(scene->mNumMeshes, -1); + out.meshMaterialIndices.assign(scene->mNumMeshes, -1); + + out.materials.clear(); + out.materials.reserve(scene->mNumMaterials); + for (unsigned int i = 0; i < scene->mNumMaterials; ++i) { + aiMaterial* mat = scene->mMaterials[i]; + ModelMaterialInfo info; + info.name = mat->GetName().C_Str(); + if (info.name.empty()) { + info.name = "Material_" + std::to_string(i); + } + + aiColor3D diffuse(1.0f, 1.0f, 1.0f); + if (AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_DIFFUSE, diffuse)) { + info.props.color = glm::vec3(diffuse.r, diffuse.g, diffuse.b); + } + + aiColor3D specular(0.0f, 0.0f, 0.0f); + if (AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_SPECULAR, specular)) { + float avg = (specular.r + specular.g + specular.b) / 3.0f; + info.props.specularStrength = avg; + } + + float shininess = info.props.shininess; + if (AI_SUCCESS == mat->Get(AI_MATKEY_SHININESS, shininess)) { + info.props.shininess = shininess; + } + + aiString tex; + if (AI_SUCCESS == mat->GetTexture(aiTextureType_DIFFUSE, 0, &tex)) { + info.albedoPath = tex.C_Str(); + } + if (AI_SUCCESS == mat->GetTexture(aiTextureType_NORMALS, 0, &tex)) { + info.normalPath = tex.C_Str(); + } else if (AI_SUCCESS == mat->GetTexture(aiTextureType_HEIGHT, 0, &tex)) { + info.normalPath = tex.C_Str(); + } + + if (!info.albedoPath.empty()) { + info.props.textureMix = 1.0f; + } + + out.materials.push_back(info); + } + + for (unsigned int i = 0; i < scene->mNumMeshes; ++i) { + aiMesh* mesh = scene->mMeshes[i]; + if (!mesh || mesh->mNumVertices == 0 || mesh->mNumFaces == 0) { + continue; + } + + std::vector vertices; + struct BoneVertex { + int ids[4]; + float weights[4]; + }; + std::vector boneVertices; + std::vector vertexBoneIds(mesh->mNumVertices, glm::ivec4(0)); + std::vector vertexBoneWeights(mesh->mNumVertices, glm::vec4(0.0f)); + std::vector triPositions; + std::vector positions; + std::vector triangleIndices; + vertices.reserve(mesh->mNumFaces * 3 * 8); + boneVertices.reserve(mesh->mNumFaces * 3); + triPositions.reserve(mesh->mNumFaces * 3); + positions.reserve(mesh->mNumVertices); + triangleIndices.reserve(mesh->mNumFaces * 3); + + glm::vec3 boundsMin(FLT_MAX); + glm::vec3 boundsMax(-FLT_MAX); + + std::vector boneNames; + std::vector inverseBindMatrices; + if (mesh->mNumBones > 0) { + boneNames.reserve(mesh->mNumBones); + inverseBindMatrices.reserve(mesh->mNumBones); + for (unsigned int b = 0; b < mesh->mNumBones; ++b) { + aiBone* bone = mesh->mBones[b]; + int boneIndex = static_cast(boneNames.size()); + boneNames.push_back(bone->mName.C_Str()); + inverseBindMatrices.push_back(aiToGlm(bone->mOffsetMatrix)); + + for (unsigned int w = 0; w < bone->mNumWeights; ++w) { + unsigned int vId = bone->mWeights[w].mVertexId; + float weight = bone->mWeights[w].mWeight; + if (vId >= vertexBoneWeights.size()) continue; + + glm::vec4& weights = vertexBoneWeights[vId]; + glm::ivec4& ids = vertexBoneIds[vId]; + int replaceIndex = -1; + float minWeight = weight; + for (int k = 0; k < 4; ++k) { + if (weights[k] == 0.0f) { + replaceIndex = k; + break; + } + if (weights[k] < minWeight) { + minWeight = weights[k]; + replaceIndex = k; + } + } + if (replaceIndex >= 0) { + weights[replaceIndex] = weight; + ids[replaceIndex] = boneIndex; + } + } + } + } + + for (unsigned int v = 0; v < mesh->mNumVertices; ++v) { + glm::vec3 pos(mesh->mVertices[v].x, mesh->mVertices[v].y, mesh->mVertices[v].z); + positions.push_back(pos); + boundsMin.x = std::min(boundsMin.x, pos.x); + boundsMin.y = std::min(boundsMin.y, pos.y); + boundsMin.z = std::min(boundsMin.z, pos.z); + boundsMax.x = std::max(boundsMax.x, pos.x); + boundsMax.y = std::max(boundsMax.y, pos.y); + boundsMax.z = std::max(boundsMax.z, pos.z); + } + + for (unsigned int f = 0; f < mesh->mNumFaces; ++f) { + const aiFace& face = mesh->mFaces[f]; + if (face.mNumIndices != 3) continue; + + triangleIndices.push_back(static_cast(face.mIndices[0])); + triangleIndices.push_back(static_cast(face.mIndices[1])); + triangleIndices.push_back(static_cast(face.mIndices[2])); + + for (unsigned int j = 0; j < 3; ++j) { + unsigned int index = face.mIndices[j]; + glm::vec3 pos(mesh->mVertices[index].x, + mesh->mVertices[index].y, + mesh->mVertices[index].z); + vertices.push_back(pos.x); + vertices.push_back(pos.y); + vertices.push_back(pos.z); + triPositions.push_back(pos); + + if (mesh->mNormals) { + glm::vec3 n(mesh->mNormals[index].x, + mesh->mNormals[index].y, + mesh->mNormals[index].z); + vertices.push_back(n.x); + vertices.push_back(n.y); + vertices.push_back(n.z); + } else { + vertices.push_back(0.0f); + vertices.push_back(1.0f); + vertices.push_back(0.0f); + } + + if (mesh->mTextureCoords[0]) { + vertices.push_back(mesh->mTextureCoords[0][index].x); + vertices.push_back(mesh->mTextureCoords[0][index].y); + } else { + vertices.push_back(0.0f); + vertices.push_back(0.0f); + } + + BoneVertex bv{}; + glm::ivec4 ids = vertexBoneIds[index]; + glm::vec4 weights = vertexBoneWeights[index]; + float weightSum = weights.x + weights.y + weights.z + weights.w; + if (weightSum > 0.0f) { + weights /= weightSum; + } + bv.ids[0] = ids.x; + bv.ids[1] = ids.y; + bv.ids[2] = ids.z; + bv.ids[3] = ids.w; + bv.weights[0] = weights.x; + bv.weights[1] = weights.y; + bv.weights[2] = weights.z; + bv.weights[3] = weights.w; + boneVertices.push_back(bv); + } + } + + if (vertices.empty()) { + continue; + } + + OBJLoader::LoadedMesh loaded; + loaded.path = filepath; + loaded.name = mesh->mName.C_Str(); + if (loaded.name.empty()) { + loaded.name = fs::path(filepath).stem().string() + "_mesh" + std::to_string(i); + } + bool isSkinned = mesh->mNumBones > 0 && boneVertices.size() == vertices.size() / 8; + if (isSkinned) { + loaded.mesh = std::make_unique(vertices.data(), vertices.size() * sizeof(float), true, + boneVertices.data(), boneVertices.size() * sizeof(BoneVertex)); + } else { + loaded.mesh = std::make_unique(vertices.data(), vertices.size() * sizeof(float)); + } + loaded.vertexCount = static_cast(vertices.size() / 8); + loaded.faceCount = static_cast(mesh->mNumFaces); + loaded.hasNormals = mesh->mNormals != nullptr; + loaded.hasTexCoords = mesh->mTextureCoords[0] != nullptr; + loaded.boundsMin = boundsMin; + loaded.boundsMax = boundsMax; + loaded.triangleVertices = std::move(triPositions); + loaded.positions = std::move(positions); + loaded.triangleIndices = std::move(triangleIndices); + loaded.isSkinned = isSkinned; + loaded.boneNames = std::move(boneNames); + loaded.inverseBindMatrices = std::move(inverseBindMatrices); + if (isSkinned) { + loaded.boneIds.reserve(boneVertices.size()); + loaded.boneWeights.reserve(boneVertices.size()); + for (const auto& bv : boneVertices) { + loaded.boneIds.emplace_back(bv.ids[0], bv.ids[1], bv.ids[2], bv.ids[3]); + loaded.boneWeights.emplace_back(bv.weights[0], bv.weights[1], bv.weights[2], bv.weights[3]); + } + loaded.baseVertices = vertices; + } + + out.meshMaterialIndices[i] = mesh->mMaterialIndex < (int)out.materials.size() + ? static_cast(mesh->mMaterialIndex) + : -1; + + out.meshIndices[i] = static_cast(loadedMeshes.size()); + loadedMeshes.push_back(std::move(loaded)); + } + + bool anyMesh = false; + for (int idx : out.meshIndices) { + if (idx >= 0) { anyMesh = true; break; } + } + if (!anyMesh) { + errorMsg = "No meshes found in model file"; + return false; + } + + return true; +} + +static void buildSceneNodes(const aiScene* scene, + const std::vector& meshIndices, + ModelSceneData& out) { + std::unordered_set boneNames; + for (unsigned int i = 0; i < scene->mNumMeshes; ++i) { + aiMesh* mesh = scene->mMeshes[i]; + for (unsigned int b = 0; b < mesh->mNumBones; ++b) { + boneNames.insert(mesh->mBones[b]->mName.C_Str()); + } + } + + std::function walk = [&](aiNode* node, int parentIndex) { + ModelNodeInfo info; + info.name = node->mName.C_Str(); + if (info.name.empty()) { + info.name = "Node_" + std::to_string(out.nodes.size()); + } + info.parentIndex = parentIndex; + info.isBone = boneNames.find(info.name) != boneNames.end(); + + aiVector3D scaling(1.0f, 1.0f, 1.0f); + aiVector3D position(0.0f, 0.0f, 0.0f); + aiQuaternion rotation; + node->mTransformation.Decompose(scaling, rotation, position); + + info.localPosition = glm::vec3(position.x, position.y, position.z); + info.localScale = glm::vec3(scaling.x, scaling.y, scaling.z); + info.localRotation = quatToEulerDegrees(rotation); + + for (unsigned int i = 0; i < node->mNumMeshes; ++i) { + unsigned int meshIndex = node->mMeshes[i]; + if (meshIndex < meshIndices.size()) { + info.meshIndices.push_back(static_cast(meshIndex)); + } + } + + int thisIndex = static_cast(out.nodes.size()); + out.nodes.push_back(info); + + for (unsigned int c = 0; c < node->mNumChildren; ++c) { + walk(node->mChildren[c], thisIndex); + } + }; + + out.nodes.clear(); + if (scene->mRootNode) { + walk(scene->mRootNode, -1); + } +} + static void collectRawMeshData(aiNode* node, const aiScene* scene, const aiMatrix4x4& parentTransform, RawMeshAsset& out) { aiMatrix4x4 current = parentTransform * node->mTransformation; glm::mat4 gTransform = aiToGlm(current); @@ -608,6 +1127,89 @@ static void collectRawMeshData(aiNode* node, const aiScene* scene, const aiMatri } } +bool ModelLoader::buildRawMeshFromScene(const std::string& filepath, RawMeshAsset& out, std::string& errorMsg, + glm::vec3* outRootPos, glm::vec3* outRootRot, glm::vec3* outRootScale) { + out = RawMeshAsset(); + + fs::path inPath(filepath); + if (!fs::exists(inPath)) { + errorMsg = "File not found: " + filepath; + return false; + } + if (!isSupported(filepath)) { + errorMsg = "Unsupported file format for raw mesh build"; + return false; + } + + Assimp::Importer localImporter; + unsigned int importFlags = + aiProcess_Triangulate | + aiProcess_JoinIdenticalVertices | + aiProcess_GenSmoothNormals | + aiProcess_FlipUVs; + + const aiScene* scene = localImporter.ReadFile(filepath, importFlags); + if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) { + errorMsg = "Assimp error: " + std::string(localImporter.GetErrorString()); + return false; + } + + aiMatrix4x4 parent; + parent = aiMatrix4x4(); + if (scene->mRootNode && (outRootPos || outRootRot || outRootScale)) { + aiVector3D scaling(1.0f, 1.0f, 1.0f); + aiVector3D position(0.0f, 0.0f, 0.0f); + aiQuaternion rotation; + scene->mRootNode->mTransformation.Decompose(scaling, rotation, position); + if (outRootPos) *outRootPos = glm::vec3(position.x, position.y, position.z); + if (outRootScale) *outRootScale = glm::vec3(scaling.x, scaling.y, scaling.z); + if (outRootRot) *outRootRot = quatToEulerDegrees(rotation); + + aiMatrix4x4 rootTransform = scene->mRootNode->mTransformation; + rootTransform.Inverse(); + parent = rootTransform; + } + + collectRawMeshData(scene->mRootNode, scene, parent, out); + + if (out.positions.empty() || out.faces.empty()) { + errorMsg = "No geometry found to build raw mesh"; + return false; + } + + out.hasNormals = false; + for (const auto& n : out.normals) { + if (glm::length(n) > 1e-4f) { out.hasNormals = true; break; } + } + + out.hasUVs = false; + for (const auto& uv : out.uvs) { + if (std::abs(uv.x) > 1e-6f || std::abs(uv.y) > 1e-6f) { out.hasUVs = true; break; } + } + + if (!out.hasNormals) { + out.normals.assign(out.positions.size(), glm::vec3(0.0f)); + std::vector accum(out.positions.size(), glm::vec3(0.0f)); + for (const auto& face : out.faces) { + const glm::vec3& a = out.positions[face.x]; + const glm::vec3& b = out.positions[face.y]; + const glm::vec3& c = out.positions[face.z]; + glm::vec3 n = glm::normalize(glm::cross(b - a, c - a)); + accum[face.x] += n; + accum[face.y] += n; + accum[face.z] += n; + } + for (size_t i = 0; i < accum.size(); i++) { + if (glm::length(accum[i]) > 1e-6f) { + out.normals[i] = glm::normalize(accum[i]); + } + } + out.hasNormals = true; + } + + return true; +} + void ModelLoader::processNode(aiNode* node, const aiScene* scene, const aiMatrix4x4& parentTransform, std::vector& vertices, std::vector& triPositions, std::vector& positions, std::vector& indices, @@ -720,6 +1322,7 @@ const std::vector& ModelLoader::getAllMeshes() const { void ModelLoader::clear() { loadedMeshes.clear(); + cachedScenes.clear(); } size_t ModelLoader::getMeshCount() const { diff --git a/src/ModelLoader.h b/src/ModelLoader.h index aa1a0fd..5435668 100644 --- a/src/ModelLoader.h +++ b/src/ModelLoader.h @@ -3,6 +3,7 @@ #include "Common.h" #include "Rendering.h" #include +#include #include #include @@ -19,6 +20,7 @@ struct ModelFormat { struct ModelLoadResult { bool success = false; int meshIndex = -1; + std::vector meshIndices; std::string errorMessage; int vertexCount = 0; int faceCount = 0; @@ -41,6 +43,51 @@ struct RawMeshAsset { bool hasUVs = false; }; +struct ModelMaterialInfo { + std::string name; + MaterialProperties props; + std::string albedoPath; + std::string normalPath; +}; + +struct ModelNodeInfo { + std::string name; + int parentIndex = -1; + std::vector meshIndices; + glm::vec3 localPosition = glm::vec3(0.0f); + glm::vec3 localRotation = glm::vec3(0.0f); + glm::vec3 localScale = glm::vec3(1.0f); + bool isBone = false; +}; + +struct ModelSceneData { + std::vector nodes; + std::vector materials; + std::vector meshIndices; + std::vector meshMaterialIndices; + struct AnimVecKey { + float time = 0.0f; + glm::vec3 value = glm::vec3(0.0f); + }; + struct AnimQuatKey { + float time = 0.0f; + glm::quat value = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + }; + struct AnimChannel { + std::string nodeName; + std::vector positions; + std::vector rotations; + std::vector scales; + }; + struct AnimationClip { + std::string name; + double duration = 0.0; + double ticksPerSecond = 0.0; + std::vector channels; + }; + std::vector animations; +}; + class ModelLoader { public: // Singleton access @@ -48,6 +95,9 @@ public: // Load a model file (FBX, OBJ, GLTF, etc.) ModelLoadResult loadModel(const std::string& filepath); + + // Load a model scene with node hierarchy and per-mesh materials + bool loadModelScene(const std::string& filepath, ModelSceneData& out, std::string& errorMsg); // Get mesh by index Mesh* getMesh(int index); @@ -72,6 +122,16 @@ public: // Update an already-loaded raw mesh in GPU memory bool updateRawMesh(int meshIndex, const RawMeshAsset& asset, std::string& errorMsg); + + // Build a raw mesh asset from a model scene without writing to disk + bool buildRawMeshFromScene(const std::string& filepath, RawMeshAsset& out, std::string& errorMsg, + glm::vec3* outRootPos = nullptr, + glm::vec3* outRootRot = nullptr, + glm::vec3* outRootScale = nullptr); + + // Add a raw mesh asset to the GPU cache and return its mesh index + int addRawMesh(const RawMeshAsset& asset, const std::string& sourcePath, + const std::string& name, std::string& errorMsg); // Get list of supported formats static std::vector getSupportedFormats(); @@ -100,6 +160,7 @@ private: // Storage for loaded meshes (reusing OBJLoader::LoadedMesh structure) std::vector loadedMeshes; + std::unordered_map cachedScenes; // Assimp importer (kept for resource management) Assimp::Importer importer; diff --git a/src/PhysicsSystem.cpp b/src/PhysicsSystem.cpp index 4ab3fdf..4b5338b 100644 --- a/src/PhysicsSystem.cpp +++ b/src/PhysicsSystem.cpp @@ -122,9 +122,9 @@ void PhysicsSystem::createGroundPlane() { bool PhysicsSystem::gatherMeshData(const SceneObject& obj, std::vector& vertices, std::vector& indices) const { const OBJLoader::LoadedMesh* meshInfo = nullptr; - if (obj.type == ObjectType::OBJMesh && obj.meshId >= 0) { + if (obj.hasRenderer && obj.renderType == RenderType::OBJMesh && obj.meshId >= 0) { meshInfo = g_objLoader.getMeshInfo(obj.meshId); - } else if (obj.type == ObjectType::Model && obj.meshId >= 0) { + } else if (obj.hasRenderer && obj.renderType == RenderType::Model && obj.meshId >= 0) { meshInfo = getModelLoader().getMeshInfo(obj.meshId); } if (!meshInfo) { @@ -215,21 +215,21 @@ bool PhysicsSystem::attachPrimitiveShape(PxRigidActor* actor, const SceneObject& s->setRestOffset(rest); }; - switch (obj.type) { - case ObjectType::Cube: { + switch (obj.renderType) { + case RenderType::Cube: { PxVec3 halfExtents = ToPxVec3(glm::max(obj.scale * 0.5f, glm::vec3(0.01f))); shape = mPhysics->createShape(PxBoxGeometry(halfExtents), *mDefaultMaterial, true); tuneShape(shape, std::min({halfExtents.x, halfExtents.y, halfExtents.z}) * 2.0f, isDynamic); break; } - case ObjectType::Sphere: { + case RenderType::Sphere: { float radius = std::max({obj.scale.x, obj.scale.y, obj.scale.z}) * 0.5f; radius = std::max(radius, 0.01f); shape = mPhysics->createShape(PxSphereGeometry(radius), *mDefaultMaterial, true); tuneShape(shape, radius * 2.0f, isDynamic); break; } - case ObjectType::Capsule: { + case RenderType::Capsule: { float radius = std::max(obj.scale.x, obj.scale.z) * 0.5f; radius = std::max(radius, 0.01f); float cylHeight = std::max(0.05f, obj.scale.y - radius * 2.0f); @@ -242,21 +242,21 @@ bool PhysicsSystem::attachPrimitiveShape(PxRigidActor* actor, const SceneObject& tuneShape(shape, std::min(radius * 2.0f, halfHeight * 2.0f), isDynamic); break; } - case ObjectType::Plane: { + case RenderType::Plane: { glm::vec3 halfExtents = glm::max(obj.scale * 0.5f, glm::vec3(0.01f)); halfExtents.z = std::max(halfExtents.z, 0.01f); shape = mPhysics->createShape(PxBoxGeometry(ToPxVec3(halfExtents)), *mDefaultMaterial, true); tuneShape(shape, std::min({halfExtents.x, halfExtents.y, halfExtents.z}) * 2.0f, isDynamic); break; } - case ObjectType::Sprite: { + case RenderType::Sprite: { glm::vec3 halfExtents = glm::max(obj.scale * 0.5f, glm::vec3(0.01f)); halfExtents.z = std::max(halfExtents.z, 0.01f); shape = mPhysics->createShape(PxBoxGeometry(ToPxVec3(halfExtents)), *mDefaultMaterial, true); tuneShape(shape, std::min({halfExtents.x, halfExtents.y, halfExtents.z}) * 2.0f, isDynamic); break; } - case ObjectType::Torus: { + case RenderType::Torus: { float radius = std::max({obj.scale.x, obj.scale.y, obj.scale.z}) * 0.5f; radius = std::max(radius, 0.01f); shape = mPhysics->createShape(PxSphereGeometry(radius), *mDefaultMaterial, true); @@ -302,9 +302,9 @@ bool PhysicsSystem::attachColliderShape(PxRigidActor* actor, const SceneObject& minDim = std::min(radius * 2.0f, halfHeight * 2.0f); } else { const OBJLoader::LoadedMesh* meshInfo = nullptr; - if (obj.type == ObjectType::OBJMesh && obj.meshId >= 0) { + if (obj.hasRenderer && obj.renderType == RenderType::OBJMesh && obj.meshId >= 0) { meshInfo = g_objLoader.getMeshInfo(obj.meshId); - } else if (obj.type == ObjectType::Model && obj.meshId >= 0) { + } else if (obj.hasRenderer && obj.renderType == RenderType::Model && obj.meshId >= 0) { meshInfo = getModelLoader().getMeshInfo(obj.meshId); } if (!meshInfo) { @@ -491,7 +491,6 @@ void PhysicsSystem::onPlayStart(const std::vector& objects) { if (!isReady()) return; clearActors(); - createGroundPlane(); struct MeshCookInfo { std::string name; @@ -506,9 +505,9 @@ void PhysicsSystem::onPlayStart(const std::vector& objects) { if (!obj.enabled || !obj.hasCollider || !obj.collider.enabled) continue; if (obj.collider.type == ColliderType::Box || obj.collider.type == ColliderType::Capsule) continue; const OBJLoader::LoadedMesh* meshInfo = nullptr; - if (obj.type == ObjectType::OBJMesh && obj.meshId >= 0) { + if (obj.hasRenderer && obj.renderType == RenderType::OBJMesh && obj.meshId >= 0) { meshInfo = g_objLoader.getMeshInfo(obj.meshId); - } else if (obj.type == ObjectType::Model && obj.meshId >= 0) { + } else if (obj.hasRenderer && obj.renderType == RenderType::Model && obj.meshId >= 0) { meshInfo = getModelLoader().getMeshInfo(obj.meshId); } if (!meshInfo) continue; diff --git a/src/ProjectManager.cpp b/src/ProjectManager.cpp index c64cf0b..ae6439d 100644 --- a/src/ProjectManager.cpp +++ b/src/ProjectManager.cpp @@ -1,6 +1,10 @@ #include "ProjectManager.h" #include "Rendering.h" #include "ModelLoader.h" +#include +#include + +ObjectType GetLegacyTypeFromComponents(const SceneObject& obj); // Project implementation Project::Project(const std::string& projectName, const fs::path& basePath) @@ -293,14 +297,16 @@ bool ProjectManager::loadProject(const std::string& path) { // SceneSerializer implementation bool SceneSerializer::saveScene(const fs::path& filePath, const std::vector& objects, - int nextId) { + int nextId, + float timeOfDay) { try { std::ofstream file(filePath); if (!file.is_open()) return false; file << "# Scene File\n"; - file << "version=11\n"; + file << "version=17\n"; file << "nextId=" << nextId << "\n"; + file << "timeOfDay=" << timeOfDay << "\n"; file << "objectCount=" << objects.size() << "\n"; file << "\n"; @@ -308,10 +314,18 @@ bool SceneSerializer::saveScene(const fs::path& filePath, file << "[Object]\n"; file << "id=" << obj.id << "\n"; file << "name=" << obj.name << "\n"; - file << "type=" << static_cast(obj.type) << "\n"; + ObjectType legacyType = GetLegacyTypeFromComponents(obj); + file << "type=" << static_cast(legacyType) << "\n"; file << "enabled=" << (obj.enabled ? 1 : 0) << "\n"; file << "layer=" << obj.layer << "\n"; file << "tag=" << obj.tag << "\n"; + file << "hasRenderer=" << (obj.hasRenderer ? 1 : 0) << "\n"; + file << "renderType=" << static_cast(obj.renderType) << "\n"; + file << "hasLight=" << (obj.hasLight ? 1 : 0) << "\n"; + file << "hasCamera=" << (obj.hasCamera ? 1 : 0) << "\n"; + file << "hasPostFX=" << (obj.hasPostFX ? 1 : 0) << "\n"; + file << "hasUI=" << (obj.hasUI ? 1 : 0) << "\n"; + file << "uiType=" << static_cast(obj.ui.type) << "\n"; file << "parentId=" << obj.parentId << "\n"; file << "position=" << obj.localPosition.x << "," << obj.localPosition.y << "," << obj.localPosition.z << "\n"; file << "rotation=" << obj.localRotation.x << "," << obj.localRotation.y << "," << obj.localRotation.z << "\n"; @@ -336,6 +350,36 @@ bool SceneSerializer::saveScene(const fs::path& filePath, file << "rb2dLinearDamping=" << obj.rigidbody2D.linearDamping << "\n"; file << "rb2dVelocity=" << obj.rigidbody2D.velocity.x << "," << obj.rigidbody2D.velocity.y << "\n"; } + file << "hasCollider2D=" << (obj.hasCollider2D ? 1 : 0) << "\n"; + if (obj.hasCollider2D) { + file << "collider2dEnabled=" << (obj.collider2D.enabled ? 1 : 0) << "\n"; + file << "collider2dType=" << static_cast(obj.collider2D.type) << "\n"; + file << "collider2dBox=" << obj.collider2D.boxSize.x << "," << obj.collider2D.boxSize.y << "\n"; + file << "collider2dClosed=" << (obj.collider2D.closed ? 1 : 0) << "\n"; + file << "collider2dEdgeThickness=" << obj.collider2D.edgeThickness << "\n"; + file << "collider2dPoints="; + for (size_t i = 0; i < obj.collider2D.points.size(); ++i) { + if (i > 0) file << ";"; + file << obj.collider2D.points[i].x << "," << obj.collider2D.points[i].y; + } + file << "\n"; + } + file << "hasParallaxLayer2D=" << (obj.hasParallaxLayer2D ? 1 : 0) << "\n"; + if (obj.hasParallaxLayer2D) { + file << "parallax2dEnabled=" << (obj.parallaxLayer2D.enabled ? 1 : 0) << "\n"; + file << "parallax2dOrder=" << obj.parallaxLayer2D.order << "\n"; + file << "parallax2dFactor=" << obj.parallaxLayer2D.factor << "\n"; + file << "parallax2dRepeatX=" << (obj.parallaxLayer2D.repeatX ? 1 : 0) << "\n"; + file << "parallax2dRepeatY=" << (obj.parallaxLayer2D.repeatY ? 1 : 0) << "\n"; + file << "parallax2dSpacing=" << obj.parallaxLayer2D.repeatSpacing.x << "," << obj.parallaxLayer2D.repeatSpacing.y << "\n"; + } + file << "hasCameraFollow2D=" << (obj.hasCameraFollow2D ? 1 : 0) << "\n"; + if (obj.hasCameraFollow2D) { + file << "cameraFollow2dEnabled=" << (obj.cameraFollow2D.enabled ? 1 : 0) << "\n"; + file << "cameraFollow2dTarget=" << obj.cameraFollow2D.targetId << "\n"; + file << "cameraFollow2dOffset=" << obj.cameraFollow2D.offset.x << "," << obj.cameraFollow2D.offset.y << "\n"; + file << "cameraFollow2dSmoothTime=" << obj.cameraFollow2D.smoothTime << "\n"; + } file << "hasCollider=" << (obj.hasCollider ? 1 : 0) << "\n"; if (obj.hasCollider) { file << "colliderEnabled=" << (obj.collider.enabled ? 1 : 0) << "\n"; @@ -362,6 +406,67 @@ bool SceneSerializer::saveScene(const fs::path& filePath, file << "audioSpatial=" << (obj.audioSource.spatial ? 1 : 0) << "\n"; file << "audioMinDistance=" << obj.audioSource.minDistance << "\n"; file << "audioMaxDistance=" << obj.audioSource.maxDistance << "\n"; + file << "audioRolloffMode=" << static_cast(obj.audioSource.rolloffMode) << "\n"; + file << "audioRolloff=" << obj.audioSource.rolloff << "\n"; + file << "audioCustomMidDistance=" << obj.audioSource.customMidDistance << "\n"; + file << "audioCustomMidGain=" << obj.audioSource.customMidGain << "\n"; + file << "audioCustomEndGain=" << obj.audioSource.customEndGain << "\n"; + } + file << "hasReverbZone=" << (obj.hasReverbZone ? 1 : 0) << "\n"; + if (obj.hasReverbZone) { + file << "reverbEnabled=" << (obj.reverbZone.enabled ? 1 : 0) << "\n"; + file << "reverbPreset=" << static_cast(obj.reverbZone.preset) << "\n"; + file << "reverbShape=" << static_cast(obj.reverbZone.shape) << "\n"; + file << "reverbBox=" << obj.reverbZone.boxSize.x << "," << obj.reverbZone.boxSize.y << "," << obj.reverbZone.boxSize.z << "\n"; + file << "reverbRadius=" << obj.reverbZone.radius << "\n"; + file << "reverbBlend=" << obj.reverbZone.blendDistance << "\n"; + file << "reverbMinDistance=" << obj.reverbZone.minDistance << "\n"; + file << "reverbMaxDistance=" << obj.reverbZone.maxDistance << "\n"; + file << "reverbRoom=" << obj.reverbZone.room << "\n"; + file << "reverbRoomHF=" << obj.reverbZone.roomHF << "\n"; + file << "reverbRoomLF=" << obj.reverbZone.roomLF << "\n"; + file << "reverbDecayTime=" << obj.reverbZone.decayTime << "\n"; + file << "reverbDecayHFRatio=" << obj.reverbZone.decayHFRatio << "\n"; + file << "reverbReflections=" << obj.reverbZone.reflections << "\n"; + file << "reverbReflectionsDelay=" << obj.reverbZone.reflectionsDelay << "\n"; + file << "reverbReverb=" << obj.reverbZone.reverb << "\n"; + file << "reverbReverbDelay=" << obj.reverbZone.reverbDelay << "\n"; + file << "reverbHFReference=" << obj.reverbZone.hfReference << "\n"; + file << "reverbLFReference=" << obj.reverbZone.lfReference << "\n"; + file << "reverbRoomRolloffFactor=" << obj.reverbZone.roomRolloffFactor << "\n"; + file << "reverbDiffusion=" << obj.reverbZone.diffusion << "\n"; + file << "reverbDensity=" << obj.reverbZone.density << "\n"; + } + file << "hasAnimation=" << (obj.hasAnimation ? 1 : 0) << "\n"; + if (obj.hasAnimation) { + file << "animEnabled=" << (obj.animation.enabled ? 1 : 0) << "\n"; + file << "animClipLength=" << obj.animation.clipLength << "\n"; + file << "animPlaySpeed=" << obj.animation.playSpeed << "\n"; + file << "animLoop=" << (obj.animation.loop ? 1 : 0) << "\n"; + file << "animApplyOnScrub=" << (obj.animation.applyOnScrub ? 1 : 0) << "\n"; + file << "animKeyCount=" << obj.animation.keyframes.size() << "\n"; + for (size_t ki = 0; ki < obj.animation.keyframes.size(); ++ki) { + const auto& key = obj.animation.keyframes[ki]; + file << "animKey" << ki << "_time=" << key.time << "\n"; + file << "animKey" << ki << "_pos=" << key.position.x << "," << key.position.y << "," << key.position.z << "\n"; + file << "animKey" << ki << "_rot=" << key.rotation.x << "," << key.rotation.y << "," << key.rotation.z << "\n"; + file << "animKey" << ki << "_scale=" << key.scale.x << "," << key.scale.y << "," << key.scale.z << "\n"; + file << "animKey" << ki << "_interp=" << static_cast(key.interpolation) << "\n"; + file << "animKey" << ki << "_curve=" << static_cast(key.curveMode) << "\n"; + file << "animKey" << ki << "_in=" << key.bezierIn.x << "," << key.bezierIn.y << "\n"; + file << "animKey" << ki << "_out=" << key.bezierOut.x << "," << key.bezierOut.y << "\n"; + } + } + file << "hasSkeletalAnimation=" << (obj.hasSkeletalAnimation ? 1 : 0) << "\n"; + if (obj.hasSkeletalAnimation) { + file << "skelEnabled=" << (obj.skeletal.enabled ? 1 : 0) << "\n"; + file << "skelUseGpu=" << (obj.skeletal.useGpuSkinning ? 1 : 0) << "\n"; + file << "skelAllowCpuFallback=" << (obj.skeletal.allowCpuFallback ? 1 : 0) << "\n"; + file << "skelUseAnimation=" << (obj.skeletal.useAnimation ? 1 : 0) << "\n"; + file << "skelClipIndex=" << obj.skeletal.clipIndex << "\n"; + file << "skelPlaySpeed=" << obj.skeletal.playSpeed << "\n"; + file << "skelLoop=" << (obj.skeletal.loop ? 1 : 0) << "\n"; + file << "skelMaxBones=" << obj.skeletal.maxBones << "\n"; } file << "materialColor=" << obj.material.color.r << "," << obj.material.color.g << "," << obj.material.color.b << "\n"; file << "materialAmbient=" << obj.material.ambientStrength << "\n"; @@ -383,6 +488,8 @@ bool SceneSerializer::saveScene(const fs::path& filePath, for (size_t si = 0; si < obj.scripts.size(); ++si) { const auto& sc = obj.scripts[si]; file << "script" << si << "_path=" << sc.path << "\n"; + file << "script" << si << "_lang=" << static_cast(sc.language) << "\n"; + file << "script" << si << "_type=" << sc.managedType << "\n"; file << "script" << si << "_enabled=" << (sc.enabled ? 1 : 0) << "\n"; file << "script" << si << "_settings=" << sc.settings.size() << "\n"; for (size_t k = 0; k < sc.settings.size(); ++k) { @@ -390,6 +497,9 @@ bool SceneSerializer::saveScene(const fs::path& filePath, } } file << "lightColor=" << obj.light.color.r << "," << obj.light.color.g << "," << obj.light.color.b << "\n"; + if (obj.hasLight) { + file << "lightType=" << static_cast(obj.light.type) << "\n"; + } file << "lightIntensity=" << obj.light.intensity << "\n"; file << "lightRange=" << obj.light.range << "\n"; file << "lightEdgeFade=" << obj.light.edgeFade << "\n"; @@ -402,8 +512,11 @@ bool SceneSerializer::saveScene(const fs::path& filePath, file << "cameraNear=" << obj.camera.nearClip << "\n"; file << "cameraFar=" << obj.camera.farClip << "\n"; file << "cameraPostFX=" << (obj.camera.applyPostFX ? 1 : 0) << "\n"; + file << "cameraUse2D=" << (obj.camera.use2D ? 1 : 0) << "\n"; + file << "cameraPixelsPerUnit=" << obj.camera.pixelsPerUnit << "\n"; file << "uiAnchor=" << static_cast(obj.ui.anchor) << "\n"; file << "uiPosition=" << obj.ui.position.x << "," << obj.ui.position.y << "\n"; + file << "uiRotation=" << obj.ui.rotation << "\n"; file << "uiSize=" << obj.ui.size.x << "," << obj.ui.size.y << "\n"; file << "uiSliderValue=" << obj.ui.sliderValue << "\n"; file << "uiSliderMin=" << obj.ui.sliderMin << "\n"; @@ -415,7 +528,7 @@ bool SceneSerializer::saveScene(const fs::path& filePath, file << "uiButtonStyle=" << static_cast(obj.ui.buttonStyle) << "\n"; file << "uiStylePreset=" << obj.ui.stylePreset << "\n"; file << "uiTextScale=" << obj.ui.textScale << "\n"; - if (obj.type == ObjectType::PostFXNode) { + if (obj.hasPostFX) { file << "postEnabled=" << (obj.postFx.enabled ? 1 : 0) << "\n"; file << "postBloomEnabled=" << (obj.postFx.bloomEnabled ? 1 : 0) << "\n"; file << "postBloomThreshold=" << obj.postFx.bloomThreshold << "\n"; @@ -442,6 +555,8 @@ bool SceneSerializer::saveScene(const fs::path& filePath, for (size_t s = 0; s < obj.scripts.size(); ++s) { const auto& sc = obj.scripts[s]; file << "script" << s << "_path=" << sc.path << "\n"; + file << "script" << s << "_lang=" << static_cast(sc.language) << "\n"; + file << "script" << s << "_type=" << sc.managedType << "\n"; file << "script" << s << "_enabled=" << (sc.enabled ? 1 : 0) << "\n"; file << "script" << s << "_settingCount=" << sc.settings.size() << "\n"; for (size_t si = 0; si < sc.settings.size(); ++si) { @@ -449,8 +564,13 @@ bool SceneSerializer::saveScene(const fs::path& filePath, } } - if ((obj.type == ObjectType::OBJMesh || obj.type == ObjectType::Model) && !obj.meshPath.empty()) { + if (obj.hasRenderer && + (obj.renderType == RenderType::OBJMesh || obj.renderType == RenderType::Model) && + !obj.meshPath.empty()) { file << "meshPath=" << obj.meshPath << "\n"; + if (obj.renderType == RenderType::Model && obj.meshSourceIndex >= 0) { + file << "meshSourceIndex=" << obj.meshSourceIndex << "\n"; + } } file << "children="; @@ -469,10 +589,494 @@ bool SceneSerializer::saveScene(const fs::path& filePath, } } +namespace { +template +void ParseVec2(const std::string& value, Vec2T& out) { + sscanf(value.c_str(), "%f,%f", &out.x, &out.y); +} + +void ParseVec2List(const std::string& value, std::vector& out) { + out.clear(); + std::stringstream ss(value); + std::string item; + while (std::getline(ss, item, ';')) { + if (item.empty()) continue; + glm::vec2 v(0.0f); + ParseVec2(item, v); + out.push_back(v); + } +} + +template +void ParseVec3(const std::string& value, Vec3T& out) { + sscanf(value.c_str(), "%f,%f,%f", &out.x, &out.y, &out.z); +} + +template +void ParseVec4(const std::string& value, Vec4T& out) { + sscanf(value.c_str(), "%f,%f,%f,%f", &out.x, &out.y, &out.z, &out.w); +} + +bool g_deferSceneAssetLoading = false; + +bool IsDefaultTransform(const SceneObject& obj) { + auto nearZero = [](float v) { return std::abs(v) < 1e-4f; }; + auto nearOne = [](float v) { return std::abs(v - 1.0f) < 1e-4f; }; + return nearZero(obj.localPosition.x) && + nearZero(obj.localPosition.y) && + nearZero(obj.localPosition.z) && + nearZero(obj.localRotation.x) && + nearZero(obj.localRotation.y) && + nearZero(obj.localRotation.z) && + nearOne(obj.localScale.x) && + nearOne(obj.localScale.y) && + nearOne(obj.localScale.z); +} + +void ApplyModelRootTransform(SceneObject& obj, const ModelSceneData& sceneData) { + if (sceneData.nodes.empty()) return; + if (obj.localInitialized && !IsDefaultTransform(obj)) return; + const auto& root = sceneData.nodes.front(); + obj.localPosition = root.localPosition; + obj.localRotation = root.localRotation; + obj.localScale = root.localScale; + obj.localInitialized = true; + obj.position = obj.localPosition; + obj.rotation = obj.localRotation; + obj.scale = obj.localScale; +} + +void ApplyLegacyTypePreset(SceneObject& obj, ObjectType legacyType) { + obj.type = legacyType; + switch (legacyType) { + case ObjectType::Cube: + obj.hasRenderer = true; + obj.renderType = RenderType::Cube; + break; + case ObjectType::Sphere: + obj.hasRenderer = true; + obj.renderType = RenderType::Sphere; + break; + case ObjectType::Capsule: + obj.hasRenderer = true; + obj.renderType = RenderType::Capsule; + break; + case ObjectType::OBJMesh: + obj.hasRenderer = true; + obj.renderType = RenderType::OBJMesh; + break; + case ObjectType::Model: + obj.hasRenderer = true; + obj.renderType = RenderType::Model; + break; + case ObjectType::Mirror: + obj.hasRenderer = true; + obj.renderType = RenderType::Mirror; + break; + case ObjectType::Plane: + obj.hasRenderer = true; + obj.renderType = RenderType::Plane; + break; + case ObjectType::Torus: + obj.hasRenderer = true; + obj.renderType = RenderType::Torus; + break; + case ObjectType::Sprite: + obj.hasRenderer = true; + obj.renderType = RenderType::Sprite; + break; + case ObjectType::DirectionalLight: + obj.hasLight = true; + obj.light.type = LightType::Directional; + break; + case ObjectType::PointLight: + obj.hasLight = true; + obj.light.type = LightType::Point; + break; + case ObjectType::SpotLight: + obj.hasLight = true; + obj.light.type = LightType::Spot; + break; + case ObjectType::AreaLight: + obj.hasLight = true; + obj.light.type = LightType::Area; + break; + case ObjectType::Camera: + obj.hasCamera = true; + obj.camera.type = SceneCameraType::Scene; + break; + case ObjectType::PostFXNode: + obj.hasPostFX = true; + break; + case ObjectType::Canvas: + obj.hasUI = true; + obj.ui.type = UIElementType::Canvas; + break; + case ObjectType::UIImage: + obj.hasUI = true; + obj.ui.type = UIElementType::Image; + break; + case ObjectType::UISlider: + obj.hasUI = true; + obj.ui.type = UIElementType::Slider; + break; + case ObjectType::UIButton: + obj.hasUI = true; + obj.ui.type = UIElementType::Button; + break; + case ObjectType::UIText: + obj.hasUI = true; + obj.ui.type = UIElementType::Text; + break; + case ObjectType::Sprite2D: + obj.hasUI = true; + obj.ui.type = UIElementType::Sprite2D; + break; + case ObjectType::Empty: + default: + break; + } +} + +using KeyHandler = void (*)(SceneObject&, const std::string&); + +const std::unordered_map& GetSceneObjectKeyHandlers() { + static const std::unordered_map handlers = { + {"id", +[](SceneObject& obj, const std::string& value) { obj.id = std::stoi(value); }}, + {"name", +[](SceneObject& obj, const std::string& value) { obj.name = value; }}, + {"type", +[](SceneObject& obj, const std::string& value) { + ApplyLegacyTypePreset(obj, static_cast(std::stoi(value))); + }}, + {"enabled", +[](SceneObject& obj, const std::string& value) { obj.enabled = (std::stoi(value) != 0); }}, + {"layer", +[](SceneObject& obj, const std::string& value) { obj.layer = std::stoi(value); }}, + {"tag", +[](SceneObject& obj, const std::string& value) { obj.tag = value; }}, + {"hasRenderer", +[](SceneObject& obj, const std::string& value) { obj.hasRenderer = std::stoi(value) != 0; }}, + {"renderType", +[](SceneObject& obj, const std::string& value) { + obj.renderType = static_cast(std::stoi(value)); + if (obj.renderType != RenderType::None) { + obj.hasRenderer = true; + } + }}, + {"hasLight", +[](SceneObject& obj, const std::string& value) { obj.hasLight = std::stoi(value) != 0; }}, + {"hasCamera", +[](SceneObject& obj, const std::string& value) { obj.hasCamera = std::stoi(value) != 0; }}, + {"hasPostFX", +[](SceneObject& obj, const std::string& value) { obj.hasPostFX = std::stoi(value) != 0; }}, + {"hasUI", +[](SceneObject& obj, const std::string& value) { obj.hasUI = std::stoi(value) != 0; }}, + {"uiType", +[](SceneObject& obj, const std::string& value) { + obj.ui.type = static_cast(std::stoi(value)); + if (obj.ui.type != UIElementType::None) { + obj.hasUI = true; + } + }}, + {"parentId", +[](SceneObject& obj, const std::string& value) { obj.parentId = std::stoi(value); }}, + {"position", +[](SceneObject& obj, const std::string& value) { + ParseVec3(value, obj.position); + obj.localPosition = obj.position; + obj.localInitialized = true; + }}, + {"rotation", +[](SceneObject& obj, const std::string& value) { + ParseVec3(value, obj.rotation); + obj.rotation = NormalizeEulerDegrees(obj.rotation); + obj.localRotation = obj.rotation; + obj.localInitialized = true; + }}, + {"scale", +[](SceneObject& obj, const std::string& value) { + ParseVec3(value, obj.scale); + obj.localScale = obj.scale; + obj.localInitialized = true; + }}, + {"hasRigidbody", +[](SceneObject& obj, const std::string& value) { obj.hasRigidbody = std::stoi(value) != 0; }}, + {"rbEnabled", +[](SceneObject& obj, const std::string& value) { obj.rigidbody.enabled = std::stoi(value) != 0; }}, + {"rbMass", +[](SceneObject& obj, const std::string& value) { obj.rigidbody.mass = std::stof(value); }}, + {"rbUseGravity", +[](SceneObject& obj, const std::string& value) { obj.rigidbody.useGravity = std::stoi(value) != 0; }}, + {"rbKinematic", +[](SceneObject& obj, const std::string& value) { obj.rigidbody.isKinematic = std::stoi(value) != 0; }}, + {"rbLinearDamping", +[](SceneObject& obj, const std::string& value) { obj.rigidbody.linearDamping = std::stof(value); }}, + {"rbAngularDamping", +[](SceneObject& obj, const std::string& value) { obj.rigidbody.angularDamping = std::stof(value); }}, + {"rbLockRotX", +[](SceneObject& obj, const std::string& value) { obj.rigidbody.lockRotationX = std::stoi(value) != 0; }}, + {"rbLockRotY", +[](SceneObject& obj, const std::string& value) { obj.rigidbody.lockRotationY = std::stoi(value) != 0; }}, + {"rbLockRotZ", +[](SceneObject& obj, const std::string& value) { obj.rigidbody.lockRotationZ = std::stoi(value) != 0; }}, + {"hasRigidbody2D", +[](SceneObject& obj, const std::string& value) { obj.hasRigidbody2D = std::stoi(value) != 0; }}, + {"rb2dEnabled", +[](SceneObject& obj, const std::string& value) { obj.rigidbody2D.enabled = std::stoi(value) != 0; }}, + {"rb2dUseGravity", +[](SceneObject& obj, const std::string& value) { obj.rigidbody2D.useGravity = std::stoi(value) != 0; }}, + {"rb2dGravityScale", +[](SceneObject& obj, const std::string& value) { obj.rigidbody2D.gravityScale = std::stof(value); }}, + {"rb2dLinearDamping", +[](SceneObject& obj, const std::string& value) { obj.rigidbody2D.linearDamping = std::stof(value); }}, + {"rb2dVelocity", +[](SceneObject& obj, const std::string& value) { ParseVec2(value, obj.rigidbody2D.velocity); }}, + {"hasCollider2D", +[](SceneObject& obj, const std::string& value) { obj.hasCollider2D = std::stoi(value) != 0; }}, + {"collider2dEnabled", +[](SceneObject& obj, const std::string& value) { obj.collider2D.enabled = std::stoi(value) != 0; }}, + {"collider2dType", +[](SceneObject& obj, const std::string& value) { obj.collider2D.type = static_cast(std::stoi(value)); }}, + {"collider2dBox", +[](SceneObject& obj, const std::string& value) { ParseVec2(value, obj.collider2D.boxSize); }}, + {"collider2dClosed", +[](SceneObject& obj, const std::string& value) { obj.collider2D.closed = std::stoi(value) != 0; }}, + {"collider2dEdgeThickness", +[](SceneObject& obj, const std::string& value) { obj.collider2D.edgeThickness = std::stof(value); }}, + {"collider2dPoints", +[](SceneObject& obj, const std::string& value) { ParseVec2List(value, obj.collider2D.points); }}, + {"hasParallaxLayer2D", +[](SceneObject& obj, const std::string& value) { obj.hasParallaxLayer2D = std::stoi(value) != 0; }}, + {"parallax2dEnabled", +[](SceneObject& obj, const std::string& value) { obj.parallaxLayer2D.enabled = std::stoi(value) != 0; }}, + {"parallax2dOrder", +[](SceneObject& obj, const std::string& value) { obj.parallaxLayer2D.order = std::stoi(value); }}, + {"parallax2dFactor", +[](SceneObject& obj, const std::string& value) { obj.parallaxLayer2D.factor = std::stof(value); }}, + {"parallax2dRepeatX", +[](SceneObject& obj, const std::string& value) { obj.parallaxLayer2D.repeatX = std::stoi(value) != 0; }}, + {"parallax2dRepeatY", +[](SceneObject& obj, const std::string& value) { obj.parallaxLayer2D.repeatY = std::stoi(value) != 0; }}, + {"parallax2dSpacing", +[](SceneObject& obj, const std::string& value) { ParseVec2(value, obj.parallaxLayer2D.repeatSpacing); }}, + {"hasCameraFollow2D", +[](SceneObject& obj, const std::string& value) { obj.hasCameraFollow2D = std::stoi(value) != 0; }}, + {"cameraFollow2dEnabled", +[](SceneObject& obj, const std::string& value) { obj.cameraFollow2D.enabled = std::stoi(value) != 0; }}, + {"cameraFollow2dTarget", +[](SceneObject& obj, const std::string& value) { obj.cameraFollow2D.targetId = std::stoi(value); }}, + {"cameraFollow2dOffset", +[](SceneObject& obj, const std::string& value) { ParseVec2(value, obj.cameraFollow2D.offset); }}, + {"cameraFollow2dSmoothTime", +[](SceneObject& obj, const std::string& value) { obj.cameraFollow2D.smoothTime = std::stof(value); }}, + {"hasCollider", +[](SceneObject& obj, const std::string& value) { obj.hasCollider = std::stoi(value) != 0; }}, + {"colliderEnabled", +[](SceneObject& obj, const std::string& value) { obj.collider.enabled = std::stoi(value) != 0; }}, + {"colliderType", +[](SceneObject& obj, const std::string& value) { obj.collider.type = static_cast(std::stoi(value)); }}, + {"colliderBox", +[](SceneObject& obj, const std::string& value) { ParseVec3(value, obj.collider.boxSize); }}, + {"colliderConvex", +[](SceneObject& obj, const std::string& value) { obj.collider.convex = std::stoi(value) != 0; }}, + {"hasPlayerController", +[](SceneObject& obj, const std::string& value) { obj.hasPlayerController = std::stoi(value) != 0; }}, + {"pcEnabled", +[](SceneObject& obj, const std::string& value) { obj.playerController.enabled = std::stoi(value) != 0; }}, + {"pcMoveSpeed", +[](SceneObject& obj, const std::string& value) { obj.playerController.moveSpeed = std::stof(value); }}, + {"pcLookSensitivity", +[](SceneObject& obj, const std::string& value) { obj.playerController.lookSensitivity = std::stof(value); }}, + {"pcHeight", +[](SceneObject& obj, const std::string& value) { obj.playerController.height = std::stof(value); }}, + {"pcRadius", +[](SceneObject& obj, const std::string& value) { obj.playerController.radius = std::stof(value); }}, + {"pcJumpStrength", +[](SceneObject& obj, const std::string& value) { obj.playerController.jumpStrength = std::stof(value); }}, + {"hasAudioSource", +[](SceneObject& obj, const std::string& value) { obj.hasAudioSource = std::stoi(value) != 0; }}, + {"audioEnabled", +[](SceneObject& obj, const std::string& value) { obj.audioSource.enabled = std::stoi(value) != 0; }}, + {"audioClip", +[](SceneObject& obj, const std::string& value) { obj.audioSource.clipPath = value; }}, + {"audioVolume", +[](SceneObject& obj, const std::string& value) { obj.audioSource.volume = std::stof(value); }}, + {"audioLoop", +[](SceneObject& obj, const std::string& value) { obj.audioSource.loop = std::stoi(value) != 0; }}, + {"audioPlayOnStart", +[](SceneObject& obj, const std::string& value) { obj.audioSource.playOnStart = std::stoi(value) != 0; }}, + {"audioSpatial", +[](SceneObject& obj, const std::string& value) { obj.audioSource.spatial = std::stoi(value) != 0; }}, + {"audioMinDistance", +[](SceneObject& obj, const std::string& value) { obj.audioSource.minDistance = std::stof(value); }}, + {"audioMaxDistance", +[](SceneObject& obj, const std::string& value) { obj.audioSource.maxDistance = std::stof(value); }}, + {"audioRolloffMode", +[](SceneObject& obj, const std::string& value) { obj.audioSource.rolloffMode = static_cast(std::stoi(value)); }}, + {"audioRolloff", +[](SceneObject& obj, const std::string& value) { obj.audioSource.rolloff = std::stof(value); }}, + {"audioCustomMidDistance", +[](SceneObject& obj, const std::string& value) { obj.audioSource.customMidDistance = std::stof(value); }}, + {"audioCustomMidGain", +[](SceneObject& obj, const std::string& value) { obj.audioSource.customMidGain = std::stof(value); }}, + {"audioCustomEndGain", +[](SceneObject& obj, const std::string& value) { obj.audioSource.customEndGain = std::stof(value); }}, + {"hasReverbZone", +[](SceneObject& obj, const std::string& value) { obj.hasReverbZone = std::stoi(value) != 0; }}, + {"reverbEnabled", +[](SceneObject& obj, const std::string& value) { obj.reverbZone.enabled = std::stoi(value) != 0; }}, + {"reverbPreset", +[](SceneObject& obj, const std::string& value) { obj.reverbZone.preset = static_cast(std::stoi(value)); }}, + {"reverbShape", +[](SceneObject& obj, const std::string& value) { obj.reverbZone.shape = static_cast(std::stoi(value)); }}, + {"reverbBox", +[](SceneObject& obj, const std::string& value) { ParseVec3(value, obj.reverbZone.boxSize); }}, + {"reverbRadius", +[](SceneObject& obj, const std::string& value) { obj.reverbZone.radius = std::stof(value); }}, + {"reverbBlend", +[](SceneObject& obj, const std::string& value) { obj.reverbZone.blendDistance = std::stof(value); }}, + {"reverbMinDistance", +[](SceneObject& obj, const std::string& value) { obj.reverbZone.minDistance = std::stof(value); }}, + {"reverbMaxDistance", +[](SceneObject& obj, const std::string& value) { obj.reverbZone.maxDistance = std::stof(value); }}, + {"reverbRoom", +[](SceneObject& obj, const std::string& value) { obj.reverbZone.room = std::stof(value); }}, + {"reverbRoomHF", +[](SceneObject& obj, const std::string& value) { obj.reverbZone.roomHF = std::stof(value); }}, + {"reverbRoomLF", +[](SceneObject& obj, const std::string& value) { obj.reverbZone.roomLF = std::stof(value); }}, + {"reverbDecayTime", +[](SceneObject& obj, const std::string& value) { obj.reverbZone.decayTime = std::stof(value); }}, + {"reverbDecayHFRatio", +[](SceneObject& obj, const std::string& value) { obj.reverbZone.decayHFRatio = std::stof(value); }}, + {"reverbReflections", +[](SceneObject& obj, const std::string& value) { obj.reverbZone.reflections = std::stof(value); }}, + {"reverbReflectionsDelay", +[](SceneObject& obj, const std::string& value) { obj.reverbZone.reflectionsDelay = std::stof(value); }}, + {"reverbReverb", +[](SceneObject& obj, const std::string& value) { obj.reverbZone.reverb = std::stof(value); }}, + {"reverbReverbDelay", +[](SceneObject& obj, const std::string& value) { obj.reverbZone.reverbDelay = std::stof(value); }}, + {"reverbHFReference", +[](SceneObject& obj, const std::string& value) { obj.reverbZone.hfReference = std::stof(value); }}, + {"reverbLFReference", +[](SceneObject& obj, const std::string& value) { obj.reverbZone.lfReference = std::stof(value); }}, + {"reverbRoomRolloffFactor", +[](SceneObject& obj, const std::string& value) { obj.reverbZone.roomRolloffFactor = std::stof(value); }}, + {"reverbDiffusion", +[](SceneObject& obj, const std::string& value) { obj.reverbZone.diffusion = std::stof(value); }}, + {"reverbDensity", +[](SceneObject& obj, const std::string& value) { obj.reverbZone.density = std::stof(value); }}, + {"hasAnimation", +[](SceneObject& obj, const std::string& value) { obj.hasAnimation = std::stoi(value) != 0; }}, + {"animEnabled", +[](SceneObject& obj, const std::string& value) { obj.animation.enabled = std::stoi(value) != 0; }}, + {"animClipLength", +[](SceneObject& obj, const std::string& value) { obj.animation.clipLength = std::stof(value); }}, + {"animPlaySpeed", +[](SceneObject& obj, const std::string& value) { obj.animation.playSpeed = std::stof(value); }}, + {"animLoop", +[](SceneObject& obj, const std::string& value) { obj.animation.loop = std::stoi(value) != 0; }}, + {"animApplyOnScrub", +[](SceneObject& obj, const std::string& value) { obj.animation.applyOnScrub = std::stoi(value) != 0; }}, + {"animKeyCount", +[](SceneObject& obj, const std::string& value) { + int count = std::stoi(value); + obj.animation.keyframes.resize(std::max(0, count)); + }}, + {"hasSkeletalAnimation", +[](SceneObject& obj, const std::string& value) { obj.hasSkeletalAnimation = std::stoi(value) != 0; }}, + {"skelEnabled", +[](SceneObject& obj, const std::string& value) { obj.skeletal.enabled = std::stoi(value) != 0; }}, + {"skelUseGpu", +[](SceneObject& obj, const std::string& value) { obj.skeletal.useGpuSkinning = std::stoi(value) != 0; }}, + {"skelAllowCpuFallback", +[](SceneObject& obj, const std::string& value) { obj.skeletal.allowCpuFallback = std::stoi(value) != 0; }}, + {"skelUseAnimation", +[](SceneObject& obj, const std::string& value) { obj.skeletal.useAnimation = std::stoi(value) != 0; }}, + {"skelClipIndex", +[](SceneObject& obj, const std::string& value) { obj.skeletal.clipIndex = std::stoi(value); }}, + {"skelPlaySpeed", +[](SceneObject& obj, const std::string& value) { obj.skeletal.playSpeed = std::stof(value); }}, + {"skelLoop", +[](SceneObject& obj, const std::string& value) { obj.skeletal.loop = std::stoi(value) != 0; }}, + {"skelMaxBones", +[](SceneObject& obj, const std::string& value) { obj.skeletal.maxBones = std::stoi(value); }}, + {"materialColor", +[](SceneObject& obj, const std::string& value) { ParseVec3(value, obj.material.color); }}, + {"materialAmbient", +[](SceneObject& obj, const std::string& value) { obj.material.ambientStrength = std::stof(value); }}, + {"materialSpecular", +[](SceneObject& obj, const std::string& value) { obj.material.specularStrength = std::stof(value); }}, + {"materialShininess", +[](SceneObject& obj, const std::string& value) { obj.material.shininess = std::stof(value); }}, + {"materialTextureMix", +[](SceneObject& obj, const std::string& value) { obj.material.textureMix = std::stof(value); }}, + {"materialPath", +[](SceneObject& obj, const std::string& value) { obj.materialPath = value; }}, + {"albedoTex", +[](SceneObject& obj, const std::string& value) { obj.albedoTexturePath = value; }}, + {"overlayTex", +[](SceneObject& obj, const std::string& value) { obj.overlayTexturePath = value; }}, + {"normalMap", +[](SceneObject& obj, const std::string& value) { obj.normalMapPath = value; }}, + {"vertexShader", +[](SceneObject& obj, const std::string& value) { obj.vertexShaderPath = value; }}, + {"fragmentShader", +[](SceneObject& obj, const std::string& value) { obj.fragmentShaderPath = value; }}, + {"useOverlay", +[](SceneObject& obj, const std::string& value) { obj.useOverlay = (std::stoi(value) != 0); }}, + {"additionalMaterialCount", +[](SceneObject& obj, const std::string& value) { + int count = std::stoi(value); + obj.additionalMaterialPaths.resize(std::max(0, count)); + }}, + {"scripts", +[](SceneObject& obj, const std::string& value) { + int count = std::stoi(value); + obj.scripts.resize(std::max(0, count)); + }}, + {"scriptCount", +[](SceneObject& obj, const std::string& value) { + int count = std::stoi(value); + obj.scripts.resize(std::max(0, count)); + }}, + {"lightColor", +[](SceneObject& obj, const std::string& value) { ParseVec3(value, obj.light.color); }}, + {"lightType", +[](SceneObject& obj, const std::string& value) { + obj.light.type = static_cast(std::stoi(value)); + }}, + {"lightIntensity", +[](SceneObject& obj, const std::string& value) { obj.light.intensity = std::stof(value); }}, + {"lightRange", +[](SceneObject& obj, const std::string& value) { obj.light.range = std::stof(value); }}, + {"lightEdgeFade", +[](SceneObject& obj, const std::string& value) { obj.light.edgeFade = std::stof(value); }}, + {"lightInner", +[](SceneObject& obj, const std::string& value) { obj.light.innerAngle = std::stof(value); }}, + {"lightOuter", +[](SceneObject& obj, const std::string& value) { obj.light.outerAngle = std::stof(value); }}, + {"lightSize", +[](SceneObject& obj, const std::string& value) { ParseVec2(value, obj.light.size); }}, + {"lightEnabled", +[](SceneObject& obj, const std::string& value) { obj.light.enabled = (std::stoi(value) != 0); }}, + {"cameraType", +[](SceneObject& obj, const std::string& value) { obj.camera.type = static_cast(std::stoi(value)); }}, + {"cameraFov", +[](SceneObject& obj, const std::string& value) { obj.camera.fov = std::stof(value); }}, + {"cameraNear", +[](SceneObject& obj, const std::string& value) { obj.camera.nearClip = std::stof(value); }}, + {"cameraFar", +[](SceneObject& obj, const std::string& value) { obj.camera.farClip = std::stof(value); }}, + {"cameraPostFX", +[](SceneObject& obj, const std::string& value) { obj.camera.applyPostFX = (std::stoi(value) != 0); }}, + {"cameraUse2D", +[](SceneObject& obj, const std::string& value) { obj.camera.use2D = (std::stoi(value) != 0); }}, + {"cameraPixelsPerUnit", +[](SceneObject& obj, const std::string& value) { obj.camera.pixelsPerUnit = std::stof(value); }}, + {"uiAnchor", +[](SceneObject& obj, const std::string& value) { obj.ui.anchor = static_cast(std::stoi(value)); }}, + {"uiPosition", +[](SceneObject& obj, const std::string& value) { ParseVec2(value, obj.ui.position); }}, + {"uiRotation", +[](SceneObject& obj, const std::string& value) { obj.ui.rotation = std::stof(value); }}, + {"uiSize", +[](SceneObject& obj, const std::string& value) { ParseVec2(value, obj.ui.size); }}, + {"uiSliderValue", +[](SceneObject& obj, const std::string& value) { obj.ui.sliderValue = std::stof(value); }}, + {"uiSliderMin", +[](SceneObject& obj, const std::string& value) { obj.ui.sliderMin = std::stof(value); }}, + {"uiSliderMax", +[](SceneObject& obj, const std::string& value) { obj.ui.sliderMax = std::stof(value); }}, + {"uiLabel", +[](SceneObject& obj, const std::string& value) { obj.ui.label = value; }}, + {"uiColor", +[](SceneObject& obj, const std::string& value) { ParseVec4(value, obj.ui.color); }}, + {"uiInteractable", +[](SceneObject& obj, const std::string& value) { obj.ui.interactable = (std::stoi(value) != 0); }}, + {"uiSliderStyle", +[](SceneObject& obj, const std::string& value) { obj.ui.sliderStyle = static_cast(std::stoi(value)); }}, + {"uiButtonStyle", +[](SceneObject& obj, const std::string& value) { obj.ui.buttonStyle = static_cast(std::stoi(value)); }}, + {"uiStylePreset", +[](SceneObject& obj, const std::string& value) { obj.ui.stylePreset = value; }}, + {"uiTextScale", +[](SceneObject& obj, const std::string& value) { obj.ui.textScale = std::stof(value); }}, + {"postEnabled", +[](SceneObject& obj, const std::string& value) { obj.postFx.enabled = (std::stoi(value) != 0); }}, + {"postBloomEnabled", +[](SceneObject& obj, const std::string& value) { obj.postFx.bloomEnabled = (std::stoi(value) != 0); }}, + {"postBloomThreshold", +[](SceneObject& obj, const std::string& value) { obj.postFx.bloomThreshold = std::stof(value); }}, + {"postBloomIntensity", +[](SceneObject& obj, const std::string& value) { obj.postFx.bloomIntensity = std::stof(value); }}, + {"postBloomRadius", +[](SceneObject& obj, const std::string& value) { obj.postFx.bloomRadius = std::stof(value); }}, + {"postColorAdjustEnabled", +[](SceneObject& obj, const std::string& value) { obj.postFx.colorAdjustEnabled = (std::stoi(value) != 0); }}, + {"postExposure", +[](SceneObject& obj, const std::string& value) { obj.postFx.exposure = std::stof(value); }}, + {"postContrast", +[](SceneObject& obj, const std::string& value) { obj.postFx.contrast = std::stof(value); }}, + {"postSaturation", +[](SceneObject& obj, const std::string& value) { obj.postFx.saturation = std::stof(value); }}, + {"postColorFilter", +[](SceneObject& obj, const std::string& value) { ParseVec3(value, obj.postFx.colorFilter); }}, + {"postMotionBlurEnabled", +[](SceneObject& obj, const std::string& value) { obj.postFx.motionBlurEnabled = (std::stoi(value) != 0); }}, + {"postMotionBlurStrength", +[](SceneObject& obj, const std::string& value) { obj.postFx.motionBlurStrength = std::stof(value); }}, + {"postVignetteEnabled", +[](SceneObject& obj, const std::string& value) { obj.postFx.vignetteEnabled = (std::stoi(value) != 0); }}, + {"postVignetteIntensity", +[](SceneObject& obj, const std::string& value) { obj.postFx.vignetteIntensity = std::stof(value); }}, + {"postVignetteSmoothness", +[](SceneObject& obj, const std::string& value) { obj.postFx.vignetteSmoothness = std::stof(value); }}, + {"postChromaticEnabled", +[](SceneObject& obj, const std::string& value) { obj.postFx.chromaticAberrationEnabled = (std::stoi(value) != 0); }}, + {"postChromaticAmount", +[](SceneObject& obj, const std::string& value) { obj.postFx.chromaticAmount = std::stof(value); }}, + {"postAOEnabled", +[](SceneObject& obj, const std::string& value) { obj.postFx.ambientOcclusionEnabled = (std::stoi(value) != 0); }}, + {"postAORadius", +[](SceneObject& obj, const std::string& value) { obj.postFx.aoRadius = std::stof(value); }}, + {"postAOStrength", +[](SceneObject& obj, const std::string& value) { obj.postFx.aoStrength = std::stof(value); }}, + {"meshPath", +[](SceneObject& obj, const std::string& value) { + obj.meshPath = value; + if (g_deferSceneAssetLoading) { + return; + } + if (!value.empty() && obj.hasRenderer && obj.renderType == RenderType::OBJMesh) { + std::string err; + obj.meshId = g_objLoader.loadOBJ(value, err); + } else if (!value.empty() && obj.hasRenderer && obj.renderType == RenderType::Model) { + ModelSceneData sceneData; + std::string err; + if (getModelLoader().loadModelScene(value, sceneData, err)) { + int sourceIndex = obj.meshSourceIndex; + if (sourceIndex < 0 || sourceIndex >= (int)sceneData.meshIndices.size()) { + sourceIndex = 0; + } + if (!sceneData.meshIndices.empty() && + sourceIndex >= 0 && sourceIndex < (int)sceneData.meshIndices.size()) { + obj.meshId = sceneData.meshIndices[sourceIndex]; + } + ApplyModelRootTransform(obj, sceneData); + } else { + std::cerr << "Failed to load model from scene: " << err << std::endl; + obj.meshId = -1; + } + } + }}, + {"meshSourceIndex", +[](SceneObject& obj, const std::string& value) { + obj.meshSourceIndex = std::stoi(value); + if (g_deferSceneAssetLoading) { + return; + } + if (!obj.meshPath.empty() && obj.hasRenderer && obj.renderType == RenderType::Model) { + ModelSceneData sceneData; + std::string err; + if (getModelLoader().loadModelScene(obj.meshPath, sceneData, err)) { + int sourceIndex = obj.meshSourceIndex; + if (sourceIndex < 0 || sourceIndex >= (int)sceneData.meshIndices.size()) { + sourceIndex = 0; + } + if (!sceneData.meshIndices.empty() && + sourceIndex >= 0 && sourceIndex < (int)sceneData.meshIndices.size()) { + obj.meshId = sceneData.meshIndices[sourceIndex]; + } + ApplyModelRootTransform(obj, sceneData); + } else { + std::cerr << "Failed to load model from scene: " << err << std::endl; + } + } + }}, + {"children", +[](SceneObject& obj, const std::string& value) { + if (!value.empty()) { + std::stringstream ss(value); + std::string item; + while (std::getline(ss, item, ',')) { + if (!item.empty()) { + obj.childIds.push_back(std::stoi(item)); + } + } + } + }}, + }; + return handlers; +} +} // namespace + +ObjectType GetLegacyTypeFromComponents(const SceneObject& obj) { + if (obj.hasRenderer) { + switch (obj.renderType) { + case RenderType::Cube: return ObjectType::Cube; + case RenderType::Sphere: return ObjectType::Sphere; + case RenderType::Capsule: return ObjectType::Capsule; + case RenderType::OBJMesh: return ObjectType::OBJMesh; + case RenderType::Model: return ObjectType::Model; + case RenderType::Mirror: return ObjectType::Mirror; + case RenderType::Plane: return ObjectType::Plane; + case RenderType::Torus: return ObjectType::Torus; + case RenderType::Sprite: return ObjectType::Sprite; + case RenderType::None: break; + } + } + if (obj.hasUI) { + switch (obj.ui.type) { + case UIElementType::Canvas: return ObjectType::Canvas; + case UIElementType::Image: return ObjectType::UIImage; + case UIElementType::Slider: return ObjectType::UISlider; + case UIElementType::Button: return ObjectType::UIButton; + case UIElementType::Text: return ObjectType::UIText; + case UIElementType::Sprite2D: return ObjectType::Sprite2D; + case UIElementType::None: break; + } + } + if (obj.hasLight) { + switch (obj.light.type) { + case LightType::Directional: return ObjectType::DirectionalLight; + case LightType::Point: return ObjectType::PointLight; + case LightType::Spot: return ObjectType::SpotLight; + case LightType::Area: return ObjectType::AreaLight; + } + } + if (obj.hasCamera) { + return ObjectType::Camera; + } + if (obj.hasPostFX) { + return ObjectType::PostFXNode; + } + return ObjectType::Empty; +} + bool SceneSerializer::loadScene(const fs::path& filePath, std::vector& objects, int& nextId, - int& outVersion) { + int& outVersion, + float* outTimeOfDay) { try { std::ifstream file(filePath); if (!file.is_open()) return false; @@ -481,6 +1085,7 @@ bool SceneSerializer::loadScene(const fs::path& filePath, std::string line; SceneObject* currentObj = nullptr; int sceneVersion = 9; + float sceneTimeOfDay = -1.0f; while (std::getline(file, line)) { size_t first = line.find_first_not_of(" \t\r\n"); @@ -498,7 +1103,7 @@ bool SceneSerializer::loadScene(const fs::path& filePath, if (line.empty() || line[0] == '#') continue; if (line == "[Object]") { - objects.push_back(SceneObject("", ObjectType::Cube, 0)); + objects.push_back(SceneObject("", ObjectType::Empty, 0)); currentObj = &objects.back(); continue; } @@ -513,167 +1118,57 @@ bool SceneSerializer::loadScene(const fs::path& filePath, sceneVersion = std::stoi(value); } else if (key == "nextId") { nextId = std::stoi(value); + } else if (key == "timeOfDay") { + sceneTimeOfDay = std::stof(value); } else if (currentObj) { - if (key == "id") { - currentObj->id = std::stoi(value); - } else if (key == "name") { - currentObj->name = value; - } else if (key == "type") { - currentObj->type = static_cast(std::stoi(value)); - if (currentObj->type == ObjectType::DirectionalLight) currentObj->light.type = LightType::Directional; - else if (currentObj->type == ObjectType::PointLight) currentObj->light.type = LightType::Point; - else if (currentObj->type == ObjectType::SpotLight) currentObj->light.type = LightType::Spot; - else if (currentObj->type == ObjectType::AreaLight) currentObj->light.type = LightType::Area; - else if (currentObj->type == ObjectType::Camera) { - currentObj->camera.type = SceneCameraType::Scene; + const auto& handlers = GetSceneObjectKeyHandlers(); + auto handlerIt = handlers.find(key); + if (handlerIt != handlers.end()) { + handlerIt->second(*currentObj, value); + } else if (key.rfind("animKey", 0) == 0) { + size_t underscore = key.find('_'); + if (underscore != std::string::npos && underscore > 7) { + int idx = std::stoi(key.substr(7, underscore - 7)); + if (idx >= 0 && idx < static_cast(currentObj->animation.keyframes.size())) { + std::string sub = key.substr(underscore + 1); + auto& keyframe = currentObj->animation.keyframes[idx]; + if (sub == "time") { + keyframe.time = std::stof(value); + } else if (sub == "pos") { + sscanf(value.c_str(), "%f,%f,%f", + &keyframe.position.x, + &keyframe.position.y, + &keyframe.position.z); + } else if (sub == "rot") { + sscanf(value.c_str(), "%f,%f,%f", + &keyframe.rotation.x, + &keyframe.rotation.y, + &keyframe.rotation.z); + } else if (sub == "scale") { + sscanf(value.c_str(), "%f,%f,%f", + &keyframe.scale.x, + &keyframe.scale.y, + &keyframe.scale.z); + } else if (sub == "interp") { + keyframe.interpolation = static_cast(std::stoi(value)); + } else if (sub == "curve") { + keyframe.curveMode = static_cast(std::stoi(value)); + } else if (sub == "in") { + sscanf(value.c_str(), "%f,%f", + &keyframe.bezierIn.x, + &keyframe.bezierIn.y); + } else if (sub == "out") { + sscanf(value.c_str(), "%f,%f", + &keyframe.bezierOut.x, + &keyframe.bezierOut.y); + } + } } - } else if (key == "enabled") { - currentObj->enabled = (std::stoi(value) != 0); - } else if (key == "layer") { - currentObj->layer = std::stoi(value); - } else if (key == "tag") { - currentObj->tag = value; - } else if (key == "parentId") { - currentObj->parentId = std::stoi(value); - } else if (key == "position") { - sscanf(value.c_str(), "%f,%f,%f", - ¤tObj->position.x, - ¤tObj->position.y, - ¤tObj->position.z); - currentObj->localPosition = currentObj->position; - currentObj->localInitialized = true; - } else if (key == "rotation") { - sscanf(value.c_str(), "%f,%f,%f", - ¤tObj->rotation.x, - ¤tObj->rotation.y, - ¤tObj->rotation.z); - currentObj->rotation = NormalizeEulerDegrees(currentObj->rotation); - currentObj->localRotation = currentObj->rotation; - currentObj->localInitialized = true; - } else if (key == "scale") { - sscanf(value.c_str(), "%f,%f,%f", - ¤tObj->scale.x, - ¤tObj->scale.y, - ¤tObj->scale.z); - currentObj->localScale = currentObj->scale; - currentObj->localInitialized = true; - } else if (key == "hasRigidbody") { - currentObj->hasRigidbody = std::stoi(value) != 0; - } else if (key == "rbEnabled") { - currentObj->rigidbody.enabled = std::stoi(value) != 0; - } else if (key == "rbMass") { - currentObj->rigidbody.mass = std::stof(value); - } else if (key == "rbUseGravity") { - currentObj->rigidbody.useGravity = std::stoi(value) != 0; - } else if (key == "rbKinematic") { - currentObj->rigidbody.isKinematic = std::stoi(value) != 0; - } else if (key == "rbLinearDamping") { - currentObj->rigidbody.linearDamping = std::stof(value); - } else if (key == "rbAngularDamping") { - currentObj->rigidbody.angularDamping = std::stof(value); - } else if (key == "rbLockRotX") { - currentObj->rigidbody.lockRotationX = std::stoi(value) != 0; - } else if (key == "rbLockRotY") { - currentObj->rigidbody.lockRotationY = std::stoi(value) != 0; - } else if (key == "rbLockRotZ") { - currentObj->rigidbody.lockRotationZ = std::stoi(value) != 0; - } else if (key == "hasRigidbody2D") { - currentObj->hasRigidbody2D = std::stoi(value) != 0; - } else if (key == "rb2dEnabled") { - currentObj->rigidbody2D.enabled = std::stoi(value) != 0; - } else if (key == "rb2dUseGravity") { - currentObj->rigidbody2D.useGravity = std::stoi(value) != 0; - } else if (key == "rb2dGravityScale") { - currentObj->rigidbody2D.gravityScale = std::stof(value); - } else if (key == "rb2dLinearDamping") { - currentObj->rigidbody2D.linearDamping = std::stof(value); - } else if (key == "rb2dVelocity") { - sscanf(value.c_str(), "%f,%f", - ¤tObj->rigidbody2D.velocity.x, - ¤tObj->rigidbody2D.velocity.y); - } else if (key == "hasCollider") { - currentObj->hasCollider = std::stoi(value) != 0; - } else if (key == "colliderEnabled") { - currentObj->collider.enabled = std::stoi(value) != 0; - } else if (key == "colliderType") { - currentObj->collider.type = static_cast(std::stoi(value)); - } else if (key == "colliderBox") { - sscanf(value.c_str(), "%f,%f,%f", - ¤tObj->collider.boxSize.x, - ¤tObj->collider.boxSize.y, - ¤tObj->collider.boxSize.z); - } else if (key == "colliderConvex") { - currentObj->collider.convex = std::stoi(value) != 0; - } else if (key == "hasPlayerController") { - currentObj->hasPlayerController = std::stoi(value) != 0; - } else if (key == "pcEnabled") { - currentObj->playerController.enabled = std::stoi(value) != 0; - } else if (key == "pcMoveSpeed") { - currentObj->playerController.moveSpeed = std::stof(value); - } else if (key == "pcLookSensitivity") { - currentObj->playerController.lookSensitivity = std::stof(value); - } else if (key == "pcHeight") { - currentObj->playerController.height = std::stof(value); - } else if (key == "pcRadius") { - currentObj->playerController.radius = std::stof(value); - } else if (key == "pcJumpStrength") { - currentObj->playerController.jumpStrength = std::stof(value); - } else if (key == "hasAudioSource") { - currentObj->hasAudioSource = std::stoi(value) != 0; - } else if (key == "audioEnabled") { - currentObj->audioSource.enabled = std::stoi(value) != 0; - } else if (key == "audioClip") { - currentObj->audioSource.clipPath = value; - } else if (key == "audioVolume") { - currentObj->audioSource.volume = std::stof(value); - } else if (key == "audioLoop") { - currentObj->audioSource.loop = std::stoi(value) != 0; - } else if (key == "audioPlayOnStart") { - currentObj->audioSource.playOnStart = std::stoi(value) != 0; - } else if (key == "audioSpatial") { - currentObj->audioSource.spatial = std::stoi(value) != 0; - } else if (key == "audioMinDistance") { - currentObj->audioSource.minDistance = std::stof(value); - } else if (key == "audioMaxDistance") { - currentObj->audioSource.maxDistance = std::stof(value); - } else if (key == "materialColor") { - sscanf(value.c_str(), "%f,%f,%f", - ¤tObj->material.color.r, - ¤tObj->material.color.g, - ¤tObj->material.color.b); - } else if (key == "materialAmbient") { - currentObj->material.ambientStrength = std::stof(value); - } else if (key == "materialSpecular") { - currentObj->material.specularStrength = std::stof(value); - } else if (key == "materialShininess") { - currentObj->material.shininess = std::stof(value); - } else if (key == "materialTextureMix") { - currentObj->material.textureMix = std::stof(value); - } else if (key == "materialPath") { - currentObj->materialPath = value; - } else if (key == "albedoTex") { - currentObj->albedoTexturePath = value; - } else if (key == "overlayTex") { - currentObj->overlayTexturePath = value; - } else if (key == "normalMap") { - currentObj->normalMapPath = value; - } else if (key == "vertexShader") { - currentObj->vertexShaderPath = value; - } else if (key == "fragmentShader") { - currentObj->fragmentShaderPath = value; - } else if (key == "useOverlay") { - currentObj->useOverlay = (std::stoi(value) != 0); - } else if (key == "additionalMaterialCount") { - int count = std::stoi(value); - currentObj->additionalMaterialPaths.resize(std::max(0, count)); } else if (key.rfind("additionalMaterial", 0) == 0) { int idx = std::stoi(key.substr(18)); // length of "additionalMaterial" if (idx >= 0 && idx < (int)currentObj->additionalMaterialPaths.size()) { currentObj->additionalMaterialPaths[idx] = value; } - } else if (key == "scripts") { - int count = std::stoi(value); - currentObj->scripts.resize(std::max(0, count)); } else if (key.rfind("script", 0) == 0) { size_t underscore = key.find('_'); if (underscore != std::string::npos && underscore > 6) { @@ -683,6 +1178,13 @@ bool SceneSerializer::loadScene(const fs::path& filePath, ScriptComponent& sc = currentObj->scripts[idx]; if (sub == "path") { sc.path = value; + } else if (sub == "lang" || sub == "language") { + int langValue = std::stoi(value); + sc.language = (langValue == static_cast(ScriptLanguage::CSharp)) + ? ScriptLanguage::CSharp + : ScriptLanguage::Cpp; + } else if (sub == "type") { + sc.managedType = value; } else if (sub == "enabled") { sc.enabled = std::stoi(value) != 0; } else if (sub == "settings" || sub == "settingCount") { @@ -705,177 +1207,41 @@ bool SceneSerializer::loadScene(const fs::path& filePath, } } } - } else if (key == "lightColor") { - sscanf(value.c_str(), "%f,%f,%f", - ¤tObj->light.color.r, - ¤tObj->light.color.g, - ¤tObj->light.color.b); - } else if (key == "lightIntensity") { - currentObj->light.intensity = std::stof(value); - } else if (key == "lightRange") { - currentObj->light.range = std::stof(value); - } else if (key == "lightEdgeFade") { - currentObj->light.edgeFade = std::stof(value); - } else if (key == "lightInner") { - currentObj->light.innerAngle = std::stof(value); - } else if (key == "lightOuter") { - currentObj->light.outerAngle = std::stof(value); - } else if (key == "lightSize") { - sscanf(value.c_str(), "%f,%f", - ¤tObj->light.size.x, - ¤tObj->light.size.y); - } else if (key == "lightEnabled") { - currentObj->light.enabled = (std::stoi(value) != 0); - } else if (key == "cameraType") { - currentObj->camera.type = static_cast(std::stoi(value)); - } else if (key == "cameraFov") { - currentObj->camera.fov = std::stof(value); - } else if (key == "cameraNear") { - currentObj->camera.nearClip = std::stof(value); - } else if (key == "cameraFar") { - currentObj->camera.farClip = std::stof(value); - } else if (key == "cameraPostFX") { - currentObj->camera.applyPostFX = (std::stoi(value) != 0); - } else if (key == "uiAnchor") { - currentObj->ui.anchor = static_cast(std::stoi(value)); - } else if (key == "uiPosition") { - sscanf(value.c_str(), "%f,%f", - ¤tObj->ui.position.x, - ¤tObj->ui.position.y); - } else if (key == "uiSize") { - sscanf(value.c_str(), "%f,%f", - ¤tObj->ui.size.x, - ¤tObj->ui.size.y); - } else if (key == "uiSliderValue") { - currentObj->ui.sliderValue = std::stof(value); - } else if (key == "uiSliderMin") { - currentObj->ui.sliderMin = std::stof(value); - } else if (key == "uiSliderMax") { - currentObj->ui.sliderMax = std::stof(value); - } else if (key == "uiLabel") { - currentObj->ui.label = value; - } else if (key == "uiColor") { - sscanf(value.c_str(), "%f,%f,%f,%f", - ¤tObj->ui.color.r, - ¤tObj->ui.color.g, - ¤tObj->ui.color.b, - ¤tObj->ui.color.a); - } else if (key == "uiInteractable") { - currentObj->ui.interactable = (std::stoi(value) != 0); - } else if (key == "uiSliderStyle") { - currentObj->ui.sliderStyle = static_cast(std::stoi(value)); - } else if (key == "uiButtonStyle") { - currentObj->ui.buttonStyle = static_cast(std::stoi(value)); - } else if (key == "uiStylePreset") { - currentObj->ui.stylePreset = value; - } else if (key == "uiTextScale") { - currentObj->ui.textScale = std::stof(value); - } else if (key == "postEnabled") { - currentObj->postFx.enabled = (std::stoi(value) != 0); - } else if (key == "postBloomEnabled") { - currentObj->postFx.bloomEnabled = (std::stoi(value) != 0); - } else if (key == "postBloomThreshold") { - currentObj->postFx.bloomThreshold = std::stof(value); - } else if (key == "postBloomIntensity") { - currentObj->postFx.bloomIntensity = std::stof(value); - } else if (key == "postBloomRadius") { - currentObj->postFx.bloomRadius = std::stof(value); - } else if (key == "postColorAdjustEnabled") { - currentObj->postFx.colorAdjustEnabled = (std::stoi(value) != 0); - } else if (key == "postExposure") { - currentObj->postFx.exposure = std::stof(value); - } else if (key == "postContrast") { - currentObj->postFx.contrast = std::stof(value); - } else if (key == "postSaturation") { - currentObj->postFx.saturation = std::stof(value); - } else if (key == "postColorFilter") { - sscanf(value.c_str(), "%f,%f,%f", - ¤tObj->postFx.colorFilter.r, - ¤tObj->postFx.colorFilter.g, - ¤tObj->postFx.colorFilter.b); - } else if (key == "postMotionBlurEnabled") { - currentObj->postFx.motionBlurEnabled = (std::stoi(value) != 0); - } else if (key == "postMotionBlurStrength") { - currentObj->postFx.motionBlurStrength = std::stof(value); - } else if (key == "postVignetteEnabled") { - currentObj->postFx.vignetteEnabled = (std::stoi(value) != 0); - } else if (key == "postVignetteIntensity") { - currentObj->postFx.vignetteIntensity = std::stof(value); - } else if (key == "postVignetteSmoothness") { - currentObj->postFx.vignetteSmoothness = std::stof(value); - } else if (key == "postChromaticEnabled") { - currentObj->postFx.chromaticAberrationEnabled = (std::stoi(value) != 0); - } else if (key == "postChromaticAmount") { - currentObj->postFx.chromaticAmount = std::stof(value); - } else if (key == "postAOEnabled") { - currentObj->postFx.ambientOcclusionEnabled = (std::stoi(value) != 0); - } else if (key == "postAORadius") { - currentObj->postFx.aoRadius = std::stof(value); - } else if (key == "postAOStrength") { - currentObj->postFx.aoStrength = std::stof(value); - } else if (key == "scriptCount") { - int count = std::stoi(value); - currentObj->scripts.resize(std::max(0, count)); - } else if (key.rfind("script", 0) == 0) { - size_t underscore = key.find('_'); - if (underscore != std::string::npos && underscore > 6) { - int idx = std::stoi(key.substr(6, underscore - 6)); - if (idx >= 0 && idx < (int)currentObj->scripts.size()) { - std::string subKey = key.substr(underscore + 1); - ScriptComponent& sc = currentObj->scripts[idx]; - if (subKey == "path") { - sc.path = value; - } else if (subKey == "enabled") { - sc.enabled = std::stoi(value) != 0; - } else if (subKey == "settingCount") { - int cnt = std::stoi(value); - sc.settings.resize(std::max(0, cnt)); - } else if (subKey.rfind("setting", 0) == 0) { - int sIdx = std::stoi(subKey.substr(7)); - if (sIdx >= 0 && sIdx < (int)sc.settings.size()) { - size_t sep = value.find(':'); - if (sep != std::string::npos) { - sc.settings[sIdx].key = value.substr(0, sep); - sc.settings[sIdx].value = value.substr(sep + 1); - } else { - sc.settings[sIdx].key.clear(); - sc.settings[sIdx].value = value; - } - } - } - } - } - } else if (key == "meshPath") { - currentObj->meshPath = value; - if (!value.empty() && currentObj->type == ObjectType::OBJMesh) { - std::string err; - currentObj->meshId = g_objLoader.loadOBJ(value, err); - } else if (!value.empty() && currentObj->type == ObjectType::Model) { - ModelLoadResult result = getModelLoader().loadModel(value); - if (result.success) { - currentObj->meshId = result.meshIndex; - } else { - std::cerr << "Failed to load model from scene: " << result.errorMessage << std::endl; - currentObj->meshId = -1; - } - } - } else if (key == "children" && !value.empty()) { - std::stringstream ss(value); - std::string item; - while (std::getline(ss, item, ',')) { - if (!item.empty()) { - currentObj->childIds.push_back(std::stoi(item)); - } - } } } } file.close(); + for (auto& obj : objects) { + obj.type = GetLegacyTypeFromComponents(obj); + } outVersion = sceneVersion; + if (outTimeOfDay) { + *outTimeOfDay = sceneTimeOfDay; + } return true; } catch (const std::exception& e) { std::cerr << "Failed to load scene: " << e.what() << std::endl; return false; } } + +bool SceneSerializer::loadSceneDeferred(const fs::path& filePath, + std::vector& objects, + int& nextId, + int& outVersion, + float* outTimeOfDay) { + struct DeferGuard { + bool previous = false; + explicit DeferGuard(bool enable) { + previous = g_deferSceneAssetLoading; + g_deferSceneAssetLoading = enable; + } + ~DeferGuard() { + g_deferSceneAssetLoading = previous; + } + }; + + DeferGuard guard(true); + return loadScene(filePath, objects, nextId, outVersion, outTimeOfDay); +} diff --git a/src/ProjectManager.h b/src/ProjectManager.h index 9d3df97..8b23ec1 100644 --- a/src/ProjectManager.h +++ b/src/ProjectManager.h @@ -56,10 +56,18 @@ class SceneSerializer { public: static bool saveScene(const fs::path& filePath, const std::vector& objects, - int nextId); + int nextId, + float timeOfDay); static bool loadScene(const fs::path& filePath, std::vector& objects, int& nextId, - int& outVersion); + int& outVersion, + float* outTimeOfDay = nullptr); + + static bool loadSceneDeferred(const fs::path& filePath, + std::vector& objects, + int& nextId, + int& outVersion, + float* outTimeOfDay = nullptr); }; diff --git a/src/Rendering.cpp b/src/Rendering.cpp index 01966fe..1b7927a 100644 --- a/src/Rendering.cpp +++ b/src/Rendering.cpp @@ -13,12 +13,12 @@ OBJLoader g_objLoader; // Cube vertex data float vertices[] = { // Back face (z = -0.5f) - -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f, - 0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f, - 0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f, - -0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, + -0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, + 0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f, + -0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, + 0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f, // Front face (z = 0.5f) -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, @@ -37,12 +37,12 @@ float vertices[] = { -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, // Right face (x = 0.5f) - 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, + 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, - 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, - 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, - 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, + 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, + 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, + 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, // Bottom face (y = -0.5f) -0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f, @@ -54,11 +54,11 @@ float vertices[] = { // Top face (y = 0.5f) -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, - 0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, - 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, - 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, - -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f + 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, + -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, + 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, + 0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f }; float mirrorPlaneVertices[] = { @@ -287,6 +287,7 @@ std::vector generateTorus(int segments, int sides) { // Mesh implementation Mesh::Mesh(const float* vertexData, size_t dataSizeBytes) { vertexCount = dataSizeBytes / (8 * sizeof(float)); + strideFloats = 8; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); @@ -308,9 +309,52 @@ Mesh::Mesh(const float* vertexData, size_t dataSizeBytes) { glBindVertexArray(0); } +Mesh::Mesh(const float* vertexData, size_t dataSizeBytes, bool dynamicUsage, + const void* boneData, size_t boneDataBytes) { + vertexCount = dataSizeBytes / (8 * sizeof(float)); + strideFloats = 8; + dynamic = dynamicUsage; + hasBones = boneData && boneDataBytes > 0; + + glGenVertexArrays(1, &VAO); + glGenBuffers(1, &VBO); + + glBindVertexArray(VAO); + glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBufferData(GL_ARRAY_BUFFER, dataSizeBytes, vertexData, dynamicUsage ? GL_DYNAMIC_DRAW : GL_STATIC_DRAW); + + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, strideFloats * sizeof(float), (void*)0); + glEnableVertexAttribArray(0); + + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, strideFloats * sizeof(float), (void*)(3 * sizeof(float))); + glEnableVertexAttribArray(1); + + glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, strideFloats * sizeof(float), (void*)(6 * sizeof(float))); + glEnableVertexAttribArray(2); + + if (hasBones) { + glGenBuffers(1, &boneVBO); + glBindBuffer(GL_ARRAY_BUFFER, boneVBO); + glBufferData(GL_ARRAY_BUFFER, boneDataBytes, boneData, GL_STATIC_DRAW); + + glVertexAttribIPointer(3, 4, GL_INT, sizeof(int) * 4 + sizeof(float) * 4, (void*)0); + glEnableVertexAttribArray(3); + + glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, sizeof(int) * 4 + sizeof(float) * 4, + (void*)(sizeof(int) * 4)); + glEnableVertexAttribArray(4); + } + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); +} + Mesh::~Mesh() { glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); + if (boneVBO) { + glDeleteBuffers(1, &boneVBO); + } } void Mesh::draw() const { @@ -319,6 +363,56 @@ void Mesh::draw() const { glBindVertexArray(0); } +void Mesh::updateVertices(const float* vertexData, size_t dataSizeBytes) { + if (!dynamic) return; + glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBufferSubData(GL_ARRAY_BUFFER, 0, dataSizeBytes, vertexData); + glBindBuffer(GL_ARRAY_BUFFER, 0); + vertexCount = dataSizeBytes / (strideFloats * sizeof(float)); +} + +static void applyCpuSkinning(OBJLoader::LoadedMesh& meshInfo, const std::vector& bones, int maxBones) { + if (!meshInfo.mesh || !meshInfo.isSkinned) return; + if (meshInfo.baseVertices.empty() || meshInfo.boneIds.empty() || meshInfo.boneWeights.empty()) return; + if (!meshInfo.mesh->isDynamic()) return; + + size_t vertexCount = meshInfo.baseVertices.size() / 8; + if (vertexCount == 0 || meshInfo.boneIds.size() != vertexCount || meshInfo.boneWeights.size() != vertexCount) { + return; + } + + std::vector skinned = meshInfo.baseVertices; + int boneLimit = std::min(static_cast(bones.size()), maxBones); + for (size_t i = 0; i < vertexCount; ++i) { + glm::vec3 basePos(skinned[i * 8 + 0], skinned[i * 8 + 1], skinned[i * 8 + 2]); + glm::vec3 baseNorm(skinned[i * 8 + 3], skinned[i * 8 + 4], skinned[i * 8 + 5]); + glm::ivec4 ids = meshInfo.boneIds[i]; + glm::vec4 weights = meshInfo.boneWeights[i]; + + glm::vec4 skinnedPos(0.0f); + glm::vec3 skinnedNorm(0.0f); + for (int k = 0; k < 4; ++k) { + int id = ids[k]; + float w = weights[k]; + if (w <= 0.0f || id < 0 || id >= boneLimit) continue; + const glm::mat4& m = bones[id]; + skinnedPos += w * (m * glm::vec4(basePos, 1.0f)); + skinnedNorm += w * glm::mat3(m) * baseNorm; + } + skinned[i * 8 + 0] = skinnedPos.x; + skinned[i * 8 + 1] = skinnedPos.y; + skinned[i * 8 + 2] = skinnedPos.z; + if (glm::length(skinnedNorm) > 1e-6f) { + skinnedNorm = glm::normalize(skinnedNorm); + } + skinned[i * 8 + 3] = skinnedNorm.x; + skinned[i * 8 + 4] = skinnedNorm.y; + skinned[i * 8 + 5] = skinnedNorm.z; + } + + meshInfo.mesh->updateVertices(skinned.data(), skinned.size() * sizeof(float)); +} + // OBJLoader implementation int OBJLoader::loadOBJ(const std::string& filepath, std::string& errorMsg) { // Check if already loaded @@ -821,7 +915,7 @@ void Renderer::updateMirrorTargets(const Camera& camera, const std::vectorsetFloat("specularStrength", obj.material.specularStrength); shader->setFloat("shininess", obj.material.shininess); shader->setFloat("mixAmount", obj.material.textureMix); - shader->setBool("unlit", obj.type == ObjectType::Mirror || obj.type == ObjectType::Sprite); + shader->setBool("unlit", obj.renderType == RenderType::Mirror || obj.renderType == RenderType::Sprite); Texture* baseTex = texture1; if (!obj.albedoTexturePath.empty()) { @@ -1026,7 +1120,7 @@ void Renderer::renderObject(const SceneObject& obj) { if (baseTex) baseTex->Bind(GL_TEXTURE0); bool overlayUsed = false; - if (obj.type == ObjectType::Mirror) { + if (obj.renderType == RenderType::Mirror) { auto it = mirrorTargets.find(obj.id); if (it != mirrorTargets.end() && it->second.texture != 0) { glActiveTexture(GL_TEXTURE1); @@ -1054,29 +1148,29 @@ void Renderer::renderObject(const SceneObject& obj) { } shader->setBool("hasNormalMap", normalUsed); - switch (obj.type) { - case ObjectType::Cube: + switch (obj.renderType) { + case RenderType::Cube: cubeMesh->draw(); break; - case ObjectType::Sphere: + case RenderType::Sphere: sphereMesh->draw(); break; - case ObjectType::Capsule: + case RenderType::Capsule: capsuleMesh->draw(); break; - case ObjectType::Plane: + case RenderType::Plane: if (planeMesh) planeMesh->draw(); break; - case ObjectType::Mirror: + case RenderType::Mirror: if (planeMesh) planeMesh->draw(); break; - case ObjectType::Sprite: + case RenderType::Sprite: if (planeMesh) planeMesh->draw(); break; - case ObjectType::Torus: + case RenderType::Torus: if (torusMesh) torusMesh->draw(); break; - case ObjectType::OBJMesh: + case RenderType::OBJMesh: if (obj.meshId >= 0) { Mesh* objMesh = g_objLoader.getMesh(obj.meshId); if (objMesh) { @@ -1084,7 +1178,7 @@ void Renderer::renderObject(const SceneObject& obj) { } } break; - case ObjectType::Model: + case RenderType::Model: if (obj.meshId >= 0) { Mesh* modelMesh = getModelLoader().getMesh(obj.meshId); if (modelMesh) { @@ -1092,26 +1186,8 @@ void Renderer::renderObject(const SceneObject& obj) { } } break; - case ObjectType::PointLight: - case ObjectType::SpotLight: - case ObjectType::AreaLight: - // Lights are not rendered as geometry - break; - case ObjectType::DirectionalLight: - // Not rendered as geometry - break; - case ObjectType::Camera: - // Cameras are editor helpers only - break; - case ObjectType::PostFXNode: - break; - case ObjectType::Sprite2D: - case ObjectType::Canvas: - case ObjectType::UIImage: - case ObjectType::UISlider: - case ObjectType::UIButton: - case ObjectType::UIText: - // UI types are rendered via ImGui, not here. + case RenderType::None: + default: break; } } @@ -1157,8 +1233,8 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector= kMaxLights) break; - } else if (obj.type == ObjectType::SpotLight) { + } else if (obj.light.type == LightType::Spot) { LightUniform l; l.type = 2; l.pos = obj.position; @@ -1182,7 +1258,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector(obj.skeletal.finalMatrices.size()); + bool needsFallback = obj.hasSkeletalAnimation && obj.skeletal.enabled && + obj.skeletal.allowCpuFallback && + boneLimit > 0 && availableBones > boneLimit; + bool wantsGpuSkinning = obj.hasSkeletalAnimation && obj.skeletal.enabled && + obj.skeletal.useGpuSkinning && !needsFallback; + if (vertPath.empty() && wantsGpuSkinning) { + vertPath = skinnedVertPath; + } + Shader* active = getShader(vertPath, fragPath); if (!active) continue; shader = active; shader->use(); - shader->setMat4("view", camera.getViewMatrix()); - shader->setMat4("projection", glm::perspective(glm::radians(fovDeg), (float)width / (float)height, nearPlane, farPlane)); + shader->setMat4("view", view); + shader->setMat4("projection", proj); shader->setVec3("viewPos", camera.position); - shader->setBool("unlit", obj.type == ObjectType::Mirror); + shader->setBool("unlit", obj.renderType == RenderType::Mirror); shader->setVec3("ambientColor", ambientColor); shader->setVec3("ambientColor", ambientColor); @@ -1263,13 +1364,6 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vectorsetFloat("lightAreaFadeArr" + idx, l.areaFade); } - glm::mat4 model = glm::mat4(1.0f); - model = glm::translate(model, obj.position); - model = glm::rotate(model, glm::radians(obj.rotation.x), glm::vec3(1.0f, 0.0f, 0.0f)); - model = glm::rotate(model, glm::radians(obj.rotation.y), glm::vec3(0.0f, 1.0f, 0.0f)); - model = glm::rotate(model, glm::radians(obj.rotation.z), glm::vec3(0.0f, 0.0f, 1.0f)); - model = glm::scale(model, obj.scale); - shader->setMat4("model", model); shader->setVec3("materialColor", obj.material.color); shader->setFloat("ambientStrength", obj.material.ambientStrength); @@ -1277,6 +1371,20 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vectorsetFloat("shininess", obj.material.shininess); shader->setFloat("mixAmount", obj.material.textureMix); + if (obj.hasSkeletalAnimation && obj.skeletal.enabled) { + int safeLimit = std::max(0, boneLimit); + int boneCount = std::min(availableBones, safeLimit); + if (wantsGpuSkinning && boneCount > 0) { + shader->setInt("boneCount", boneCount); + shader->setMat4Array("bones", obj.skeletal.finalMatrices.data(), boneCount); + shader->setBool("useSkinning", true); + } else { + shader->setBool("useSkinning", false); + } + } else { + shader->setBool("useSkinning", false); + } + Texture* baseTex = texture1; if (!obj.albedoTexturePath.empty()) { if (auto* t = getTexture(obj.albedoTexturePath)) baseTex = t; @@ -1284,7 +1392,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vectorBind(GL_TEXTURE0); bool overlayUsed = false; - if (obj.type == ObjectType::Mirror) { + if (obj.renderType == RenderType::Mirror) { auto it = mirrorTargets.find(obj.id); if (it != mirrorTargets.end() && it->second.texture != 0) { glActiveTexture(GL_TEXTURE1); @@ -1313,31 +1421,51 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vectorsetBool("hasNormalMap", normalUsed); Mesh* meshToDraw = nullptr; - if (obj.type == ObjectType::Cube) meshToDraw = cubeMesh; - else if (obj.type == ObjectType::Sphere) meshToDraw = sphereMesh; - else if (obj.type == ObjectType::Capsule) meshToDraw = capsuleMesh; - else if (obj.type == ObjectType::Plane) meshToDraw = planeMesh; - else if (obj.type == ObjectType::Mirror) meshToDraw = planeMesh; - else if (obj.type == ObjectType::Sprite) meshToDraw = planeMesh; - else if (obj.type == ObjectType::Torus) meshToDraw = torusMesh; - else if (obj.type == ObjectType::OBJMesh && obj.meshId != -1) { + if (obj.renderType == RenderType::Cube) meshToDraw = cubeMesh; + else if (obj.renderType == RenderType::Sphere) meshToDraw = sphereMesh; + else if (obj.renderType == RenderType::Capsule) meshToDraw = capsuleMesh; + else if (obj.renderType == RenderType::Plane) meshToDraw = planeMesh; + else if (obj.renderType == RenderType::Mirror) meshToDraw = planeMesh; + else if (obj.renderType == RenderType::Sprite) meshToDraw = planeMesh; + else if (obj.renderType == RenderType::Torus) meshToDraw = torusMesh; + else if (obj.renderType == RenderType::OBJMesh && obj.meshId != -1) { meshToDraw = g_objLoader.getMesh(obj.meshId); - } else if (obj.type == ObjectType::Model && obj.meshId != -1) { + } else if (obj.renderType == RenderType::Model && obj.meshId != -1) { meshToDraw = getModelLoader().getMesh(obj.meshId); } + if (obj.renderType == RenderType::Model && obj.meshId != -1 && + obj.hasSkeletalAnimation && obj.skeletal.enabled && !wantsGpuSkinning) { + const auto* meshInfo = getModelLoader().getMeshInfo(obj.meshId); + if (meshInfo) { + applyCpuSkinning(*const_cast(meshInfo), + obj.skeletal.finalMatrices, + obj.skeletal.maxBones); + } + } + + bool doubleSided = (obj.renderType == RenderType::Sprite || obj.renderType == RenderType::Mirror); + if (doubleSided) { + glDisable(GL_CULL_FACE); + } else { + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + } + if (meshToDraw) { recordMeshDraw(); meshToDraw->draw(); } } - if (skybox) { - glm::mat4 view = camera.getViewMatrix(); - glm::mat4 proj = glm::perspective(glm::radians(fovDeg), - (float)width / height, - nearPlane, farPlane); + if (!cullFace) { + glDisable(GL_CULL_FACE); + } else { + glEnable(GL_CULL_FACE); + glCullFace(prevCullMode); + } + if (skybox) { recordDrawCall(); skybox->draw(glm::value_ptr(view), glm::value_ptr(proj)); } @@ -1351,7 +1479,7 @@ PostFXSettings Renderer::gatherPostFX(const std::vector& sceneObjec PostFXSettings combined; combined.enabled = false; for (const auto& obj : sceneObjects) { - if (obj.type != ObjectType::PostFXNode) continue; + if (!obj.hasPostFX) continue; if (!obj.postFx.enabled) continue; combined = obj.postFx; // Last enabled node wins for now combined.enabled = true; @@ -1601,9 +1729,9 @@ void Renderer::renderCollisionOverlay(const Camera& camera, const std::vector= 0) { + if (obj.hasRenderer && obj.renderType == RenderType::OBJMesh && obj.meshId >= 0) { meshToDraw = g_objLoader.getMesh(obj.meshId); - } else if (obj.type == ObjectType::Model && obj.meshId >= 0) { + } else if (obj.hasRenderer && obj.renderType == RenderType::Model && obj.meshId >= 0) { meshToDraw = getModelLoader().getMesh(obj.meshId); } else { meshToDraw = nullptr; @@ -1612,29 +1740,29 @@ void Renderer::renderCollisionOverlay(const Camera& camera, const std::vector= 0) meshToDraw = g_objLoader.getMesh(obj.meshId); break; - case ObjectType::Model: + case RenderType::Model: if (obj.meshId >= 0) meshToDraw = getModelLoader().getMesh(obj.meshId); break; default: @@ -1673,31 +1801,30 @@ void Renderer::renderSelectionOutline(const Camera& camera, const std::vectorenabled) return; - if (selectedObj->type == ObjectType::PointLight || - selectedObj->type == ObjectType::SpotLight || - selectedObj->type == ObjectType::AreaLight || - selectedObj->type == ObjectType::Camera || - selectedObj->type == ObjectType::PostFXNode || - selectedObj->type == ObjectType::Canvas || - selectedObj->type == ObjectType::UIImage || - selectedObj->type == ObjectType::UISlider || - selectedObj->type == ObjectType::UIButton || - selectedObj->type == ObjectType::UIText || - selectedObj->type == ObjectType::Sprite2D) { + if (!HasRendererComponent(*selectedObj)) { return; } + bool wantsGpuSkinning = selectedObj->hasSkeletalAnimation && selectedObj->skeletal.enabled && + selectedObj->skeletal.useGpuSkinning; + int boneLimit = selectedObj->skeletal.maxBones; + int availableBones = static_cast(selectedObj->skeletal.finalMatrices.size()); + if (selectedObj->hasSkeletalAnimation && selectedObj->skeletal.enabled && + selectedObj->skeletal.allowCpuFallback && boneLimit > 0 && availableBones > boneLimit) { + wantsGpuSkinning = false; + } + Mesh* meshToDraw = nullptr; - if (selectedObj->type == ObjectType::Cube) meshToDraw = cubeMesh; - else if (selectedObj->type == ObjectType::Sphere) meshToDraw = sphereMesh; - else if (selectedObj->type == ObjectType::Capsule) meshToDraw = capsuleMesh; - else if (selectedObj->type == ObjectType::Plane) meshToDraw = planeMesh; - else if (selectedObj->type == ObjectType::Mirror) meshToDraw = planeMesh; - else if (selectedObj->type == ObjectType::Sprite) meshToDraw = planeMesh; - else if (selectedObj->type == ObjectType::Torus) meshToDraw = torusMesh; - else if (selectedObj->type == ObjectType::OBJMesh && selectedObj->meshId != -1) { + if (selectedObj->renderType == RenderType::Cube) meshToDraw = cubeMesh; + else if (selectedObj->renderType == RenderType::Sphere) meshToDraw = sphereMesh; + else if (selectedObj->renderType == RenderType::Capsule) meshToDraw = capsuleMesh; + else if (selectedObj->renderType == RenderType::Plane) meshToDraw = planeMesh; + else if (selectedObj->renderType == RenderType::Mirror) meshToDraw = planeMesh; + else if (selectedObj->renderType == RenderType::Sprite) meshToDraw = planeMesh; + else if (selectedObj->renderType == RenderType::Torus) meshToDraw = torusMesh; + else if (selectedObj->renderType == RenderType::OBJMesh && selectedObj->meshId != -1) { meshToDraw = g_objLoader.getMesh(selectedObj->meshId); - } else if (selectedObj->type == ObjectType::Model && selectedObj->meshId != -1) { + } else if (selectedObj->renderType == RenderType::Model && selectedObj->meshId != -1) { meshToDraw = getModelLoader().getMesh(selectedObj->meshId); } if (!meshToDraw) return; @@ -1764,6 +1891,16 @@ void Renderer::renderSelectionOutline(const Camera& camera, const std::vectorrotation.z), glm::vec3(0.0f, 0.0f, 1.0f)); baseModel = glm::scale(baseModel, selectedObj->scale); + if (selectedObj->renderType == RenderType::Model && selectedObj->meshId != -1 && + selectedObj->hasSkeletalAnimation && selectedObj->skeletal.enabled && !wantsGpuSkinning) { + const auto* meshInfo = getModelLoader().getMeshInfo(selectedObj->meshId); + if (meshInfo) { + applyCpuSkinning(*const_cast(meshInfo), + selectedObj->skeletal.finalMatrices, + selectedObj->skeletal.maxBones); + } + } + // Mark the object in the stencil buffer. glEnable(GL_STENCIL_TEST); glStencilMask(0xFF); diff --git a/src/Rendering.h b/src/Rendering.h index bf298ec..bb0a474 100644 --- a/src/Rendering.h +++ b/src/Rendering.h @@ -19,14 +19,23 @@ std::vector generateTorus(int segments = 32, int sides = 16); class Mesh { private: unsigned int VAO, VBO; + unsigned int boneVBO = 0; int vertexCount; + int strideFloats = 8; + bool dynamic = false; + bool hasBones = false; public: Mesh(const float* vertexData, size_t dataSizeBytes); + Mesh(const float* vertexData, size_t dataSizeBytes, bool dynamicUsage, + const void* boneData, size_t boneDataBytes); ~Mesh(); void draw() const; + void updateVertices(const float* vertexData, size_t dataSizeBytes); int getVertexCount() const { return vertexCount; } + bool isDynamic() const { return dynamic; } + bool usesBones() const { return hasBones; } }; class OBJLoader { @@ -44,6 +53,12 @@ public: std::vector triangleVertices; // positions duplicated per-triangle for picking std::vector positions; // unique vertex positions for physics std::vector triangleIndices; // triangle indices into positions + bool isSkinned = false; + std::vector boneNames; + std::vector inverseBindMatrices; + std::vector boneIds; + std::vector boneWeights; + std::vector baseVertices; }; private: @@ -104,6 +119,7 @@ private: }; std::unordered_map shaderCache; std::string defaultVertPath = "Resources/Shaders/vert.glsl"; + std::string skinnedVertPath = "Resources/Shaders/skinned_vert.glsl"; std::string defaultFragPath = "Resources/Shaders/frag.glsl"; std::string postVertPath = "Resources/Shaders/postfx_vert.glsl"; std::string postFragPath = "Resources/Shaders/postfx_frag.glsl"; diff --git a/src/SceneObject.h b/src/SceneObject.h index 456757f..6be8a37 100644 --- a/src/SceneObject.h +++ b/src/SceneObject.h @@ -23,7 +23,31 @@ enum class ObjectType { UIImage = 17, UISlider = 18, UIButton = 19, - UIText = 20 + UIText = 20, + Empty = 21 +}; + +enum class RenderType { + None = 0, + Cube = 1, + Sphere = 2, + Capsule = 3, + OBJMesh = 4, + Model = 5, + Mirror = 6, + Plane = 7, + Torus = 8, + Sprite = 9 +}; + +enum class UIElementType { + None = 0, + Canvas = 1, + Image = 2, + Slider = 3, + Button = 4, + Text = 5, + Sprite2D = 6 }; struct MaterialProperties { @@ -60,6 +84,76 @@ enum class UIButtonStyle { Outline = 1 }; +enum class ReverbPreset { + Room = 0, + LivingRoom = 1, + Hall = 2, + Forest = 3, + Custom = 4 +}; + +enum class ReverbZoneShape { + Box = 0, + Sphere = 1 +}; + +enum class AudioRolloffMode { + Logarithmic = 0, + Linear = 1, + Exponential = 2, + Custom = 3 +}; + +enum class AnimationInterpolation { + Linear = 0, + SmoothStep = 1, + EaseIn = 2, + EaseOut = 3, + EaseInOut = 4 +}; + +enum class AnimationCurveMode { + Preset = 0, + Bezier = 1 +}; + +struct AnimationKeyframe { + float time = 0.0f; + glm::vec3 position = glm::vec3(0.0f); + glm::vec3 rotation = glm::vec3(0.0f); + glm::vec3 scale = glm::vec3(1.0f); + AnimationInterpolation interpolation = AnimationInterpolation::SmoothStep; + AnimationCurveMode curveMode = AnimationCurveMode::Preset; + glm::vec2 bezierIn = glm::vec2(0.25f, 0.0f); + glm::vec2 bezierOut = glm::vec2(0.75f, 1.0f); +}; + +struct AnimationComponent { + bool enabled = true; + float clipLength = 2.0f; + float playSpeed = 1.0f; + bool loop = true; + bool applyOnScrub = true; + std::vector keyframes; +}; + +struct SkeletalAnimationComponent { + bool enabled = true; + bool useGpuSkinning = true; + bool allowCpuFallback = true; + bool useAnimation = true; + int clipIndex = 0; + float time = 0.0f; + float playSpeed = 1.0f; + bool loop = true; + int skeletonRootId = -1; + int maxBones = 128; + std::vector boneNames; + std::vector boneNodeIds; + std::vector inverseBindMatrices; + std::vector finalMatrices; +}; + struct LightComponent { LightType type = LightType::Point; glm::vec3 color = glm::vec3(1.0f); @@ -85,6 +179,8 @@ struct CameraComponent { float nearClip = NEAR_PLANE; float farClip = FAR_PLANE; bool applyPostFX = true; + bool use2D = false; + float pixelsPerUnit = 100.0f; }; struct PostFXSettings { @@ -122,9 +218,16 @@ struct ScriptSetting { std::string value; }; +enum class ScriptLanguage { + Cpp = 0, + CSharp = 1 +}; + struct ScriptComponent { bool enabled = true; + ScriptLanguage language = ScriptLanguage::Cpp; std::string path; + std::string managedType; std::vector settings; std::string lastBinaryPath; std::vector activeIEnums; // function pointers registered via IEnum_Start @@ -169,9 +272,11 @@ struct PlayerControllerComponent { }; struct UIElementComponent { + UIElementType type = UIElementType::None; UIAnchor anchor = UIAnchor::Center; glm::vec2 position = glm::vec2(0.0f); // offset in pixels from anchor glm::vec2 size = glm::vec2(160.0f, 40.0f); + float rotation = 0.0f; float sliderValue = 0.5f; float sliderMin = 0.0f; float sliderMax = 1.0f; @@ -193,6 +298,37 @@ struct Rigidbody2DComponent { glm::vec2 velocity = glm::vec2(0.0f); }; +enum class Collider2DType { + Box = 0, + Polygon = 1, + Edge = 2 +}; + +struct Collider2DComponent { + bool enabled = true; + Collider2DType type = Collider2DType::Box; + glm::vec2 boxSize = glm::vec2(1.0f); + std::vector points; + bool closed = false; + float edgeThickness = 0.05f; +}; + +struct ParallaxLayer2DComponent { + bool enabled = true; + int order = 0; + float factor = 1.0f; // 1 = world locked, 0 = camera locked + bool repeatX = false; + bool repeatY = false; + glm::vec2 repeatSpacing = glm::vec2(0.0f); +}; + +struct CameraFollow2DComponent { + bool enabled = true; + int targetId = -1; + glm::vec2 offset = glm::vec2(0.0f); + float smoothTime = 0.0f; // seconds; 0 snaps to target +}; + struct AudioSourceComponent { bool enabled = true; std::string clipPath; @@ -202,6 +338,36 @@ struct AudioSourceComponent { bool spatial = true; float minDistance = 1.0f; float maxDistance = 25.0f; + AudioRolloffMode rolloffMode = AudioRolloffMode::Logarithmic; + float rolloff = 1.0f; + float customMidDistance = 0.5f; + float customMidGain = 0.6f; + float customEndGain = 0.0f; +}; + +struct ReverbZoneComponent { + bool enabled = true; + ReverbPreset preset = ReverbPreset::Room; + ReverbZoneShape shape = ReverbZoneShape::Box; + glm::vec3 boxSize = glm::vec3(6.0f); + float radius = 6.0f; + float blendDistance = 1.0f; + float minDistance = 1.0f; + float maxDistance = 15.0f; + float room = -1000.0f; // dB + float roomHF = -100.0f; // dB + float roomLF = 0.0f; // dB + float decayTime = 1.49f; // s + float decayHFRatio = 0.83f; // 0.1..2 + float reflections = -2602.0f; // dB + float reflectionsDelay = 0.007f; // s + float reverb = 200.0f; // dB + float reverbDelay = 0.011f; // s + float hfReference = 5000.0f; // Hz + float lfReference = 250.0f; // Hz + float roomRolloffFactor = 0.0f; + float diffusion = 100.0f; // 0..100 + float density = 100.0f; // 0..100 }; class SceneObject { @@ -211,6 +377,12 @@ public: bool enabled = true; int layer = 0; std::string tag = "Untagged"; + bool hasRenderer = false; + RenderType renderType = RenderType::None; + bool hasLight = false; + bool hasCamera = false; + bool hasPostFX = false; + bool hasUI = false; glm::vec3 position; glm::vec3 rotation; glm::vec3 scale; @@ -224,6 +396,7 @@ public: bool isExpanded = true; std::string meshPath; // Path to imported model file int meshId = -1; // Index into loaded mesh caches (OBJLoader / ModelLoader) + int meshSourceIndex = -1; // Source mesh index for multi-mesh models MaterialProperties material; std::string materialPath; // Optional external material asset std::string albedoTexturePath; @@ -241,12 +414,24 @@ public: RigidbodyComponent rigidbody; bool hasRigidbody2D = false; Rigidbody2DComponent rigidbody2D; + bool hasCollider2D = false; + Collider2DComponent collider2D; + bool hasParallaxLayer2D = false; + ParallaxLayer2DComponent parallaxLayer2D; + bool hasCameraFollow2D = false; + CameraFollow2DComponent cameraFollow2D; bool hasCollider = false; ColliderComponent collider; bool hasPlayerController = false; PlayerControllerComponent playerController; bool hasAudioSource = false; AudioSourceComponent audioSource; + bool hasReverbZone = false; + ReverbZoneComponent reverbZone; + bool hasAnimation = false; + AnimationComponent animation; + bool hasSkeletalAnimation = false; + SkeletalAnimationComponent skeletal; UIElementComponent ui; SceneObject(const std::string& name, ObjectType type, int id) @@ -261,3 +446,11 @@ public: localInitialized(true), id(id) {} }; + +inline bool HasRendererComponent(const SceneObject& obj) { + return obj.hasRenderer && obj.renderType != RenderType::None; +} + +inline bool HasUIComponent(const SceneObject& obj) { + return obj.hasUI && obj.ui.type != UIElementType::None; +} diff --git a/src/ScriptCompiler.cpp b/src/ScriptCompiler.cpp index 6b3df37..757eaae 100644 --- a/src/ScriptCompiler.cpp +++ b/src/ScriptCompiler.cpp @@ -173,6 +173,16 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat relToScripts.clear(); } + auto hasDotDot = [](const fs::path& path) { + for (const auto& part : path) { + if (part == "..") return true; + } + return false; + }; + if (relToScripts.empty() || relToScripts.is_absolute() || hasDotDot(relToScripts)) { + relToScripts.clear(); + } + fs::path relativeParent = relToScripts.has_parent_path() ? relToScripts.parent_path() : fs::path(); std::string baseName = scriptAbs.stem().string(); fs::path objectPath = config.outDir / relativeParent / (baseName + ".o"); @@ -246,10 +256,24 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat FunctionSpec testEditorSpec = detectFunction(scriptSource, "TestEditor"); FunctionSpec updateSpec = detectFunction(scriptSource, "Update"); FunctionSpec tickUpdateSpec = detectFunction(scriptSource, "TickUpdate"); + FunctionSpec inspectorSpec = detectFunction(scriptSource, "Script_OnInspector"); + + auto hasExternCInspector = [&]() { + try { + std::regex direct(R"(extern\s+"C"\s+void\s+Script_OnInspector\s*\()"); + if (std::regex_search(scriptSource, direct)) return true; + std::regex block(R"(extern\s+"C"\s*\{[\s\S]*?\bScript_OnInspector\b)"); + return std::regex_search(scriptSource, block); + } catch (...) { + return false; + } + }; + bool inspectorExtern = hasExternCInspector(); + bool needsInspectorWrap = inspectorSpec.present && !inspectorExtern; fs::path wrapperPath; bool useWrapper = beginSpec.present || specSpec.present || testEditorSpec.present - || updateSpec.present || tickUpdateSpec.present; + || updateSpec.present || tickUpdateSpec.present || needsInspectorWrap; fs::path sourceToCompile = scriptAbs; if (useWrapper) { @@ -264,8 +288,15 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat } std::string includePath = scriptAbs.lexically_normal().generic_string(); + if (needsInspectorWrap) { + wrapper << "#define Script_OnInspector Script_OnInspector_Impl\n"; + } wrapper << "#include \"ScriptRuntime.h\"\n"; - wrapper << "#include \"" << includePath << "\"\n\n"; + wrapper << "#include \"" << includePath << "\"\n"; + if (needsInspectorWrap) { + wrapper << "#undef Script_OnInspector\n"; + } + wrapper << "\n"; wrapper << "extern \"C\" {\n"; auto emitWrapper = [&](const char* exportedName, const char* implName, @@ -293,6 +324,16 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat emitWrapper("Script_TestEditor", "TestEditor", testEditorSpec); emitWrapper("Script_Update", "Update", updateSpec); emitWrapper("Script_TickUpdate", "TickUpdate", tickUpdateSpec); + if (needsInspectorWrap) { + wrapper << "void Script_OnInspector(ScriptContext& ctx) {\n"; + if (inspectorSpec.takesContext) { + wrapper << " Script_OnInspector_Impl(ctx);\n"; + } else { + wrapper << " (void)ctx;\n"; + wrapper << " Script_OnInspector_Impl();\n"; + } + wrapper << "}\n\n"; + } wrapper << "}\n"; sourceToCompile = wrapperPath; diff --git a/src/ScriptRuntime.cpp b/src/ScriptRuntime.cpp index d16f4cf..b782292 100644 --- a/src/ScriptRuntime.cpp +++ b/src/ScriptRuntime.cpp @@ -43,13 +43,8 @@ std::string trimString(const std::string& input) { return input.substr(start, end - start); } -bool isUIObjectType(ObjectType type) { - return type == ObjectType::Canvas || - type == ObjectType::UIImage || - type == ObjectType::UISlider || - type == ObjectType::UIButton || - type == ObjectType::UIText || - type == ObjectType::Sprite2D; +bool isUIObject(const SceneObject* obj) { + return obj && HasUIComponent(*obj); } } @@ -345,7 +340,7 @@ void ScriptContext::TickStandaloneMovement(StandaloneMovementState& state, Stand } bool ScriptContext::IsUIButtonPressed() const { - return object && object->type == ObjectType::UIButton && object->ui.buttonPressed; + return object && object->hasUI && object->ui.type == UIElementType::Button && object->ui.buttonPressed; } bool ScriptContext::IsUIInteractable() const { @@ -361,12 +356,12 @@ void ScriptContext::SetUIInteractable(bool interactable) { } float ScriptContext::GetUISliderValue() const { - if (!object || object->type != ObjectType::UISlider) return 0.0f; + if (!object || !object->hasUI || object->ui.type != UIElementType::Slider) return 0.0f; return object->ui.sliderValue; } void ScriptContext::SetUISliderValue(float value) { - if (!object || object->type != ObjectType::UISlider) return; + if (!object || !object->hasUI || object->ui.type != UIElementType::Slider) return; float clamped = std::clamp(value, object->ui.sliderMin, object->ui.sliderMax); if (object->ui.sliderValue != clamped) { object->ui.sliderValue = clamped; @@ -375,7 +370,7 @@ void ScriptContext::SetUISliderValue(float value) { } void ScriptContext::SetUISliderRange(float minValue, float maxValue) { - if (!object || object->type != ObjectType::UISlider) return; + if (!object || !object->hasUI || object->ui.type != UIElementType::Slider) return; if (maxValue < minValue) std::swap(minValue, maxValue); object->ui.sliderMin = minValue; object->ui.sliderMax = maxValue; @@ -400,12 +395,12 @@ void ScriptContext::SetUIColor(const glm::vec4& color) { } float ScriptContext::GetUITextScale() const { - if (!object || object->type != ObjectType::UIText) return 1.0f; + if (!object || !object->hasUI || object->ui.type != UIElementType::Text) return 1.0f; return object->ui.textScale; } void ScriptContext::SetUITextScale(float scale) { - if (!object || object->type != ObjectType::UIText) return; + if (!object || !object->hasUI || object->ui.type != UIElementType::Text) return; float clamped = std::max(0.1f, scale); if (object->ui.textScale != clamped) { object->ui.textScale = clamped; @@ -414,7 +409,7 @@ void ScriptContext::SetUITextScale(float scale) { } void ScriptContext::SetUISliderStyle(UISliderStyle style) { - if (!object || object->type != ObjectType::UISlider) return; + if (!object || !object->hasUI || object->ui.type != UIElementType::Slider) return; if (object->ui.sliderStyle != style) { object->ui.sliderStyle = style; MarkDirty(); @@ -422,7 +417,7 @@ void ScriptContext::SetUISliderStyle(UISliderStyle style) { } void ScriptContext::SetUIButtonStyle(UIButtonStyle style) { - if (!object || object->type != ObjectType::UIButton) return; + if (!object || !object->hasUI || object->ui.type != UIElementType::Button) return; if (object->ui.buttonStyle != style) { object->ui.buttonStyle = style; MarkDirty(); @@ -454,7 +449,7 @@ bool ScriptContext::HasRigidbody() const { } bool ScriptContext::HasRigidbody2D() const { - return object && isUIObjectType(object->type) && object->hasRigidbody2D && object->rigidbody2D.enabled; + return isUIObject(object) && object->hasRigidbody2D && object->rigidbody2D.enabled; } bool ScriptContext::EnsureCapsuleCollider(float height, float radius) { diff --git a/src/Shaders/Shader_Manager/Shader.cpp b/src/Shaders/Shader_Manager/Shader.cpp index 6160e13..32e04c5 100644 --- a/src/Shaders/Shader_Manager/Shader.cpp +++ b/src/Shaders/Shader_Manager/Shader.cpp @@ -138,3 +138,9 @@ void Shader::setMat4(const std::string &name, const glm::mat4 &mat) const { glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, glm::value_ptr(mat)); } + +void Shader::setMat4Array(const std::string &name, const glm::mat4 *data, int count) const +{ + if (count <= 0 || !data) return; + glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), count, GL_FALSE, glm::value_ptr(data[0])); +} diff --git a/src/ThirdParty/ImGuiColorTextEdit/TextEditor.cpp b/src/ThirdParty/ImGuiColorTextEdit/TextEditor.cpp new file mode 100644 index 0000000..d6139d7 --- /dev/null +++ b/src/ThirdParty/ImGuiColorTextEdit/TextEditor.cpp @@ -0,0 +1,3224 @@ +#include +#include +#include +#include +#include + +#include "TextEditor.h" + +#include "imgui.h" + +// TODO +// - multiline comments vs single-line: latter is blocking start of a ML + +template +bool equals(InputIt1 first1, InputIt1 last1, + InputIt2 first2, InputIt2 last2, BinaryPredicate p) +{ + for (; first1 != last1 && first2 != last2; ++first1, ++first2) + { + if (!p(*first1, *first2)) + return false; + } + return first1 == last1 && first2 == last2; +} + +TextEditor::TextEditor() + : mLineSpacing(1.0f) + , mUndoIndex(0) + , mTabSize(4) + , mOverwrite(false) + , mReadOnly(false) + , mWithinRender(false) + , mScrollToCursor(false) + , mScrollToTop(false) + , mTextChanged(false) + , mColorizerEnabled(true) + , mTextStart(20.0f) + , mLeftMargin(10) + , mCursorPositionChanged(false) + , mColorRangeMin(0) + , mColorRangeMax(0) + , mSelectionMode(SelectionMode::Normal) + , mCheckComments(true) + , mLastClick(-1.0f) + , mHandleKeyboardInputs(true) + , mHandleMouseInputs(true) + , mAllowTabInput(true) + , mSmartTabDelete(true) + , mCursorScreenPos(0.0f, 0.0f) + , mCursorScreenPosValid(false) + , mIgnoreImGuiChild(false) + , mShowWhitespaces(true) + , mStartTime(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) +{ + SetPalette(GetDarkPalette()); + SetLanguageDefinition(LanguageDefinition::HLSL()); + mLines.push_back(Line()); +} + +TextEditor::~TextEditor() +{ +} + +void TextEditor::SetLanguageDefinition(const LanguageDefinition & aLanguageDef) +{ + mLanguageDefinition = aLanguageDef; + mRegexList.clear(); + + for (auto& r : mLanguageDefinition.mTokenRegexStrings) + mRegexList.push_back(std::make_pair(std::regex(r.first, std::regex_constants::optimize), r.second)); + + Colorize(); +} + +void TextEditor::SetPalette(const Palette & aValue) +{ + mPaletteBase = aValue; +} + +std::string TextEditor::GetText(const Coordinates & aStart, const Coordinates & aEnd) const +{ + std::string result; + + auto lstart = aStart.mLine; + auto lend = aEnd.mLine; + auto istart = GetCharacterIndex(aStart); + auto iend = GetCharacterIndex(aEnd); + size_t s = 0; + + for (size_t i = lstart; i < lend; i++) + s += mLines[i].size(); + + result.reserve(s + s / 8); + + while (istart < iend || lstart < lend) + { + if (lstart >= (int)mLines.size()) + break; + + auto& line = mLines[lstart]; + if (istart < (int)line.size()) + { + result += line[istart].mChar; + istart++; + } + else + { + istart = 0; + ++lstart; + result += '\n'; + } + } + + return result; +} + +TextEditor::Coordinates TextEditor::GetActualCursorCoordinates() const +{ + return SanitizeCoordinates(mState.mCursorPosition); +} + +std::string TextEditor::GetWordUnderCursorPublic() const +{ + return GetWordUnderCursor(); +} + +std::string TextEditor::GetWordAtPublic(const Coordinates& aCoords) const +{ + return GetWordAt(aCoords); +} + +TextEditor::Coordinates TextEditor::SanitizeCoordinates(const Coordinates & aValue) const +{ + auto line = aValue.mLine; + auto column = aValue.mColumn; + if (line >= (int)mLines.size()) + { + if (mLines.empty()) + { + line = 0; + column = 0; + } + else + { + line = (int)mLines.size() - 1; + column = GetLineMaxColumn(line); + } + return Coordinates(line, column); + } + else + { + column = mLines.empty() ? 0 : std::min(column, GetLineMaxColumn(line)); + return Coordinates(line, column); + } +} + +// https://en.wikipedia.org/wiki/UTF-8 +// We assume that the char is a standalone character (<128) or a leading byte of an UTF-8 code sequence (non-10xxxxxx code) +static int UTF8CharLength(TextEditor::Char c) +{ + if ((c & 0xFE) == 0xFC) + return 6; + if ((c & 0xFC) == 0xF8) + return 5; + if ((c & 0xF8) == 0xF0) + return 4; + else if ((c & 0xF0) == 0xE0) + return 3; + else if ((c & 0xE0) == 0xC0) + return 2; + return 1; +} + +// "Borrowed" from ImGui source +static inline int ImTextCharToUtf8(char* buf, int buf_size, unsigned int c) +{ + if (c < 0x80) + { + buf[0] = (char)c; + return 1; + } + if (c < 0x800) + { + if (buf_size < 2) return 0; + buf[0] = (char)(0xc0 + (c >> 6)); + buf[1] = (char)(0x80 + (c & 0x3f)); + return 2; + } + if (c >= 0xdc00 && c < 0xe000) + { + return 0; + } + if (c >= 0xd800 && c < 0xdc00) + { + if (buf_size < 4) return 0; + buf[0] = (char)(0xf0 + (c >> 18)); + buf[1] = (char)(0x80 + ((c >> 12) & 0x3f)); + buf[2] = (char)(0x80 + ((c >> 6) & 0x3f)); + buf[3] = (char)(0x80 + ((c) & 0x3f)); + return 4; + } + //else if (c < 0x10000) + { + if (buf_size < 3) return 0; + buf[0] = (char)(0xe0 + (c >> 12)); + buf[1] = (char)(0x80 + ((c >> 6) & 0x3f)); + buf[2] = (char)(0x80 + ((c) & 0x3f)); + return 3; + } +} + +void TextEditor::Advance(Coordinates & aCoordinates) const +{ + if (aCoordinates.mLine < (int)mLines.size()) + { + auto& line = mLines[aCoordinates.mLine]; + auto cindex = GetCharacterIndex(aCoordinates); + + if (cindex + 1 < (int)line.size()) + { + auto delta = UTF8CharLength(line[cindex].mChar); + cindex = std::min(cindex + delta, (int)line.size() - 1); + } + else + { + ++aCoordinates.mLine; + cindex = 0; + } + aCoordinates.mColumn = GetCharacterColumn(aCoordinates.mLine, cindex); + } +} + +void TextEditor::DeleteRange(const Coordinates & aStart, const Coordinates & aEnd) +{ + assert(aEnd >= aStart); + assert(!mReadOnly); + + //printf("D(%d.%d)-(%d.%d)\n", aStart.mLine, aStart.mColumn, aEnd.mLine, aEnd.mColumn); + + if (aEnd == aStart) + return; + + auto start = GetCharacterIndex(aStart); + auto end = GetCharacterIndex(aEnd); + + if (aStart.mLine == aEnd.mLine) + { + auto& line = mLines[aStart.mLine]; + auto n = GetLineMaxColumn(aStart.mLine); + if (aEnd.mColumn >= n) + line.erase(line.begin() + start, line.end()); + else + line.erase(line.begin() + start, line.begin() + end); + } + else + { + auto& firstLine = mLines[aStart.mLine]; + auto& lastLine = mLines[aEnd.mLine]; + + firstLine.erase(firstLine.begin() + start, firstLine.end()); + lastLine.erase(lastLine.begin(), lastLine.begin() + end); + + if (aStart.mLine < aEnd.mLine) + firstLine.insert(firstLine.end(), lastLine.begin(), lastLine.end()); + + if (aStart.mLine < aEnd.mLine) + RemoveLine(aStart.mLine + 1, aEnd.mLine + 1); + } + + mTextChanged = true; +} + +int TextEditor::InsertTextAt(Coordinates& /* inout */ aWhere, const char * aValue) +{ + assert(!mReadOnly); + + int cindex = GetCharacterIndex(aWhere); + int totalLines = 0; + while (*aValue != '\0') + { + assert(!mLines.empty()); + + if (*aValue == '\r') + { + // skip + ++aValue; + } + else if (*aValue == '\n') + { + if (cindex < (int)mLines[aWhere.mLine].size()) + { + auto& newLine = InsertLine(aWhere.mLine + 1); + auto& line = mLines[aWhere.mLine]; + newLine.insert(newLine.begin(), line.begin() + cindex, line.end()); + line.erase(line.begin() + cindex, line.end()); + } + else + { + InsertLine(aWhere.mLine + 1); + } + ++aWhere.mLine; + aWhere.mColumn = 0; + cindex = 0; + ++totalLines; + ++aValue; + } + else + { + auto& line = mLines[aWhere.mLine]; + auto d = UTF8CharLength(*aValue); + while (d-- > 0 && *aValue != '\0') + line.insert(line.begin() + cindex++, Glyph(*aValue++, PaletteIndex::Default)); + ++aWhere.mColumn; + } + + mTextChanged = true; + } + + return totalLines; +} + +void TextEditor::AddUndo(UndoRecord& aValue) +{ + assert(!mReadOnly); + //printf("AddUndo: (@%d.%d) +\'%s' [%d.%d .. %d.%d], -\'%s', [%d.%d .. %d.%d] (@%d.%d)\n", + // aValue.mBefore.mCursorPosition.mLine, aValue.mBefore.mCursorPosition.mColumn, + // aValue.mAdded.c_str(), aValue.mAddedStart.mLine, aValue.mAddedStart.mColumn, aValue.mAddedEnd.mLine, aValue.mAddedEnd.mColumn, + // aValue.mRemoved.c_str(), aValue.mRemovedStart.mLine, aValue.mRemovedStart.mColumn, aValue.mRemovedEnd.mLine, aValue.mRemovedEnd.mColumn, + // aValue.mAfter.mCursorPosition.mLine, aValue.mAfter.mCursorPosition.mColumn + // ); + + mUndoBuffer.resize((size_t)(mUndoIndex + 1)); + mUndoBuffer.back() = aValue; + ++mUndoIndex; +} + +TextEditor::Coordinates TextEditor::ScreenPosToCoordinates(const ImVec2& aPosition) const +{ + ImVec2 origin = ImGui::GetCursorScreenPos(); + ImVec2 local(aPosition.x - origin.x, aPosition.y - origin.y); + + int lineNo = std::max(0, (int)floor(local.y / mCharAdvance.y)); + + int columnCoord = 0; + + if (lineNo >= 0 && lineNo < (int)mLines.size()) + { + auto& line = mLines.at(lineNo); + + int columnIndex = 0; + float columnX = 0.0f; + + while ((size_t)columnIndex < line.size()) + { + float columnWidth = 0.0f; + + if (line[columnIndex].mChar == '\t') + { + float spaceSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ").x; + float oldX = columnX; + float newColumnX = (1.0f + std::floor((1.0f + columnX) / (float(mTabSize) * spaceSize))) * (float(mTabSize) * spaceSize); + columnWidth = newColumnX - oldX; + if (mTextStart + columnX + columnWidth * 0.5f > local.x) + break; + columnX = newColumnX; + columnCoord = (columnCoord / mTabSize) * mTabSize + mTabSize; + columnIndex++; + } + else + { + char buf[7]; + auto d = UTF8CharLength(line[columnIndex].mChar); + int i = 0; + while (i < 6 && d-- > 0) + buf[i++] = line[columnIndex++].mChar; + buf[i] = '\0'; + columnWidth = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf).x; + if (mTextStart + columnX + columnWidth * 0.5f > local.x) + break; + columnX += columnWidth; + columnCoord++; + } + } + } + + return SanitizeCoordinates(Coordinates(lineNo, columnCoord)); +} + +TextEditor::Coordinates TextEditor::FindWordStart(const Coordinates & aFrom) const +{ + Coordinates at = aFrom; + if (at.mLine >= (int)mLines.size()) + return at; + + auto& line = mLines[at.mLine]; + auto cindex = GetCharacterIndex(at); + + if (cindex >= (int)line.size()) + return at; + + while (cindex > 0 && isspace(line[cindex].mChar)) + --cindex; + + auto cstart = (PaletteIndex)line[cindex].mColorIndex; + while (cindex > 0) + { + auto c = line[cindex].mChar; + if ((c & 0xC0) != 0x80) // not UTF code sequence 10xxxxxx + { + if (c <= 32 && isspace(c)) + { + cindex++; + break; + } + if (cstart != (PaletteIndex)line[size_t(cindex - 1)].mColorIndex) + break; + } + --cindex; + } + return Coordinates(at.mLine, GetCharacterColumn(at.mLine, cindex)); +} + +TextEditor::Coordinates TextEditor::FindWordEnd(const Coordinates & aFrom) const +{ + Coordinates at = aFrom; + if (at.mLine >= (int)mLines.size()) + return at; + + auto& line = mLines[at.mLine]; + auto cindex = GetCharacterIndex(at); + + if (cindex >= (int)line.size()) + return at; + + bool prevspace = isspace(line[cindex].mChar) != 0; + auto cstart = (PaletteIndex)line[cindex].mColorIndex; + while (cindex < (int)line.size()) + { + auto c = line[cindex].mChar; + auto d = UTF8CharLength(c); + if (cstart != (PaletteIndex)line[cindex].mColorIndex) + break; + + if (prevspace != !!isspace(c)) + { + if (isspace(c)) + while (cindex < (int)line.size() && isspace(line[cindex].mChar)) + ++cindex; + break; + } + cindex += d; + } + return Coordinates(aFrom.mLine, GetCharacterColumn(aFrom.mLine, cindex)); +} + +TextEditor::Coordinates TextEditor::FindNextWord(const Coordinates & aFrom) const +{ + Coordinates at = aFrom; + if (at.mLine >= (int)mLines.size()) + return at; + + // skip to the next non-word character + auto cindex = GetCharacterIndex(aFrom); + bool isword = false; + bool skip = false; + if (cindex < (int)mLines[at.mLine].size()) + { + auto& line = mLines[at.mLine]; + isword = isalnum(line[cindex].mChar) != 0; + skip = isword; + } + + while (!isword || skip) + { + if (at.mLine >= mLines.size()) + { + auto l = std::max(0, (int) mLines.size() - 1); + return Coordinates(l, GetLineMaxColumn(l)); + } + + auto& line = mLines[at.mLine]; + if (cindex < (int)line.size()) + { + isword = isalnum(line[cindex].mChar) != 0; + + if (isword && !skip) + return Coordinates(at.mLine, GetCharacterColumn(at.mLine, cindex)); + + if (!isword) + skip = false; + + cindex++; + } + else + { + cindex = 0; + ++at.mLine; + skip = false; + isword = false; + } + } + + return at; +} + +int TextEditor::GetCharacterIndex(const Coordinates& aCoordinates) const +{ + if (aCoordinates.mLine >= mLines.size()) + return -1; + auto& line = mLines[aCoordinates.mLine]; + int c = 0; + int i = 0; + for (; i < line.size() && c < aCoordinates.mColumn;) + { + if (line[i].mChar == '\t') + c = (c / mTabSize) * mTabSize + mTabSize; + else + ++c; + i += UTF8CharLength(line[i].mChar); + } + return i; +} + +int TextEditor::GetCharacterColumn(int aLine, int aIndex) const +{ + if (aLine >= mLines.size()) + return 0; + auto& line = mLines[aLine]; + int col = 0; + int i = 0; + while (i < aIndex && i < (int)line.size()) + { + auto c = line[i].mChar; + i += UTF8CharLength(c); + if (c == '\t') + col = (col / mTabSize) * mTabSize + mTabSize; + else + col++; + } + return col; +} + +int TextEditor::GetLineCharacterCount(int aLine) const +{ + if (aLine >= mLines.size()) + return 0; + auto& line = mLines[aLine]; + int c = 0; + for (unsigned i = 0; i < line.size(); c++) + i += UTF8CharLength(line[i].mChar); + return c; +} + +int TextEditor::GetLineMaxColumn(int aLine) const +{ + if (aLine >= mLines.size()) + return 0; + auto& line = mLines[aLine]; + int col = 0; + for (unsigned i = 0; i < line.size(); ) + { + auto c = line[i].mChar; + if (c == '\t') + col = (col / mTabSize) * mTabSize + mTabSize; + else + col++; + i += UTF8CharLength(c); + } + return col; +} + +bool TextEditor::IsOnWordBoundary(const Coordinates & aAt) const +{ + if (aAt.mLine >= (int)mLines.size() || aAt.mColumn == 0) + return true; + + auto& line = mLines[aAt.mLine]; + auto cindex = GetCharacterIndex(aAt); + if (cindex >= (int)line.size()) + return true; + + if (mColorizerEnabled) + return line[cindex].mColorIndex != line[size_t(cindex - 1)].mColorIndex; + + return isspace(line[cindex].mChar) != isspace(line[cindex - 1].mChar); +} + +void TextEditor::RemoveLine(int aStart, int aEnd) +{ + assert(!mReadOnly); + assert(aEnd >= aStart); + assert(mLines.size() > (size_t)(aEnd - aStart)); + + ErrorMarkers etmp; + for (auto& i : mErrorMarkers) + { + ErrorMarkers::value_type e(i.first >= aStart ? i.first - 1 : i.first, i.second); + if (e.first >= aStart && e.first <= aEnd) + continue; + etmp.insert(e); + } + mErrorMarkers = std::move(etmp); + + Breakpoints btmp; + for (auto i : mBreakpoints) + { + if (i >= aStart && i <= aEnd) + continue; + btmp.insert(i >= aStart ? i - 1 : i); + } + mBreakpoints = std::move(btmp); + + mLines.erase(mLines.begin() + aStart, mLines.begin() + aEnd); + assert(!mLines.empty()); + + mTextChanged = true; +} + +void TextEditor::RemoveLine(int aIndex) +{ + assert(!mReadOnly); + assert(mLines.size() > 1); + + ErrorMarkers etmp; + for (auto& i : mErrorMarkers) + { + ErrorMarkers::value_type e(i.first > aIndex ? i.first - 1 : i.first, i.second); + if (e.first - 1 == aIndex) + continue; + etmp.insert(e); + } + mErrorMarkers = std::move(etmp); + + Breakpoints btmp; + for (auto i : mBreakpoints) + { + if (i == aIndex) + continue; + btmp.insert(i >= aIndex ? i - 1 : i); + } + mBreakpoints = std::move(btmp); + + mLines.erase(mLines.begin() + aIndex); + assert(!mLines.empty()); + + mTextChanged = true; +} + +TextEditor::Line& TextEditor::InsertLine(int aIndex) +{ + assert(!mReadOnly); + + auto& result = *mLines.insert(mLines.begin() + aIndex, Line()); + + ErrorMarkers etmp; + for (auto& i : mErrorMarkers) + etmp.insert(ErrorMarkers::value_type(i.first >= aIndex ? i.first + 1 : i.first, i.second)); + mErrorMarkers = std::move(etmp); + + Breakpoints btmp; + for (auto i : mBreakpoints) + btmp.insert(i >= aIndex ? i + 1 : i); + mBreakpoints = std::move(btmp); + + return result; +} + +std::string TextEditor::GetWordUnderCursor() const +{ + auto c = GetCursorPosition(); + return GetWordAt(c); +} + +std::string TextEditor::GetWordAt(const Coordinates & aCoords) const +{ + auto start = FindWordStart(aCoords); + auto end = FindWordEnd(aCoords); + + std::string r; + + auto istart = GetCharacterIndex(start); + auto iend = GetCharacterIndex(end); + + for (auto it = istart; it < iend; ++it) + r.push_back(mLines[aCoords.mLine][it].mChar); + + return r; +} + +ImU32 TextEditor::GetGlyphColor(const Glyph & aGlyph) const +{ + if (!mColorizerEnabled) + return mPalette[(int)PaletteIndex::Default]; + if (aGlyph.mComment) + return mPalette[(int)PaletteIndex::Comment]; + if (aGlyph.mMultiLineComment) + return mPalette[(int)PaletteIndex::MultiLineComment]; + auto const color = mPalette[(int)aGlyph.mColorIndex]; + if (aGlyph.mPreprocessor) + { + const auto ppcolor = mPalette[(int)PaletteIndex::Preprocessor]; + const int c0 = ((ppcolor & 0xff) + (color & 0xff)) / 2; + const int c1 = (((ppcolor >> 8) & 0xff) + ((color >> 8) & 0xff)) / 2; + const int c2 = (((ppcolor >> 16) & 0xff) + ((color >> 16) & 0xff)) / 2; + const int c3 = (((ppcolor >> 24) & 0xff) + ((color >> 24) & 0xff)) / 2; + return ImU32(c0 | (c1 << 8) | (c2 << 16) | (c3 << 24)); + } + return color; +} + +void TextEditor::HandleKeyboardInputs() +{ + ImGuiIO& io = ImGui::GetIO(); + auto shift = io.KeyShift; + auto ctrl = io.ConfigMacOSXBehaviors ? io.KeySuper : io.KeyCtrl; + auto alt = io.ConfigMacOSXBehaviors ? io.KeyCtrl : io.KeyAlt; + + if (ImGui::IsWindowFocused()) + { + if (ImGui::IsWindowHovered()) + ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput); + //ImGui::CaptureKeyboardFromApp(true); + + io.WantCaptureKeyboard = true; + io.WantTextInput = true; + + if (!IsReadOnly() && ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Z)) + Undo(); + else if (!IsReadOnly() && !ctrl && !shift && alt && ImGui::IsKeyPressed(ImGuiKey_Backspace)) + Undo(); + else if (!IsReadOnly() && ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Y)) + Redo(); + else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_UpArrow)) + MoveUp(1, shift); + else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_DownArrow)) + MoveDown(1, shift); + else if (!alt && ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) + MoveLeft(1, shift, ctrl); + else if (!alt && ImGui::IsKeyPressed(ImGuiKey_RightArrow)) + MoveRight(1, shift, ctrl); + else if (!alt && ImGui::IsKeyPressed(ImGuiKey_PageUp)) + MoveUp(GetPageSize() - 4, shift); + else if (!alt && ImGui::IsKeyPressed(ImGuiKey_PageDown)) + MoveDown(GetPageSize() - 4, shift); + else if (!alt && ctrl && ImGui::IsKeyPressed(ImGuiKey_Home)) + MoveTop(shift); + else if (ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_End)) + MoveBottom(shift); + else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_Home)) + MoveHome(shift); + else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_End)) + MoveEnd(shift); + else if (!IsReadOnly() && !ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Delete)) + Delete(); + else if (!IsReadOnly() && !ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Backspace)) + { + if (mSmartTabDelete) + { + auto cursor = GetActualCursorCoordinates(); + int col = cursor.mColumn; + int tabSize = std::max(1, mTabSize); + bool handled = false; + if (col > 0 && (col % tabSize) == 0) + { + auto& line = mLines[cursor.mLine]; + int startIndex = GetCharacterIndex(Coordinates(cursor.mLine, col - tabSize)); + int endIndex = GetCharacterIndex(cursor); + if (startIndex >= 0 && endIndex >= 0 && endIndex <= (int)line.size() && startIndex < endIndex) + { + bool allSpaces = true; + for (int i = startIndex; i < endIndex; ++i) + { + if (line[i].mChar != ' ') + { + allSpaces = false; + break; + } + } + if (allSpaces) + { + Coordinates start(cursor.mLine, col - tabSize); + Coordinates end(cursor.mLine, col); + DeleteRange(start, end); + mState.mCursorPosition = start; + mCursorPositionChanged = true; + EnsureCursorVisible(); + handled = true; + } + } + } + if (!handled) + Backspace(); + } + else + { + Backspace(); + } + } + else if (!ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Insert)) + mOverwrite ^= true; + else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Insert)) + Copy(); + else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_C)) + Copy(); + else if (!IsReadOnly() && !ctrl && shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Insert)) + Paste(); + else if (!IsReadOnly() && ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_V)) + Paste(); + else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_X)) + Cut(); + else if (!ctrl && shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Delete)) + Cut(); + else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_A)) + SelectAll(); + else if (!IsReadOnly() && !ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Enter)) + EnterCharacter('\n', false); + else if (mAllowTabInput && !IsReadOnly() && !ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_Tab)) + EnterCharacter('\t', shift); + + if (!IsReadOnly() && !io.InputQueueCharacters.empty()) + { + for (int i = 0; i < io.InputQueueCharacters.Size; i++) + { + auto c = io.InputQueueCharacters[i]; + if (c != 0 && (c == '\n' || c >= 32)) + EnterCharacter(c, shift); + } + io.InputQueueCharacters.resize(0); + } + } +} + +void TextEditor::HandleMouseInputs() +{ + ImGuiIO& io = ImGui::GetIO(); + auto shift = io.KeyShift; + auto ctrl = io.ConfigMacOSXBehaviors ? io.KeySuper : io.KeyCtrl; + auto alt = io.ConfigMacOSXBehaviors ? io.KeyCtrl : io.KeyAlt; + + if (ImGui::IsWindowHovered()) + { + if (!shift && !alt) + { + auto click = ImGui::IsMouseClicked(0); + auto doubleClick = ImGui::IsMouseDoubleClicked(0); + auto t = ImGui::GetTime(); + auto tripleClick = click && !doubleClick && (mLastClick != -1.0f && (t - mLastClick) < io.MouseDoubleClickTime); + + /* + Left mouse button triple click + */ + + if (tripleClick) + { + if (!ctrl) + { + mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); + mSelectionMode = SelectionMode::Line; + SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + } + + mLastClick = -1.0f; + } + + /* + Left mouse button double click + */ + + else if (doubleClick) + { + if (!ctrl) + { + mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); + if (mSelectionMode == SelectionMode::Line) + mSelectionMode = SelectionMode::Normal; + else + mSelectionMode = SelectionMode::Word; + SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + } + + mLastClick = (float)ImGui::GetTime(); + } + + /* + Left mouse button click + */ + else if (click) + { + mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); + if (ctrl) + mSelectionMode = SelectionMode::Word; + else + mSelectionMode = SelectionMode::Normal; + SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + + mLastClick = (float)ImGui::GetTime(); + } + // Mouse left button dragging (=> update selection) + else if (ImGui::IsMouseDragging(0) && ImGui::IsMouseDown(0)) + { + io.WantCaptureMouse = true; + mState.mCursorPosition = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); + SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + } + } + } +} + +void TextEditor::Render() +{ + /* Compute mCharAdvance regarding to scaled font size (Ctrl + mouse wheel)*/ + const float fontSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, "#", nullptr, nullptr).x; + mCharAdvance = ImVec2(fontSize, ImGui::GetTextLineHeightWithSpacing() * mLineSpacing); + + /* Update palette with the current alpha from style */ + for (int i = 0; i < (int)PaletteIndex::Max; ++i) + { + auto color = ImGui::ColorConvertU32ToFloat4(mPaletteBase[i]); + color.w *= ImGui::GetStyle().Alpha; + mPalette[i] = ImGui::ColorConvertFloat4ToU32(color); + } + + assert(mLineBuffer.empty()); + + auto contentSize = ImGui::GetWindowContentRegionMax(); + auto drawList = ImGui::GetWindowDrawList(); + float longest(mTextStart); + + if (mScrollToTop) + { + mScrollToTop = false; + ImGui::SetScrollY(0.f); + } + + ImVec2 cursorScreenPos = ImGui::GetCursorScreenPos(); + auto scrollX = ImGui::GetScrollX(); + auto scrollY = ImGui::GetScrollY(); + mCursorScreenPosValid = false; + + auto lineNo = (int)floor(scrollY / mCharAdvance.y); + auto globalLineMax = (int)mLines.size(); + auto lineMax = std::max(0, std::min((int)mLines.size() - 1, lineNo + (int)floor((scrollY + contentSize.y) / mCharAdvance.y))); + + // Deduce mTextStart by evaluating mLines size (global lineMax) plus two spaces as text width + char buf[16]; + snprintf(buf, 16, " %d ", globalLineMax); + mTextStart = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf, nullptr, nullptr).x + mLeftMargin; + + if (!mLines.empty()) + { + float spaceSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ", nullptr, nullptr).x; + + while (lineNo <= lineMax) + { + ImVec2 lineStartScreenPos = ImVec2(cursorScreenPos.x, cursorScreenPos.y + lineNo * mCharAdvance.y); + ImVec2 textScreenPos = ImVec2(lineStartScreenPos.x + mTextStart, lineStartScreenPos.y); + + auto& line = mLines[lineNo]; + longest = std::max(mTextStart + TextDistanceToLineStart(Coordinates(lineNo, GetLineMaxColumn(lineNo))), longest); + auto columnNo = 0; + Coordinates lineStartCoord(lineNo, 0); + Coordinates lineEndCoord(lineNo, GetLineMaxColumn(lineNo)); + + // Draw selection for the current line + float sstart = -1.0f; + float ssend = -1.0f; + + assert(mState.mSelectionStart <= mState.mSelectionEnd); + if (mState.mSelectionStart <= lineEndCoord) + sstart = mState.mSelectionStart > lineStartCoord ? TextDistanceToLineStart(mState.mSelectionStart) : 0.0f; + if (mState.mSelectionEnd > lineStartCoord) + ssend = TextDistanceToLineStart(mState.mSelectionEnd < lineEndCoord ? mState.mSelectionEnd : lineEndCoord); + + if (mState.mSelectionEnd.mLine > lineNo) + ssend += mCharAdvance.x; + + if (sstart != -1 && ssend != -1 && sstart < ssend) + { + ImVec2 vstart(lineStartScreenPos.x + mTextStart + sstart, lineStartScreenPos.y); + ImVec2 vend(lineStartScreenPos.x + mTextStart + ssend, lineStartScreenPos.y + mCharAdvance.y); + drawList->AddRectFilled(vstart, vend, mPalette[(int)PaletteIndex::Selection]); + } + + // Draw breakpoints + auto start = ImVec2(lineStartScreenPos.x + scrollX, lineStartScreenPos.y); + + if (mBreakpoints.count(lineNo + 1) != 0) + { + auto end = ImVec2(lineStartScreenPos.x + contentSize.x + 2.0f * scrollX, lineStartScreenPos.y + mCharAdvance.y); + drawList->AddRectFilled(start, end, mPalette[(int)PaletteIndex::Breakpoint]); + } + + // Draw error markers + auto errorIt = mErrorMarkers.find(lineNo + 1); + if (errorIt != mErrorMarkers.end()) + { + auto end = ImVec2(lineStartScreenPos.x + contentSize.x + 2.0f * scrollX, lineStartScreenPos.y + mCharAdvance.y); + drawList->AddRectFilled(start, end, mPalette[(int)PaletteIndex::ErrorMarker]); + + if (ImGui::IsMouseHoveringRect(lineStartScreenPos, end)) + { + ImGui::BeginTooltip(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.2f, 0.2f, 1.0f)); + ImGui::Text("Error at line %d:", errorIt->first); + ImGui::PopStyleColor(); + ImGui::Separator(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 0.2f, 1.0f)); + ImGui::Text("%s", errorIt->second.c_str()); + ImGui::PopStyleColor(); + ImGui::EndTooltip(); + } + } + + // Draw line number (right aligned) + snprintf(buf, 16, "%d ", lineNo + 1); + + auto lineNoWidth = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf, nullptr, nullptr).x; + drawList->AddText(ImVec2(lineStartScreenPos.x + mTextStart - lineNoWidth, lineStartScreenPos.y), mPalette[(int)PaletteIndex::LineNumber], buf); + + if (mState.mCursorPosition.mLine == lineNo) + { + auto focused = ImGui::IsWindowFocused(); + + // Highlight the current line (where the cursor is) + if (!HasSelection()) + { + auto end = ImVec2(start.x + contentSize.x + scrollX, start.y + mCharAdvance.y); + drawList->AddRectFilled(start, end, mPalette[(int)(focused ? PaletteIndex::CurrentLineFill : PaletteIndex::CurrentLineFillInactive)]); + drawList->AddRect(start, end, mPalette[(int)PaletteIndex::CurrentLineEdge], 1.0f); + } + + // Render the cursor + if (focused) + { + auto timeEnd = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + auto elapsed = timeEnd - mStartTime; + if (elapsed > 400) + { + float width = 1.0f; + auto cindex = GetCharacterIndex(mState.mCursorPosition); + float cx = TextDistanceToLineStart(mState.mCursorPosition); + + if (mOverwrite && cindex < (int)line.size()) + { + auto c = line[cindex].mChar; + if (c == '\t') + { + auto x = (1.0f + std::floor((1.0f + cx) / (float(mTabSize) * spaceSize))) * (float(mTabSize) * spaceSize); + width = x - cx; + } + else + { + char buf2[2]; + buf2[0] = line[cindex].mChar; + buf2[1] = '\0'; + width = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf2).x; + } + } + ImVec2 cstart(textScreenPos.x + cx, lineStartScreenPos.y); + ImVec2 cend(textScreenPos.x + cx + width, lineStartScreenPos.y + mCharAdvance.y); + drawList->AddRectFilled(cstart, cend, mPalette[(int)PaletteIndex::Cursor]); + mCursorScreenPos = cstart; + mCursorScreenPosValid = true; + if (elapsed > 800) + mStartTime = timeEnd; + } + } + if (!mCursorScreenPosValid) + { + float cx = TextDistanceToLineStart(mState.mCursorPosition); + mCursorScreenPos = ImVec2(textScreenPos.x + cx, lineStartScreenPos.y); + mCursorScreenPosValid = true; + } + } + + // Render colorized text + auto prevColor = line.empty() ? mPalette[(int)PaletteIndex::Default] : GetGlyphColor(line[0]); + ImVec2 bufferOffset; + + for (int i = 0; i < line.size();) + { + auto& glyph = line[i]; + auto color = GetGlyphColor(glyph); + + if ((color != prevColor || glyph.mChar == '\t' || glyph.mChar == ' ') && !mLineBuffer.empty()) + { + const ImVec2 newOffset(textScreenPos.x + bufferOffset.x, textScreenPos.y + bufferOffset.y); + drawList->AddText(newOffset, prevColor, mLineBuffer.c_str()); + auto textSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, mLineBuffer.c_str(), nullptr, nullptr); + bufferOffset.x += textSize.x; + mLineBuffer.clear(); + } + prevColor = color; + + if (glyph.mChar == '\t') + { + auto oldX = bufferOffset.x; + bufferOffset.x = (1.0f + std::floor((1.0f + bufferOffset.x) / (float(mTabSize) * spaceSize))) * (float(mTabSize) * spaceSize); + ++i; + + if (mShowWhitespaces) + { + const auto s = ImGui::GetFontSize(); + const auto x1 = textScreenPos.x + oldX + 1.0f; + const auto x2 = textScreenPos.x + bufferOffset.x - 1.0f; + const auto y = textScreenPos.y + bufferOffset.y + s * 0.5f; + const ImVec2 p1(x1, y); + const ImVec2 p2(x2, y); + const ImVec2 p3(x2 - s * 0.2f, y - s * 0.2f); + const ImVec2 p4(x2 - s * 0.2f, y + s * 0.2f); + drawList->AddLine(p1, p2, 0x90909090); + drawList->AddLine(p2, p3, 0x90909090); + drawList->AddLine(p2, p4, 0x90909090); + } + } + else if (glyph.mChar == ' ') + { + if (mShowWhitespaces) + { + const auto s = ImGui::GetFontSize(); + const auto x = textScreenPos.x + bufferOffset.x + spaceSize * 0.5f; + const auto y = textScreenPos.y + bufferOffset.y + s * 0.5f; + drawList->AddCircleFilled(ImVec2(x, y), 1.5f, 0x80808080, 4); + } + bufferOffset.x += spaceSize; + i++; + } + else + { + auto l = UTF8CharLength(glyph.mChar); + while (l-- > 0) + mLineBuffer.push_back(line[i++].mChar); + } + ++columnNo; + } + + if (!mLineBuffer.empty()) + { + const ImVec2 newOffset(textScreenPos.x + bufferOffset.x, textScreenPos.y + bufferOffset.y); + drawList->AddText(newOffset, prevColor, mLineBuffer.c_str()); + mLineBuffer.clear(); + } + + ++lineNo; + } + + // Draw a tooltip on known identifiers/preprocessor symbols + if (ImGui::IsMousePosValid()) + { + auto id = GetWordAt(ScreenPosToCoordinates(ImGui::GetMousePos())); + if (!id.empty()) + { + auto it = mLanguageDefinition.mIdentifiers.find(id); + if (it != mLanguageDefinition.mIdentifiers.end()) + { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(it->second.mDeclaration.c_str()); + ImGui::EndTooltip(); + } + else + { + auto pi = mLanguageDefinition.mPreprocIdentifiers.find(id); + if (pi != mLanguageDefinition.mPreprocIdentifiers.end()) + { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(pi->second.mDeclaration.c_str()); + ImGui::EndTooltip(); + } + } + } + } + } + + + ImGui::Dummy(ImVec2((longest + 2), mLines.size() * mCharAdvance.y)); + + if (mScrollToCursor) + { + EnsureCursorVisible(); + ImGui::SetWindowFocus(); + mScrollToCursor = false; + } +} + +void TextEditor::Render(const char* aTitle, const ImVec2& aSize, bool aBorder) +{ + mWithinRender = true; + mTextChanged = false; + mCursorPositionChanged = false; + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(mPalette[(int)PaletteIndex::Background])); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); + if (!mIgnoreImGuiChild) + ImGui::BeginChild(aTitle, aSize, aBorder, ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar | ImGuiWindowFlags_NoMove); + + if (mHandleKeyboardInputs) + { + HandleKeyboardInputs(); + ImGui::PushItemFlag(ImGuiItemFlags_NoTabStop, false); + } + + if (mHandleMouseInputs) + HandleMouseInputs(); + + ColorizeInternal(); + Render(); + + if (mHandleKeyboardInputs) + ImGui::PopItemFlag(); + + if (!mIgnoreImGuiChild) + ImGui::EndChild(); + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + + mWithinRender = false; +} + +void TextEditor::SetText(const std::string & aText) +{ + mLines.clear(); + mLines.emplace_back(Line()); + for (auto chr : aText) + { + if (chr == '\r') + { + // ignore the carriage return character + } + else if (chr == '\n') + mLines.emplace_back(Line()); + else + { + mLines.back().emplace_back(Glyph(chr, PaletteIndex::Default)); + } + } + + mTextChanged = true; + mScrollToTop = true; + + mUndoBuffer.clear(); + mUndoIndex = 0; + + Colorize(); +} + +void TextEditor::SetTextLines(const std::vector & aLines) +{ + mLines.clear(); + + if (aLines.empty()) + { + mLines.emplace_back(Line()); + } + else + { + mLines.resize(aLines.size()); + + for (size_t i = 0; i < aLines.size(); ++i) + { + const std::string & aLine = aLines[i]; + + mLines[i].reserve(aLine.size()); + for (size_t j = 0; j < aLine.size(); ++j) + mLines[i].emplace_back(Glyph(aLine[j], PaletteIndex::Default)); + } + } + + mTextChanged = true; + mScrollToTop = true; + + mUndoBuffer.clear(); + mUndoIndex = 0; + + Colorize(); +} + +void TextEditor::EnterCharacter(ImWchar aChar, bool aShift) +{ + assert(!mReadOnly); + + UndoRecord u; + + u.mBefore = mState; + + if (HasSelection()) + { + if (aChar == '\t' && mState.mSelectionStart.mLine != mState.mSelectionEnd.mLine) + { + + auto start = mState.mSelectionStart; + auto end = mState.mSelectionEnd; + auto originalEnd = end; + + if (start > end) + std::swap(start, end); + start.mColumn = 0; + // end.mColumn = end.mLine < mLines.size() ? mLines[end.mLine].size() : 0; + if (end.mColumn == 0 && end.mLine > 0) + --end.mLine; + if (end.mLine >= (int)mLines.size()) + end.mLine = mLines.empty() ? 0 : (int)mLines.size() - 1; + end.mColumn = GetLineMaxColumn(end.mLine); + + //if (end.mColumn >= GetLineMaxColumn(end.mLine)) + // end.mColumn = GetLineMaxColumn(end.mLine) - 1; + + u.mRemovedStart = start; + u.mRemovedEnd = end; + u.mRemoved = GetText(start, end); + + bool modified = false; + + for (int i = start.mLine; i <= end.mLine; i++) + { + auto& line = mLines[i]; + if (aShift) + { + if (!line.empty()) + { + if (line.front().mChar == '\t') + { + line.erase(line.begin()); + modified = true; + } + else + { + for (int j = 0; j < mTabSize && !line.empty() && line.front().mChar == ' '; j++) + { + line.erase(line.begin()); + modified = true; + } + } + } + } + else + { + line.insert(line.begin(), Glyph('\t', TextEditor::PaletteIndex::Background)); + modified = true; + } + } + + if (modified) + { + start = Coordinates(start.mLine, GetCharacterColumn(start.mLine, 0)); + Coordinates rangeEnd; + if (originalEnd.mColumn != 0) + { + end = Coordinates(end.mLine, GetLineMaxColumn(end.mLine)); + rangeEnd = end; + u.mAdded = GetText(start, end); + } + else + { + end = Coordinates(originalEnd.mLine, 0); + rangeEnd = Coordinates(end.mLine - 1, GetLineMaxColumn(end.mLine - 1)); + u.mAdded = GetText(start, rangeEnd); + } + + u.mAddedStart = start; + u.mAddedEnd = rangeEnd; + u.mAfter = mState; + + mState.mSelectionStart = start; + mState.mSelectionEnd = end; + AddUndo(u); + + mTextChanged = true; + + EnsureCursorVisible(); + } + + return; + } // c == '\t' + else + { + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + DeleteSelection(); + } + } // HasSelection + + auto coord = GetActualCursorCoordinates(); + u.mAddedStart = coord; + + assert(!mLines.empty()); + + if (aChar == '\n') + { + InsertLine(coord.mLine + 1); + auto& line = mLines[coord.mLine]; + auto& newLine = mLines[coord.mLine + 1]; + + if (mLanguageDefinition.mAutoIndentation) + for (size_t it = 0; it < line.size() && isascii(line[it].mChar) && isblank(line[it].mChar); ++it) + newLine.push_back(line[it]); + + const size_t whitespaceSize = newLine.size(); + auto cindex = GetCharacterIndex(coord); + newLine.insert(newLine.end(), line.begin() + cindex, line.end()); + line.erase(line.begin() + cindex, line.begin() + line.size()); + SetCursorPosition(Coordinates(coord.mLine + 1, GetCharacterColumn(coord.mLine + 1, (int)whitespaceSize))); + u.mAdded = (char)aChar; + } + else + { + char buf[7]; + int e = ImTextCharToUtf8(buf, 7, aChar); + if (e > 0) + { + buf[e] = '\0'; + auto& line = mLines[coord.mLine]; + auto cindex = GetCharacterIndex(coord); + + if (mOverwrite && cindex < (int)line.size()) + { + auto d = UTF8CharLength(line[cindex].mChar); + + u.mRemovedStart = mState.mCursorPosition; + u.mRemovedEnd = Coordinates(coord.mLine, GetCharacterColumn(coord.mLine, cindex + d)); + + while (d-- > 0 && cindex < (int)line.size()) + { + u.mRemoved += line[cindex].mChar; + line.erase(line.begin() + cindex); + } + } + + for (auto p = buf; *p != '\0'; p++, ++cindex) + line.insert(line.begin() + cindex, Glyph(*p, PaletteIndex::Default)); + u.mAdded = buf; + + SetCursorPosition(Coordinates(coord.mLine, GetCharacterColumn(coord.mLine, cindex))); + } + else + return; + } + + mTextChanged = true; + + u.mAddedEnd = GetActualCursorCoordinates(); + u.mAfter = mState; + + AddUndo(u); + + Colorize(coord.mLine - 1, 3); + EnsureCursorVisible(); +} + +void TextEditor::SetReadOnly(bool aValue) +{ + mReadOnly = aValue; +} + +void TextEditor::SetColorizerEnable(bool aValue) +{ + mColorizerEnabled = aValue; +} + +void TextEditor::SetCursorPosition(const Coordinates & aPosition) +{ + if (mState.mCursorPosition != aPosition) + { + mState.mCursorPosition = aPosition; + mCursorPositionChanged = true; + EnsureCursorVisible(); + } +} + +void TextEditor::SetSelectionStart(const Coordinates & aPosition) +{ + mState.mSelectionStart = SanitizeCoordinates(aPosition); + if (mState.mSelectionStart > mState.mSelectionEnd) + std::swap(mState.mSelectionStart, mState.mSelectionEnd); +} + +void TextEditor::SetSelectionEnd(const Coordinates & aPosition) +{ + mState.mSelectionEnd = SanitizeCoordinates(aPosition); + if (mState.mSelectionStart > mState.mSelectionEnd) + std::swap(mState.mSelectionStart, mState.mSelectionEnd); +} + +void TextEditor::SetSelection(const Coordinates & aStart, const Coordinates & aEnd, SelectionMode aMode) +{ + auto oldSelStart = mState.mSelectionStart; + auto oldSelEnd = mState.mSelectionEnd; + + mState.mSelectionStart = SanitizeCoordinates(aStart); + mState.mSelectionEnd = SanitizeCoordinates(aEnd); + if (mState.mSelectionStart > mState.mSelectionEnd) + std::swap(mState.mSelectionStart, mState.mSelectionEnd); + + switch (aMode) + { + case TextEditor::SelectionMode::Normal: + break; + case TextEditor::SelectionMode::Word: + { + mState.mSelectionStart = FindWordStart(mState.mSelectionStart); + if (!IsOnWordBoundary(mState.mSelectionEnd)) + mState.mSelectionEnd = FindWordEnd(FindWordStart(mState.mSelectionEnd)); + break; + } + case TextEditor::SelectionMode::Line: + { + const auto lineNo = mState.mSelectionEnd.mLine; + const auto lineSize = (size_t)lineNo < mLines.size() ? mLines[lineNo].size() : 0; + mState.mSelectionStart = Coordinates(mState.mSelectionStart.mLine, 0); + mState.mSelectionEnd = Coordinates(lineNo, GetLineMaxColumn(lineNo)); + break; + } + default: + break; + } + + if (mState.mSelectionStart != oldSelStart || + mState.mSelectionEnd != oldSelEnd) + mCursorPositionChanged = true; +} + +void TextEditor::SetTabSize(int aValue) +{ + mTabSize = std::max(0, std::min(32, aValue)); +} + +void TextEditor::InsertText(const std::string & aValue) +{ + InsertText(aValue.c_str()); +} + +void TextEditor::InsertText(const char * aValue) +{ + if (aValue == nullptr) + return; + + auto pos = GetActualCursorCoordinates(); + auto start = std::min(pos, mState.mSelectionStart); + int totalLines = pos.mLine - start.mLine; + + totalLines += InsertTextAt(pos, aValue); + + SetSelection(pos, pos); + SetCursorPosition(pos); + Colorize(start.mLine - 1, totalLines + 2); +} + +void TextEditor::DeleteSelection() +{ + assert(mState.mSelectionEnd >= mState.mSelectionStart); + + if (mState.mSelectionEnd == mState.mSelectionStart) + return; + + DeleteRange(mState.mSelectionStart, mState.mSelectionEnd); + + SetSelection(mState.mSelectionStart, mState.mSelectionStart); + SetCursorPosition(mState.mSelectionStart); + Colorize(mState.mSelectionStart.mLine, 1); +} + +void TextEditor::MoveUp(int aAmount, bool aSelect) +{ + auto oldPos = mState.mCursorPosition; + mState.mCursorPosition.mLine = std::max(0, mState.mCursorPosition.mLine - aAmount); + if (oldPos != mState.mCursorPosition) + { + if (aSelect) + { + if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else + { + mInteractiveStart = mState.mCursorPosition; + mInteractiveEnd = oldPos; + } + } + else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + + EnsureCursorVisible(); + } +} + +void TextEditor::MoveDown(int aAmount, bool aSelect) +{ + assert(mState.mCursorPosition.mColumn >= 0); + auto oldPos = mState.mCursorPosition; + mState.mCursorPosition.mLine = std::max(0, std::min((int)mLines.size() - 1, mState.mCursorPosition.mLine + aAmount)); + + if (mState.mCursorPosition != oldPos) + { + if (aSelect) + { + if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else + { + mInteractiveStart = oldPos; + mInteractiveEnd = mState.mCursorPosition; + } + } + else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + + EnsureCursorVisible(); + } +} + +static bool IsUTFSequence(char c) +{ + return (c & 0xC0) == 0x80; +} + +void TextEditor::MoveLeft(int aAmount, bool aSelect, bool aWordMode) +{ + if (mLines.empty()) + return; + + auto oldPos = mState.mCursorPosition; + mState.mCursorPosition = GetActualCursorCoordinates(); + auto line = mState.mCursorPosition.mLine; + auto cindex = GetCharacterIndex(mState.mCursorPosition); + + while (aAmount-- > 0) + { + if (cindex == 0) + { + if (line > 0) + { + --line; + if ((int)mLines.size() > line) + cindex = (int)mLines[line].size(); + else + cindex = 0; + } + } + else + { + --cindex; + if (cindex > 0) + { + if ((int)mLines.size() > line) + { + while (cindex > 0 && IsUTFSequence(mLines[line][cindex].mChar)) + --cindex; + } + } + } + + mState.mCursorPosition = Coordinates(line, GetCharacterColumn(line, cindex)); + if (aWordMode) + { + mState.mCursorPosition = FindWordStart(mState.mCursorPosition); + cindex = GetCharacterIndex(mState.mCursorPosition); + } + } + + mState.mCursorPosition = Coordinates(line, GetCharacterColumn(line, cindex)); + + assert(mState.mCursorPosition.mColumn >= 0); + if (aSelect) + { + if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else + { + mInteractiveStart = mState.mCursorPosition; + mInteractiveEnd = oldPos; + } + } + else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd, aSelect && aWordMode ? SelectionMode::Word : SelectionMode::Normal); + + EnsureCursorVisible(); +} + +void TextEditor::MoveRight(int aAmount, bool aSelect, bool aWordMode) +{ + auto oldPos = mState.mCursorPosition; + + if (mLines.empty() || oldPos.mLine >= mLines.size()) + return; + + auto cindex = GetCharacterIndex(mState.mCursorPosition); + while (aAmount-- > 0) + { + auto lindex = mState.mCursorPosition.mLine; + auto& line = mLines[lindex]; + + if (cindex >= line.size()) + { + if (mState.mCursorPosition.mLine < mLines.size() - 1) + { + mState.mCursorPosition.mLine = std::max(0, std::min((int)mLines.size() - 1, mState.mCursorPosition.mLine + 1)); + mState.mCursorPosition.mColumn = 0; + } + else + return; + } + else + { + cindex += UTF8CharLength(line[cindex].mChar); + mState.mCursorPosition = Coordinates(lindex, GetCharacterColumn(lindex, cindex)); + if (aWordMode) + mState.mCursorPosition = FindNextWord(mState.mCursorPosition); + } + } + + if (aSelect) + { + if (oldPos == mInteractiveEnd) + mInteractiveEnd = SanitizeCoordinates(mState.mCursorPosition); + else if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else + { + mInteractiveStart = oldPos; + mInteractiveEnd = mState.mCursorPosition; + } + } + else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd, aSelect && aWordMode ? SelectionMode::Word : SelectionMode::Normal); + + EnsureCursorVisible(); +} + +void TextEditor::MoveTop(bool aSelect) +{ + auto oldPos = mState.mCursorPosition; + SetCursorPosition(Coordinates(0, 0)); + + if (mState.mCursorPosition != oldPos) + { + if (aSelect) + { + mInteractiveEnd = oldPos; + mInteractiveStart = mState.mCursorPosition; + } + else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + } +} + +void TextEditor::TextEditor::MoveBottom(bool aSelect) +{ + auto oldPos = GetCursorPosition(); + auto newPos = Coordinates((int)mLines.size() - 1, 0); + SetCursorPosition(newPos); + if (aSelect) + { + mInteractiveStart = oldPos; + mInteractiveEnd = newPos; + } + else + mInteractiveStart = mInteractiveEnd = newPos; + SetSelection(mInteractiveStart, mInteractiveEnd); +} + +void TextEditor::MoveHome(bool aSelect) +{ + auto oldPos = mState.mCursorPosition; + SetCursorPosition(Coordinates(mState.mCursorPosition.mLine, 0)); + + if (mState.mCursorPosition != oldPos) + { + if (aSelect) + { + if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else + { + mInteractiveStart = mState.mCursorPosition; + mInteractiveEnd = oldPos; + } + } + else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + } +} + +void TextEditor::MoveEnd(bool aSelect) +{ + auto oldPos = mState.mCursorPosition; + SetCursorPosition(Coordinates(mState.mCursorPosition.mLine, GetLineMaxColumn(oldPos.mLine))); + + if (mState.mCursorPosition != oldPos) + { + if (aSelect) + { + if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else + { + mInteractiveStart = oldPos; + mInteractiveEnd = mState.mCursorPosition; + } + } + else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + } +} + +void TextEditor::Delete() +{ + assert(!mReadOnly); + + if (mLines.empty()) + return; + + UndoRecord u; + u.mBefore = mState; + + if (HasSelection()) + { + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + + DeleteSelection(); + } + else + { + auto pos = GetActualCursorCoordinates(); + SetCursorPosition(pos); + auto& line = mLines[pos.mLine]; + + if (pos.mColumn == GetLineMaxColumn(pos.mLine)) + { + if (pos.mLine == (int)mLines.size() - 1) + return; + + u.mRemoved = '\n'; + u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates(); + Advance(u.mRemovedEnd); + + auto& nextLine = mLines[pos.mLine + 1]; + line.insert(line.end(), nextLine.begin(), nextLine.end()); + RemoveLine(pos.mLine + 1); + } + else + { + auto cindex = GetCharacterIndex(pos); + u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates(); + u.mRemovedEnd.mColumn++; + u.mRemoved = GetText(u.mRemovedStart, u.mRemovedEnd); + + auto d = UTF8CharLength(line[cindex].mChar); + while (d-- > 0 && cindex < (int)line.size()) + line.erase(line.begin() + cindex); + } + + mTextChanged = true; + + Colorize(pos.mLine, 1); + } + + u.mAfter = mState; + AddUndo(u); +} + +void TextEditor::Backspace() +{ + assert(!mReadOnly); + + if (mLines.empty()) + return; + + UndoRecord u; + u.mBefore = mState; + + if (HasSelection()) + { + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + + DeleteSelection(); + } + else + { + auto pos = GetActualCursorCoordinates(); + SetCursorPosition(pos); + + if (mState.mCursorPosition.mColumn == 0) + { + if (mState.mCursorPosition.mLine == 0) + return; + + u.mRemoved = '\n'; + u.mRemovedStart = u.mRemovedEnd = Coordinates(pos.mLine - 1, GetLineMaxColumn(pos.mLine - 1)); + Advance(u.mRemovedEnd); + + auto& line = mLines[mState.mCursorPosition.mLine]; + auto& prevLine = mLines[mState.mCursorPosition.mLine - 1]; + auto prevSize = GetLineMaxColumn(mState.mCursorPosition.mLine - 1); + prevLine.insert(prevLine.end(), line.begin(), line.end()); + + ErrorMarkers etmp; + for (auto& i : mErrorMarkers) + etmp.insert(ErrorMarkers::value_type(i.first - 1 == mState.mCursorPosition.mLine ? i.first - 1 : i.first, i.second)); + mErrorMarkers = std::move(etmp); + + RemoveLine(mState.mCursorPosition.mLine); + --mState.mCursorPosition.mLine; + mState.mCursorPosition.mColumn = prevSize; + } + else + { + auto& line = mLines[mState.mCursorPosition.mLine]; + auto cindex = GetCharacterIndex(pos) - 1; + auto cend = cindex + 1; + while (cindex > 0 && IsUTFSequence(line[cindex].mChar)) + --cindex; + + //if (cindex > 0 && UTF8CharLength(line[cindex].mChar) > 1) + // --cindex; + + u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates(); + --u.mRemovedStart.mColumn; + --mState.mCursorPosition.mColumn; + + while (cindex < line.size() && cend-- > cindex) + { + u.mRemoved += line[cindex].mChar; + line.erase(line.begin() + cindex); + } + } + + mTextChanged = true; + + EnsureCursorVisible(); + Colorize(mState.mCursorPosition.mLine, 1); + } + + u.mAfter = mState; + AddUndo(u); +} + +void TextEditor::SelectWordUnderCursor() +{ + auto c = GetCursorPosition(); + SetSelection(FindWordStart(c), FindWordEnd(c)); +} + +void TextEditor::SelectAll() +{ + SetSelection(Coordinates(0, 0), Coordinates((int)mLines.size(), 0)); +} + +bool TextEditor::HasSelection() const +{ + return mState.mSelectionEnd > mState.mSelectionStart; +} + +void TextEditor::Copy() +{ + if (HasSelection()) + { + ImGui::SetClipboardText(GetSelectedText().c_str()); + } + else + { + if (!mLines.empty()) + { + std::string str; + auto& line = mLines[GetActualCursorCoordinates().mLine]; + for (auto& g : line) + str.push_back(g.mChar); + ImGui::SetClipboardText(str.c_str()); + } + } +} + +void TextEditor::Cut() +{ + if (IsReadOnly()) + { + Copy(); + } + else + { + if (HasSelection()) + { + UndoRecord u; + u.mBefore = mState; + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + + Copy(); + DeleteSelection(); + + u.mAfter = mState; + AddUndo(u); + } + } +} + +void TextEditor::Paste() +{ + if (IsReadOnly()) + return; + + auto clipText = ImGui::GetClipboardText(); + if (clipText != nullptr && strlen(clipText) > 0) + { + UndoRecord u; + u.mBefore = mState; + + if (HasSelection()) + { + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + DeleteSelection(); + } + + u.mAdded = clipText; + u.mAddedStart = GetActualCursorCoordinates(); + + InsertText(clipText); + + u.mAddedEnd = GetActualCursorCoordinates(); + u.mAfter = mState; + AddUndo(u); + } +} + +bool TextEditor::CanUndo() const +{ + return !mReadOnly && mUndoIndex > 0; +} + +bool TextEditor::CanRedo() const +{ + return !mReadOnly && mUndoIndex < (int)mUndoBuffer.size(); +} + +void TextEditor::Undo(int aSteps) +{ + while (CanUndo() && aSteps-- > 0) + mUndoBuffer[--mUndoIndex].Undo(this); +} + +void TextEditor::Redo(int aSteps) +{ + while (CanRedo() && aSteps-- > 0) + mUndoBuffer[mUndoIndex++].Redo(this); +} + +const TextEditor::Palette & TextEditor::GetDarkPalette() +{ + const static Palette p = { { + 0xff7f7f7f, // Default + 0xffd69c56, // Keyword + 0xff00ff00, // Number + 0xff7070e0, // String + 0xff70a0e0, // Char literal + 0xffffffff, // Punctuation + 0xff408080, // Preprocessor + 0xffaaaaaa, // Identifier + 0xff9bc64d, // Known identifier + 0xffc040a0, // Preproc identifier + 0xff206020, // Comment (single line) + 0xff406020, // Comment (multi line) + 0xff101010, // Background + 0xffe0e0e0, // Cursor + 0x80a06020, // Selection + 0x800020ff, // ErrorMarker + 0x40f08000, // Breakpoint + 0xff707000, // Line number + 0x40000000, // Current line fill + 0x40808080, // Current line fill (inactive) + 0x40a0a0a0, // Current line edge + } }; + return p; +} + +const TextEditor::Palette & TextEditor::GetLightPalette() +{ + const static Palette p = { { + 0xff7f7f7f, // None + 0xffff0c06, // Keyword + 0xff008000, // Number + 0xff2020a0, // String + 0xff304070, // Char literal + 0xff000000, // Punctuation + 0xff406060, // Preprocessor + 0xff404040, // Identifier + 0xff606010, // Known identifier + 0xffc040a0, // Preproc identifier + 0xff205020, // Comment (single line) + 0xff405020, // Comment (multi line) + 0xffffffff, // Background + 0xff000000, // Cursor + 0x80600000, // Selection + 0xa00010ff, // ErrorMarker + 0x80f08000, // Breakpoint + 0xff505000, // Line number + 0x40000000, // Current line fill + 0x40808080, // Current line fill (inactive) + 0x40000000, // Current line edge + } }; + return p; +} + +const TextEditor::Palette & TextEditor::GetRetroBluePalette() +{ + const static Palette p = { { + 0xff00ffff, // None + 0xffffff00, // Keyword + 0xff00ff00, // Number + 0xff808000, // String + 0xff808000, // Char literal + 0xffffffff, // Punctuation + 0xff008000, // Preprocessor + 0xff00ffff, // Identifier + 0xffffffff, // Known identifier + 0xffff00ff, // Preproc identifier + 0xff808080, // Comment (single line) + 0xff404040, // Comment (multi line) + 0xff800000, // Background + 0xff0080ff, // Cursor + 0x80ffff00, // Selection + 0xa00000ff, // ErrorMarker + 0x80ff8000, // Breakpoint + 0xff808000, // Line number + 0x40000000, // Current line fill + 0x40808080, // Current line fill (inactive) + 0x40000000, // Current line edge + } }; + return p; +} + + +std::string TextEditor::GetText() const +{ + return GetText(Coordinates(), Coordinates((int)mLines.size(), 0)); +} + +std::vector TextEditor::GetTextLines() const +{ + std::vector result; + + result.reserve(mLines.size()); + + for (auto & line : mLines) + { + std::string text; + + text.resize(line.size()); + + for (size_t i = 0; i < line.size(); ++i) + text[i] = line[i].mChar; + + result.emplace_back(std::move(text)); + } + + return result; +} + +std::string TextEditor::GetSelectedText() const +{ + return GetText(mState.mSelectionStart, mState.mSelectionEnd); +} + +std::string TextEditor::GetCurrentLineText()const +{ + auto lineLength = GetLineMaxColumn(mState.mCursorPosition.mLine); + return GetText( + Coordinates(mState.mCursorPosition.mLine, 0), + Coordinates(mState.mCursorPosition.mLine, lineLength)); +} + +void TextEditor::ProcessInputs() +{ +} + +void TextEditor::Colorize(int aFromLine, int aLines) +{ + int toLine = aLines == -1 ? (int)mLines.size() : std::min((int)mLines.size(), aFromLine + aLines); + mColorRangeMin = std::min(mColorRangeMin, aFromLine); + mColorRangeMax = std::max(mColorRangeMax, toLine); + mColorRangeMin = std::max(0, mColorRangeMin); + mColorRangeMax = std::max(mColorRangeMin, mColorRangeMax); + mCheckComments = true; +} + +void TextEditor::ColorizeRange(int aFromLine, int aToLine) +{ + if (mLines.empty() || aFromLine >= aToLine) + return; + + std::string buffer; + std::cmatch results; + std::string id; + + int endLine = std::max(0, std::min((int)mLines.size(), aToLine)); + for (int i = aFromLine; i < endLine; ++i) + { + auto& line = mLines[i]; + + if (line.empty()) + continue; + + buffer.resize(line.size()); + for (size_t j = 0; j < line.size(); ++j) + { + auto& col = line[j]; + buffer[j] = col.mChar; + col.mColorIndex = PaletteIndex::Default; + } + + const char * bufferBegin = &buffer.front(); + const char * bufferEnd = bufferBegin + buffer.size(); + + auto last = bufferEnd; + + for (auto first = bufferBegin; first != last; ) + { + const char * token_begin = nullptr; + const char * token_end = nullptr; + PaletteIndex token_color = PaletteIndex::Default; + + bool hasTokenizeResult = false; + + if (mLanguageDefinition.mTokenize != nullptr) + { + if (mLanguageDefinition.mTokenize(first, last, token_begin, token_end, token_color)) + hasTokenizeResult = true; + } + + if (hasTokenizeResult == false) + { + // todo : remove + //printf("using regex for %.*s\n", first + 10 < last ? 10 : int(last - first), first); + + for (auto& p : mRegexList) + { + if (std::regex_search(first, last, results, p.first, std::regex_constants::match_continuous)) + { + hasTokenizeResult = true; + + auto& v = *results.begin(); + token_begin = v.first; + token_end = v.second; + token_color = p.second; + break; + } + } + } + + if (hasTokenizeResult == false) + { + first++; + } + else + { + const size_t token_length = token_end - token_begin; + + if (token_color == PaletteIndex::Identifier) + { + id.assign(token_begin, token_end); + + // todo : allmost all language definitions use lower case to specify keywords, so shouldn't this use ::tolower ? + if (!mLanguageDefinition.mCaseSensitive) + std::transform(id.begin(), id.end(), id.begin(), ::toupper); + + if (!line[first - bufferBegin].mPreprocessor) + { + if (mLanguageDefinition.mKeywords.count(id) != 0) + token_color = PaletteIndex::Keyword; + else if (mLanguageDefinition.mIdentifiers.count(id) != 0) + token_color = PaletteIndex::KnownIdentifier; + else if (mLanguageDefinition.mPreprocIdentifiers.count(id) != 0) + token_color = PaletteIndex::PreprocIdentifier; + } + else + { + if (mLanguageDefinition.mPreprocIdentifiers.count(id) != 0) + token_color = PaletteIndex::PreprocIdentifier; + } + } + + for (size_t j = 0; j < token_length; ++j) + line[(token_begin - bufferBegin) + j].mColorIndex = token_color; + + first = token_end; + } + } + } +} + +void TextEditor::ColorizeInternal() +{ + if (mLines.empty() || !mColorizerEnabled) + return; + + if (mCheckComments) + { + auto endLine = mLines.size(); + auto endIndex = 0; + auto commentStartLine = endLine; + auto commentStartIndex = endIndex; + auto withinString = false; + auto withinSingleLineComment = false; + auto withinPreproc = false; + auto firstChar = true; // there is no other non-whitespace characters in the line before + auto concatenate = false; // '\' on the very end of the line + auto currentLine = 0; + auto currentIndex = 0; + while (currentLine < endLine || currentIndex < endIndex) + { + auto& line = mLines[currentLine]; + + if (currentIndex == 0 && !concatenate) + { + withinSingleLineComment = false; + withinPreproc = false; + firstChar = true; + } + + concatenate = false; + + if (!line.empty()) + { + auto& g = line[currentIndex]; + auto c = g.mChar; + + if (c != mLanguageDefinition.mPreprocChar && !isspace(c)) + firstChar = false; + + if (currentIndex == (int)line.size() - 1 && line[line.size() - 1].mChar == '\\') + concatenate = true; + + bool inComment = (commentStartLine < currentLine || (commentStartLine == currentLine && commentStartIndex <= currentIndex)); + + if (withinString) + { + line[currentIndex].mMultiLineComment = inComment; + + if (c == '\"') + { + if (currentIndex + 1 < (int)line.size() && line[currentIndex + 1].mChar == '\"') + { + currentIndex += 1; + if (currentIndex < (int)line.size()) + line[currentIndex].mMultiLineComment = inComment; + } + else + withinString = false; + } + else if (c == '\\') + { + currentIndex += 1; + if (currentIndex < (int)line.size()) + line[currentIndex].mMultiLineComment = inComment; + } + } + else + { + if (firstChar && c == mLanguageDefinition.mPreprocChar) + withinPreproc = true; + + if (c == '\"') + { + withinString = true; + line[currentIndex].mMultiLineComment = inComment; + } + else + { + auto pred = [](const char& a, const Glyph& b) { return a == b.mChar; }; + auto from = line.begin() + currentIndex; + auto& startStr = mLanguageDefinition.mCommentStart; + auto& singleStartStr = mLanguageDefinition.mSingleLineComment; + + if (singleStartStr.size() > 0 && + currentIndex + singleStartStr.size() <= line.size() && + equals(singleStartStr.begin(), singleStartStr.end(), from, from + singleStartStr.size(), pred)) + { + withinSingleLineComment = true; + } + else if (!withinSingleLineComment && currentIndex + startStr.size() <= line.size() && + equals(startStr.begin(), startStr.end(), from, from + startStr.size(), pred)) + { + commentStartLine = currentLine; + commentStartIndex = currentIndex; + } + + inComment = inComment = (commentStartLine < currentLine || (commentStartLine == currentLine && commentStartIndex <= currentIndex)); + + line[currentIndex].mMultiLineComment = inComment; + line[currentIndex].mComment = withinSingleLineComment; + + auto& endStr = mLanguageDefinition.mCommentEnd; + if (currentIndex + 1 >= (int)endStr.size() && + equals(endStr.begin(), endStr.end(), from + 1 - endStr.size(), from + 1, pred)) + { + commentStartIndex = endIndex; + commentStartLine = endLine; + } + } + } + line[currentIndex].mPreprocessor = withinPreproc; + currentIndex += UTF8CharLength(c); + if (currentIndex >= (int)line.size()) + { + currentIndex = 0; + ++currentLine; + } + } + else + { + currentIndex = 0; + ++currentLine; + } + } + mCheckComments = false; + } + + if (mColorRangeMin < mColorRangeMax) + { + const int increment = (mLanguageDefinition.mTokenize == nullptr) ? 10 : 10000; + const int to = std::min(mColorRangeMin + increment, mColorRangeMax); + ColorizeRange(mColorRangeMin, to); + mColorRangeMin = to; + + if (mColorRangeMax == mColorRangeMin) + { + mColorRangeMin = std::numeric_limits::max(); + mColorRangeMax = 0; + } + return; + } +} + +float TextEditor::TextDistanceToLineStart(const Coordinates& aFrom) const +{ + auto& line = mLines[aFrom.mLine]; + float distance = 0.0f; + float spaceSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ", nullptr, nullptr).x; + int colIndex = GetCharacterIndex(aFrom); + for (size_t it = 0u; it < line.size() && it < colIndex; ) + { + if (line[it].mChar == '\t') + { + distance = (1.0f + std::floor((1.0f + distance) / (float(mTabSize) * spaceSize))) * (float(mTabSize) * spaceSize); + ++it; + } + else + { + auto d = UTF8CharLength(line[it].mChar); + char tempCString[7]; + int i = 0; + for (; i < 6 && d-- > 0 && it < (int)line.size(); i++, it++) + tempCString[i] = line[it].mChar; + + tempCString[i] = '\0'; + distance += ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, tempCString, nullptr, nullptr).x; + } + } + + return distance; +} + +void TextEditor::EnsureCursorVisible() +{ + if (!mWithinRender) + { + mScrollToCursor = true; + return; + } + + float scrollX = ImGui::GetScrollX(); + float scrollY = ImGui::GetScrollY(); + + auto height = ImGui::GetWindowHeight(); + auto width = ImGui::GetWindowWidth(); + + auto top = 1 + (int)ceil(scrollY / mCharAdvance.y); + auto bottom = (int)ceil((scrollY + height) / mCharAdvance.y); + + auto left = (int)ceil(scrollX / mCharAdvance.x); + auto right = (int)ceil((scrollX + width) / mCharAdvance.x); + + auto pos = GetActualCursorCoordinates(); + auto len = TextDistanceToLineStart(pos); + + if (pos.mLine < top) + ImGui::SetScrollY(std::max(0.0f, (pos.mLine - 1) * mCharAdvance.y)); + if (pos.mLine > bottom - 4) + ImGui::SetScrollY(std::max(0.0f, (pos.mLine + 4) * mCharAdvance.y - height)); + if (len + mTextStart < left + 4) + ImGui::SetScrollX(std::max(0.0f, len + mTextStart - 4)); + if (len + mTextStart > right - 4) + ImGui::SetScrollX(std::max(0.0f, len + mTextStart + 4 - width)); +} + +int TextEditor::GetPageSize() const +{ + auto height = ImGui::GetWindowHeight() - 20.0f; + return (int)floor(height / mCharAdvance.y); +} + +TextEditor::UndoRecord::UndoRecord( + const std::string& aAdded, + const TextEditor::Coordinates aAddedStart, + const TextEditor::Coordinates aAddedEnd, + const std::string& aRemoved, + const TextEditor::Coordinates aRemovedStart, + const TextEditor::Coordinates aRemovedEnd, + TextEditor::EditorState& aBefore, + TextEditor::EditorState& aAfter) + : mAdded(aAdded) + , mAddedStart(aAddedStart) + , mAddedEnd(aAddedEnd) + , mRemoved(aRemoved) + , mRemovedStart(aRemovedStart) + , mRemovedEnd(aRemovedEnd) + , mBefore(aBefore) + , mAfter(aAfter) +{ + assert(mAddedStart <= mAddedEnd); + assert(mRemovedStart <= mRemovedEnd); +} + +void TextEditor::UndoRecord::Undo(TextEditor * aEditor) +{ + if (!mAdded.empty()) + { + aEditor->DeleteRange(mAddedStart, mAddedEnd); + aEditor->Colorize(mAddedStart.mLine - 1, mAddedEnd.mLine - mAddedStart.mLine + 2); + } + + if (!mRemoved.empty()) + { + auto start = mRemovedStart; + aEditor->InsertTextAt(start, mRemoved.c_str()); + aEditor->Colorize(mRemovedStart.mLine - 1, mRemovedEnd.mLine - mRemovedStart.mLine + 2); + } + + aEditor->mState = mBefore; + aEditor->EnsureCursorVisible(); + +} + +void TextEditor::UndoRecord::Redo(TextEditor * aEditor) +{ + if (!mRemoved.empty()) + { + aEditor->DeleteRange(mRemovedStart, mRemovedEnd); + aEditor->Colorize(mRemovedStart.mLine - 1, mRemovedEnd.mLine - mRemovedStart.mLine + 1); + } + + if (!mAdded.empty()) + { + auto start = mAddedStart; + aEditor->InsertTextAt(start, mAdded.c_str()); + aEditor->Colorize(mAddedStart.mLine - 1, mAddedEnd.mLine - mAddedStart.mLine + 1); + } + + aEditor->mState = mAfter; + aEditor->EnsureCursorVisible(); +} + +static bool TokenizeCStyleString(const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end) +{ + const char * p = in_begin; + + if (*p == '"') + { + p++; + + while (p < in_end) + { + // handle end of string + if (*p == '"') + { + out_begin = in_begin; + out_end = p + 1; + return true; + } + + // handle escape character for " + if (*p == '\\' && p + 1 < in_end && p[1] == '"') + p++; + + p++; + } + } + + return false; +} + +static bool TokenizeCStyleCharacterLiteral(const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end) +{ + const char * p = in_begin; + + if (*p == '\'') + { + p++; + + // handle escape characters + if (p < in_end && *p == '\\') + p++; + + if (p < in_end) + p++; + + // handle end of character literal + if (p < in_end && *p == '\'') + { + out_begin = in_begin; + out_end = p + 1; + return true; + } + } + + return false; +} + +static bool TokenizeCStyleIdentifier(const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end) +{ + const char * p = in_begin; + + if ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || *p == '_') + { + p++; + + while ((p < in_end) && ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || *p == '_')) + p++; + + out_begin = in_begin; + out_end = p; + return true; + } + + return false; +} + +static bool TokenizeCStyleNumber(const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end) +{ + const char * p = in_begin; + + const bool startsWithNumber = *p >= '0' && *p <= '9'; + + if (*p != '+' && *p != '-' && !startsWithNumber) + return false; + + p++; + + bool hasNumber = startsWithNumber; + + while (p < in_end && (*p >= '0' && *p <= '9')) + { + hasNumber = true; + + p++; + } + + if (hasNumber == false) + return false; + + bool isFloat = false; + bool isHex = false; + bool isBinary = false; + + if (p < in_end) + { + if (*p == '.') + { + isFloat = true; + + p++; + + while (p < in_end && (*p >= '0' && *p <= '9')) + p++; + } + else if (*p == 'x' || *p == 'X') + { + // hex formatted integer of the type 0xef80 + + isHex = true; + + p++; + + while (p < in_end && ((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f') || (*p >= 'A' && *p <= 'F'))) + p++; + } + else if (*p == 'b' || *p == 'B') + { + // binary formatted integer of the type 0b01011101 + + isBinary = true; + + p++; + + while (p < in_end && (*p >= '0' && *p <= '1')) + p++; + } + } + + if (isHex == false && isBinary == false) + { + // floating point exponent + if (p < in_end && (*p == 'e' || *p == 'E')) + { + isFloat = true; + + p++; + + if (p < in_end && (*p == '+' || *p == '-')) + p++; + + bool hasDigits = false; + + while (p < in_end && (*p >= '0' && *p <= '9')) + { + hasDigits = true; + + p++; + } + + if (hasDigits == false) + return false; + } + + // single precision floating point type + if (p < in_end && *p == 'f') + p++; + } + + if (isFloat == false) + { + // integer size type + while (p < in_end && (*p == 'u' || *p == 'U' || *p == 'l' || *p == 'L')) + p++; + } + + out_begin = in_begin; + out_end = p; + return true; +} + +static bool TokenizeCStylePunctuation(const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end) +{ + (void)in_end; + + switch (*in_begin) + { + case '[': + case ']': + case '{': + case '}': + case '!': + case '%': + case '^': + case '&': + case '*': + case '(': + case ')': + case '-': + case '+': + case '=': + case '~': + case '|': + case '<': + case '>': + case '?': + case ':': + case '/': + case ';': + case ',': + case '.': + out_begin = in_begin; + out_end = in_begin + 1; + return true; + } + + return false; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::CPlusPlus() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const cppKeywords[] = { + "alignas", "alignof", "and", "and_eq", "asm", "atomic_cancel", "atomic_commit", "atomic_noexcept", "auto", "bitand", "bitor", "bool", "break", "case", "catch", "char", "char16_t", "char32_t", "class", + "compl", "concept", "const", "constexpr", "const_cast", "continue", "decltype", "default", "delete", "do", "double", "dynamic_cast", "else", "enum", "explicit", "export", "extern", "false", "float", + "for", "friend", "goto", "if", "import", "inline", "int", "long", "module", "mutable", "namespace", "new", "noexcept", "not", "not_eq", "nullptr", "operator", "or", "or_eq", "private", "protected", "public", + "register", "reinterpret_cast", "requires", "return", "short", "signed", "sizeof", "static", "static_assert", "static_cast", "struct", "switch", "synchronized", "template", "this", "thread_local", + "throw", "true", "try", "typedef", "typeid", "typename", "union", "unsigned", "using", "virtual", "void", "volatile", "wchar_t", "while", "xor", "xor_eq" + }; + for (auto& k : cppKeywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abort", "abs", "acos", "asin", "atan", "atexit", "atof", "atoi", "atol", "ceil", "clock", "cosh", "ctime", "div", "exit", "fabs", "floor", "fmod", "getchar", "getenv", "isalnum", "isalpha", "isdigit", "isgraph", + "ispunct", "isspace", "isupper", "kbhit", "log10", "log2", "log", "memcmp", "modf", "pow", "printf", "sprintf", "snprintf", "putchar", "putenv", "puts", "rand", "remove", "rename", "sinh", "sqrt", "srand", "strcat", "strcmp", "strerror", "time", "tolower", "toupper", + "std", "string", "vector", "map", "unordered_map", "set", "unordered_set", "min", "max" + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenize = [](const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end, PaletteIndex & paletteIndex) -> bool + { + paletteIndex = PaletteIndex::Max; + + while (in_begin < in_end && isascii(*in_begin) && isblank(*in_begin)) + in_begin++; + + if (in_begin == in_end) + { + out_begin = in_end; + out_end = in_end; + paletteIndex = PaletteIndex::Default; + } + else if (TokenizeCStyleString(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::String; + else if (TokenizeCStyleCharacterLiteral(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::CharLiteral; + else if (TokenizeCStyleIdentifier(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Identifier; + else if (TokenizeCStyleNumber(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Number; + else if (TokenizeCStylePunctuation(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Punctuation; + + return paletteIndex != PaletteIndex::Max; + }; + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "C++"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::HLSL() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "AppendStructuredBuffer", "asm", "asm_fragment", "BlendState", "bool", "break", "Buffer", "ByteAddressBuffer", "case", "cbuffer", "centroid", "class", "column_major", "compile", "compile_fragment", + "CompileShader", "const", "continue", "ComputeShader", "ConsumeStructuredBuffer", "default", "DepthStencilState", "DepthStencilView", "discard", "do", "double", "DomainShader", "dword", "else", + "export", "extern", "false", "float", "for", "fxgroup", "GeometryShader", "groupshared", "half", "Hullshader", "if", "in", "inline", "inout", "InputPatch", "int", "interface", "line", "lineadj", + "linear", "LineStream", "matrix", "min16float", "min10float", "min16int", "min12int", "min16uint", "namespace", "nointerpolation", "noperspective", "NULL", "out", "OutputPatch", "packoffset", + "pass", "pixelfragment", "PixelShader", "point", "PointStream", "precise", "RasterizerState", "RenderTargetView", "return", "register", "row_major", "RWBuffer", "RWByteAddressBuffer", "RWStructuredBuffer", + "RWTexture1D", "RWTexture1DArray", "RWTexture2D", "RWTexture2DArray", "RWTexture3D", "sample", "sampler", "SamplerState", "SamplerComparisonState", "shared", "snorm", "stateblock", "stateblock_state", + "static", "string", "struct", "switch", "StructuredBuffer", "tbuffer", "technique", "technique10", "technique11", "texture", "Texture1D", "Texture1DArray", "Texture2D", "Texture2DArray", "Texture2DMS", + "Texture2DMSArray", "Texture3D", "TextureCube", "TextureCubeArray", "true", "typedef", "triangle", "triangleadj", "TriangleStream", "uint", "uniform", "unorm", "unsigned", "vector", "vertexfragment", + "VertexShader", "void", "volatile", "while", + "bool1","bool2","bool3","bool4","double1","double2","double3","double4", "float1", "float2", "float3", "float4", "int1", "int2", "int3", "int4", "in", "out", "inout", + "uint1", "uint2", "uint3", "uint4", "dword1", "dword2", "dword3", "dword4", "half1", "half2", "half3", "half4", + "float1x1","float2x1","float3x1","float4x1","float1x2","float2x2","float3x2","float4x2", + "float1x3","float2x3","float3x3","float4x3","float1x4","float2x4","float3x4","float4x4", + "half1x1","half2x1","half3x1","half4x1","half1x2","half2x2","half3x2","half4x2", + "half1x3","half2x3","half3x3","half4x3","half1x4","half2x4","half3x4","half4x4", + }; + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abort", "abs", "acos", "all", "AllMemoryBarrier", "AllMemoryBarrierWithGroupSync", "any", "asdouble", "asfloat", "asin", "asint", "asint", "asuint", + "asuint", "atan", "atan2", "ceil", "CheckAccessFullyMapped", "clamp", "clip", "cos", "cosh", "countbits", "cross", "D3DCOLORtoUBYTE4", "ddx", + "ddx_coarse", "ddx_fine", "ddy", "ddy_coarse", "ddy_fine", "degrees", "determinant", "DeviceMemoryBarrier", "DeviceMemoryBarrierWithGroupSync", + "distance", "dot", "dst", "errorf", "EvaluateAttributeAtCentroid", "EvaluateAttributeAtSample", "EvaluateAttributeSnapped", "exp", "exp2", + "f16tof32", "f32tof16", "faceforward", "firstbithigh", "firstbitlow", "floor", "fma", "fmod", "frac", "frexp", "fwidth", "GetRenderTargetSampleCount", + "GetRenderTargetSamplePosition", "GroupMemoryBarrier", "GroupMemoryBarrierWithGroupSync", "InterlockedAdd", "InterlockedAnd", "InterlockedCompareExchange", + "InterlockedCompareStore", "InterlockedExchange", "InterlockedMax", "InterlockedMin", "InterlockedOr", "InterlockedXor", "isfinite", "isinf", "isnan", + "ldexp", "length", "lerp", "lit", "log", "log10", "log2", "mad", "max", "min", "modf", "msad4", "mul", "noise", "normalize", "pow", "printf", + "Process2DQuadTessFactorsAvg", "Process2DQuadTessFactorsMax", "Process2DQuadTessFactorsMin", "ProcessIsolineTessFactors", "ProcessQuadTessFactorsAvg", + "ProcessQuadTessFactorsMax", "ProcessQuadTessFactorsMin", "ProcessTriTessFactorsAvg", "ProcessTriTessFactorsMax", "ProcessTriTessFactorsMin", + "radians", "rcp", "reflect", "refract", "reversebits", "round", "rsqrt", "saturate", "sign", "sin", "sincos", "sinh", "smoothstep", "sqrt", "step", + "tan", "tanh", "tex1D", "tex1D", "tex1Dbias", "tex1Dgrad", "tex1Dlod", "tex1Dproj", "tex2D", "tex2D", "tex2Dbias", "tex2Dgrad", "tex2Dlod", "tex2Dproj", + "tex3D", "tex3D", "tex3Dbias", "tex3Dgrad", "tex3Dlod", "tex3Dproj", "texCUBE", "texCUBE", "texCUBEbias", "texCUBEgrad", "texCUBElod", "texCUBEproj", "transpose", "trunc" + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back(std::make_pair("[ \\t]*#[ \\t]*[a-zA-Z_]+", PaletteIndex::Preprocessor)); + langDef.mTokenRegexStrings.push_back(std::make_pair("L?\\\"(\\\\.|[^\\\"])*\\\"", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair("\\'\\\\?[^\\']\\'", PaletteIndex::CharLiteral)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[0-7]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/\\;\\,\\.]", PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "HLSL"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::GLSL() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "auto", "break", "case", "char", "const", "continue", "default", "do", "double", "else", "enum", "extern", "float", "for", "goto", "if", "inline", "int", "long", "register", "restrict", "return", "short", + "signed", "sizeof", "static", "struct", "switch", "typedef", "union", "unsigned", "void", "volatile", "while", "_Alignas", "_Alignof", "_Atomic", "_Bool", "_Complex", "_Generic", "_Imaginary", + "_Noreturn", "_Static_assert", "_Thread_local" + }; + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abort", "abs", "acos", "asin", "atan", "atexit", "atof", "atoi", "atol", "ceil", "clock", "cosh", "ctime", "div", "exit", "fabs", "floor", "fmod", "getchar", "getenv", "isalnum", "isalpha", "isdigit", "isgraph", + "ispunct", "isspace", "isupper", "kbhit", "log10", "log2", "log", "memcmp", "modf", "pow", "putchar", "putenv", "puts", "rand", "remove", "rename", "sinh", "sqrt", "srand", "strcat", "strcmp", "strerror", "time", "tolower", "toupper" + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back(std::make_pair("[ \\t]*#[ \\t]*[a-zA-Z_]+", PaletteIndex::Preprocessor)); + langDef.mTokenRegexStrings.push_back(std::make_pair("L?\\\"(\\\\.|[^\\\"])*\\\"", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair("\\'\\\\?[^\\']\\'", PaletteIndex::CharLiteral)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[0-7]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/\\;\\,\\.]", PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "GLSL"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::C() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "auto", "break", "case", "char", "const", "continue", "default", "do", "double", "else", "enum", "extern", "float", "for", "goto", "if", "inline", "int", "long", "register", "restrict", "return", "short", + "signed", "sizeof", "static", "struct", "switch", "typedef", "union", "unsigned", "void", "volatile", "while", "_Alignas", "_Alignof", "_Atomic", "_Bool", "_Complex", "_Generic", "_Imaginary", + "_Noreturn", "_Static_assert", "_Thread_local" + }; + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abort", "abs", "acos", "asin", "atan", "atexit", "atof", "atoi", "atol", "ceil", "clock", "cosh", "ctime", "div", "exit", "fabs", "floor", "fmod", "getchar", "getenv", "isalnum", "isalpha", "isdigit", "isgraph", + "ispunct", "isspace", "isupper", "kbhit", "log10", "log2", "log", "memcmp", "modf", "pow", "putchar", "putenv", "puts", "rand", "remove", "rename", "sinh", "sqrt", "srand", "strcat", "strcmp", "strerror", "time", "tolower", "toupper" + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenize = [](const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end, PaletteIndex & paletteIndex) -> bool + { + paletteIndex = PaletteIndex::Max; + + while (in_begin < in_end && isascii(*in_begin) && isblank(*in_begin)) + in_begin++; + + if (in_begin == in_end) + { + out_begin = in_end; + out_end = in_end; + paletteIndex = PaletteIndex::Default; + } + else if (TokenizeCStyleString(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::String; + else if (TokenizeCStyleCharacterLiteral(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::CharLiteral; + else if (TokenizeCStyleIdentifier(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Identifier; + else if (TokenizeCStyleNumber(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Number; + else if (TokenizeCStylePunctuation(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Punctuation; + + return paletteIndex != PaletteIndex::Max; + }; + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "C"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::SQL() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "ADD", "EXCEPT", "PERCENT", "ALL", "EXEC", "PLAN", "ALTER", "EXECUTE", "PRECISION", "AND", "EXISTS", "PRIMARY", "ANY", "EXIT", "PRINT", "AS", "FETCH", "PROC", "ASC", "FILE", "PROCEDURE", + "AUTHORIZATION", "FILLFACTOR", "PUBLIC", "BACKUP", "FOR", "RAISERROR", "BEGIN", "FOREIGN", "READ", "BETWEEN", "FREETEXT", "READTEXT", "BREAK", "FREETEXTTABLE", "RECONFIGURE", + "BROWSE", "FROM", "REFERENCES", "BULK", "FULL", "REPLICATION", "BY", "FUNCTION", "RESTORE", "CASCADE", "GOTO", "RESTRICT", "CASE", "GRANT", "RETURN", "CHECK", "GROUP", "REVOKE", + "CHECKPOINT", "HAVING", "RIGHT", "CLOSE", "HOLDLOCK", "ROLLBACK", "CLUSTERED", "IDENTITY", "ROWCOUNT", "COALESCE", "IDENTITY_INSERT", "ROWGUIDCOL", "COLLATE", "IDENTITYCOL", "RULE", + "COLUMN", "IF", "SAVE", "COMMIT", "IN", "SCHEMA", "COMPUTE", "INDEX", "SELECT", "CONSTRAINT", "INNER", "SESSION_USER", "CONTAINS", "INSERT", "SET", "CONTAINSTABLE", "INTERSECT", "SETUSER", + "CONTINUE", "INTO", "SHUTDOWN", "CONVERT", "IS", "SOME", "CREATE", "JOIN", "STATISTICS", "CROSS", "KEY", "SYSTEM_USER", "CURRENT", "KILL", "TABLE", "CURRENT_DATE", "LEFT", "TEXTSIZE", + "CURRENT_TIME", "LIKE", "THEN", "CURRENT_TIMESTAMP", "LINENO", "TO", "CURRENT_USER", "LOAD", "TOP", "CURSOR", "NATIONAL", "TRAN", "DATABASE", "NOCHECK", "TRANSACTION", + "DBCC", "NONCLUSTERED", "TRIGGER", "DEALLOCATE", "NOT", "TRUNCATE", "DECLARE", "NULL", "TSEQUAL", "DEFAULT", "NULLIF", "UNION", "DELETE", "OF", "UNIQUE", "DENY", "OFF", "UPDATE", + "DESC", "OFFSETS", "UPDATETEXT", "DISK", "ON", "USE", "DISTINCT", "OPEN", "USER", "DISTRIBUTED", "OPENDATASOURCE", "VALUES", "DOUBLE", "OPENQUERY", "VARYING","DROP", "OPENROWSET", "VIEW", + "DUMMY", "OPENXML", "WAITFOR", "DUMP", "OPTION", "WHEN", "ELSE", "OR", "WHERE", "END", "ORDER", "WHILE", "ERRLVL", "OUTER", "WITH", "ESCAPE", "OVER", "WRITETEXT" + }; + + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "ABS", "ACOS", "ADD_MONTHS", "ASCII", "ASCIISTR", "ASIN", "ATAN", "ATAN2", "AVG", "BFILENAME", "BIN_TO_NUM", "BITAND", "CARDINALITY", "CASE", "CAST", "CEIL", + "CHARTOROWID", "CHR", "COALESCE", "COMPOSE", "CONCAT", "CONVERT", "CORR", "COS", "COSH", "COUNT", "COVAR_POP", "COVAR_SAMP", "CUME_DIST", "CURRENT_DATE", + "CURRENT_TIMESTAMP", "DBTIMEZONE", "DECODE", "DECOMPOSE", "DENSE_RANK", "DUMP", "EMPTY_BLOB", "EMPTY_CLOB", "EXP", "EXTRACT", "FIRST_VALUE", "FLOOR", "FROM_TZ", "GREATEST", + "GROUP_ID", "HEXTORAW", "INITCAP", "INSTR", "INSTR2", "INSTR4", "INSTRB", "INSTRC", "LAG", "LAST_DAY", "LAST_VALUE", "LEAD", "LEAST", "LENGTH", "LENGTH2", "LENGTH4", + "LENGTHB", "LENGTHC", "LISTAGG", "LN", "LNNVL", "LOCALTIMESTAMP", "LOG", "LOWER", "LPAD", "LTRIM", "MAX", "MEDIAN", "MIN", "MOD", "MONTHS_BETWEEN", "NANVL", "NCHR", + "NEW_TIME", "NEXT_DAY", "NTH_VALUE", "NULLIF", "NUMTODSINTERVAL", "NUMTOYMINTERVAL", "NVL", "NVL2", "POWER", "RANK", "RAWTOHEX", "REGEXP_COUNT", "REGEXP_INSTR", + "REGEXP_REPLACE", "REGEXP_SUBSTR", "REMAINDER", "REPLACE", "ROUND", "ROWNUM", "RPAD", "RTRIM", "SESSIONTIMEZONE", "SIGN", "SIN", "SINH", + "SOUNDEX", "SQRT", "STDDEV", "SUBSTR", "SUM", "SYS_CONTEXT", "SYSDATE", "SYSTIMESTAMP", "TAN", "TANH", "TO_CHAR", "TO_CLOB", "TO_DATE", "TO_DSINTERVAL", "TO_LOB", + "TO_MULTI_BYTE", "TO_NCLOB", "TO_NUMBER", "TO_SINGLE_BYTE", "TO_TIMESTAMP", "TO_TIMESTAMP_TZ", "TO_YMINTERVAL", "TRANSLATE", "TRIM", "TRUNC", "TZ_OFFSET", "UID", "UPPER", + "USER", "USERENV", "VAR_POP", "VAR_SAMP", "VARIANCE", "VSIZE " + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back(std::make_pair("L?\\\"(\\\\.|[^\\\"])*\\\"", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair("\\\'[^\\\']*\\\'", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[0-7]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/\\;\\,\\.]", PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = false; + langDef.mAutoIndentation = false; + + langDef.mName = "SQL"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::AngelScript() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "and", "abstract", "auto", "bool", "break", "case", "cast", "class", "const", "continue", "default", "do", "double", "else", "enum", "false", "final", "float", "for", + "from", "funcdef", "function", "get", "if", "import", "in", "inout", "int", "interface", "int8", "int16", "int32", "int64", "is", "mixin", "namespace", "not", + "null", "or", "out", "override", "private", "protected", "return", "set", "shared", "super", "switch", "this ", "true", "typedef", "uint", "uint8", "uint16", "uint32", + "uint64", "void", "while", "xor" + }; + + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "cos", "sin", "tab", "acos", "asin", "atan", "atan2", "cosh", "sinh", "tanh", "log", "log10", "pow", "sqrt", "abs", "ceil", "floor", "fraction", "closeTo", "fpFromIEEE", "fpToIEEE", + "complex", "opEquals", "opAddAssign", "opSubAssign", "opMulAssign", "opDivAssign", "opAdd", "opSub", "opMul", "opDiv" + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back(std::make_pair("L?\\\"(\\\\.|[^\\\"])*\\\"", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair("\\'\\\\?[^\\']\\'", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[0-7]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/\\;\\,\\.]", PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "AngelScript"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::Lua() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "and", "break", "do", "", "else", "elseif", "end", "false", "for", "function", "if", "in", "", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while" + }; + + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "assert", "collectgarbage", "dofile", "error", "getmetatable", "ipairs", "loadfile", "load", "loadstring", "next", "pairs", "pcall", "print", "rawequal", "rawlen", "rawget", "rawset", + "select", "setmetatable", "tonumber", "tostring", "type", "xpcall", "_G", "_VERSION","arshift", "band", "bnot", "bor", "bxor", "btest", "extract", "lrotate", "lshift", "replace", + "rrotate", "rshift", "create", "resume", "running", "status", "wrap", "yield", "isyieldable", "debug","getuservalue", "gethook", "getinfo", "getlocal", "getregistry", "getmetatable", + "getupvalue", "upvaluejoin", "upvalueid", "setuservalue", "sethook", "setlocal", "setmetatable", "setupvalue", "traceback", "close", "flush", "input", "lines", "open", "output", "popen", + "read", "tmpfile", "type", "write", "close", "flush", "lines", "read", "seek", "setvbuf", "write", "__gc", "__tostring", "abs", "acos", "asin", "atan", "ceil", "cos", "deg", "exp", "tointeger", + "floor", "fmod", "ult", "log", "max", "min", "modf", "rad", "random", "randomseed", "sin", "sqrt", "string", "tan", "type", "atan2", "cosh", "sinh", "tanh", + "pow", "frexp", "ldexp", "log10", "pi", "huge", "maxinteger", "mininteger", "loadlib", "searchpath", "seeall", "preload", "cpath", "path", "searchers", "loaded", "module", "require", "clock", + "date", "difftime", "execute", "exit", "getenv", "remove", "rename", "setlocale", "time", "tmpname", "byte", "char", "dump", "find", "format", "gmatch", "gsub", "len", "lower", "match", "rep", + "reverse", "sub", "upper", "pack", "packsize", "unpack", "concat", "maxn", "insert", "pack", "unpack", "remove", "move", "sort", "offset", "codepoint", "char", "len", "codes", "charpattern", + "coroutine", "table", "io", "os", "string", "utf8", "bit32", "math", "debug", "package" + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back(std::make_pair("L?\\\"(\\\\.|[^\\\"])*\\\"", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair("\\\'[^\\\']*\\\'", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/\\;\\,\\.]", PaletteIndex::Punctuation)); + + langDef.mCommentStart = "--[["; + langDef.mCommentEnd = "]]"; + langDef.mSingleLineComment = "--"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = false; + + langDef.mName = "Lua"; + + inited = true; + } + return langDef; +} diff --git a/src/ThirdParty/ImGuiColorTextEdit/TextEditor.h b/src/ThirdParty/ImGuiColorTextEdit/TextEditor.h new file mode 100644 index 0000000..98ab99b --- /dev/null +++ b/src/ThirdParty/ImGuiColorTextEdit/TextEditor.h @@ -0,0 +1,401 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "imgui.h" + +class TextEditor +{ +public: + enum class PaletteIndex + { + Default, + Keyword, + Number, + String, + CharLiteral, + Punctuation, + Preprocessor, + Identifier, + KnownIdentifier, + PreprocIdentifier, + Comment, + MultiLineComment, + Background, + Cursor, + Selection, + ErrorMarker, + Breakpoint, + LineNumber, + CurrentLineFill, + CurrentLineFillInactive, + CurrentLineEdge, + Max + }; + + enum class SelectionMode + { + Normal, + Word, + Line + }; + + struct Breakpoint + { + int mLine; + bool mEnabled; + std::string mCondition; + + Breakpoint() + : mLine(-1) + , mEnabled(false) + {} + }; + + // Represents a character coordinate from the user's point of view, + // i. e. consider an uniform grid (assuming fixed-width font) on the + // screen as it is rendered, and each cell has its own coordinate, starting from 0. + // Tabs are counted as [1..mTabSize] count empty spaces, depending on + // how many space is necessary to reach the next tab stop. + // For example, coordinate (1, 5) represents the character 'B' in a line "\tABC", when mTabSize = 4, + // because it is rendered as " ABC" on the screen. + struct Coordinates + { + int mLine, mColumn; + Coordinates() : mLine(0), mColumn(0) {} + Coordinates(int aLine, int aColumn) : mLine(aLine), mColumn(aColumn) + { + assert(aLine >= 0); + assert(aColumn >= 0); + } + static Coordinates Invalid() { static Coordinates invalid(-1, -1); return invalid; } + + bool operator ==(const Coordinates& o) const + { + return + mLine == o.mLine && + mColumn == o.mColumn; + } + + bool operator !=(const Coordinates& o) const + { + return + mLine != o.mLine || + mColumn != o.mColumn; + } + + bool operator <(const Coordinates& o) const + { + if (mLine != o.mLine) + return mLine < o.mLine; + return mColumn < o.mColumn; + } + + bool operator >(const Coordinates& o) const + { + if (mLine != o.mLine) + return mLine > o.mLine; + return mColumn > o.mColumn; + } + + bool operator <=(const Coordinates& o) const + { + if (mLine != o.mLine) + return mLine < o.mLine; + return mColumn <= o.mColumn; + } + + bool operator >=(const Coordinates& o) const + { + if (mLine != o.mLine) + return mLine > o.mLine; + return mColumn >= o.mColumn; + } + }; + + struct Identifier + { + Coordinates mLocation; + std::string mDeclaration; + }; + + typedef std::string String; + typedef std::unordered_map Identifiers; + typedef std::unordered_set Keywords; + typedef std::map ErrorMarkers; + typedef std::unordered_set Breakpoints; + typedef std::array Palette; + typedef uint8_t Char; + + struct Glyph + { + Char mChar; + PaletteIndex mColorIndex = PaletteIndex::Default; + bool mComment : 1; + bool mMultiLineComment : 1; + bool mPreprocessor : 1; + + Glyph(Char aChar, PaletteIndex aColorIndex) : mChar(aChar), mColorIndex(aColorIndex), + mComment(false), mMultiLineComment(false), mPreprocessor(false) {} + }; + + typedef std::vector Line; + typedef std::vector Lines; + + struct LanguageDefinition + { + typedef std::pair TokenRegexString; + typedef std::vector TokenRegexStrings; + typedef bool(*TokenizeCallback)(const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end, PaletteIndex & paletteIndex); + + std::string mName; + Keywords mKeywords; + Identifiers mIdentifiers; + Identifiers mPreprocIdentifiers; + std::string mCommentStart, mCommentEnd, mSingleLineComment; + char mPreprocChar; + bool mAutoIndentation; + + TokenizeCallback mTokenize; + + TokenRegexStrings mTokenRegexStrings; + + bool mCaseSensitive; + + LanguageDefinition() + : mPreprocChar('#'), mAutoIndentation(true), mTokenize(nullptr), mCaseSensitive(true) + { + } + + static const LanguageDefinition& CPlusPlus(); + static const LanguageDefinition& HLSL(); + static const LanguageDefinition& GLSL(); + static const LanguageDefinition& C(); + static const LanguageDefinition& SQL(); + static const LanguageDefinition& AngelScript(); + static const LanguageDefinition& Lua(); + }; + + TextEditor(); + ~TextEditor(); + + void SetLanguageDefinition(const LanguageDefinition& aLanguageDef); + const LanguageDefinition& GetLanguageDefinition() const { return mLanguageDefinition; } + + const Palette& GetPalette() const { return mPaletteBase; } + void SetPalette(const Palette& aValue); + + void SetErrorMarkers(const ErrorMarkers& aMarkers) { mErrorMarkers = aMarkers; } + void SetBreakpoints(const Breakpoints& aMarkers) { mBreakpoints = aMarkers; } + + void Render(const char* aTitle, const ImVec2& aSize = ImVec2(), bool aBorder = false); + void SetText(const std::string& aText); + std::string GetText() const; + + void SetTextLines(const std::vector& aLines); + std::vector GetTextLines() const; + + std::string GetSelectedText() const; + std::string GetCurrentLineText()const; + + int GetTotalLines() const { return (int)mLines.size(); } + bool IsOverwrite() const { return mOverwrite; } + + void SetReadOnly(bool aValue); + bool IsReadOnly() const { return mReadOnly; } + bool IsTextChanged() const { return mTextChanged; } + bool IsCursorPositionChanged() const { return mCursorPositionChanged; } + + bool IsColorizerEnabled() const { return mColorizerEnabled; } + void SetColorizerEnable(bool aValue); + + Coordinates GetCursorPosition() const { return GetActualCursorCoordinates(); } + void SetCursorPosition(const Coordinates& aPosition); + + inline void SetHandleMouseInputs (bool aValue){ mHandleMouseInputs = aValue;} + inline bool IsHandleMouseInputsEnabled() const { return mHandleKeyboardInputs; } + + inline void SetHandleKeyboardInputs (bool aValue){ mHandleKeyboardInputs = aValue;} + inline bool IsHandleKeyboardInputsEnabled() const { return mHandleKeyboardInputs; } + inline void SetAllowTabInput(bool aValue) { mAllowTabInput = aValue; } + inline bool IsTabInputAllowed() const { return mAllowTabInput; } + inline void SetSmartTabDelete(bool aValue) { mSmartTabDelete = aValue; } + inline bool IsSmartTabDeleteEnabled() const { return mSmartTabDelete; } + + inline void SetImGuiChildIgnored (bool aValue){ mIgnoreImGuiChild = aValue;} + inline bool IsImGuiChildIgnored() const { return mIgnoreImGuiChild; } + + inline void SetShowWhitespaces(bool aValue) { mShowWhitespaces = aValue; } + inline bool IsShowingWhitespaces() const { return mShowWhitespaces; } + + void SetTabSize(int aValue); + inline int GetTabSize() const { return mTabSize; } + + void InsertText(const std::string& aValue); + void InsertText(const char* aValue); + + void MoveUp(int aAmount = 1, bool aSelect = false); + void MoveDown(int aAmount = 1, bool aSelect = false); + void MoveLeft(int aAmount = 1, bool aSelect = false, bool aWordMode = false); + void MoveRight(int aAmount = 1, bool aSelect = false, bool aWordMode = false); + void MoveTop(bool aSelect = false); + void MoveBottom(bool aSelect = false); + void MoveHome(bool aSelect = false); + void MoveEnd(bool aSelect = false); + + void SetSelectionStart(const Coordinates& aPosition); + void SetSelectionEnd(const Coordinates& aPosition); + void SetSelection(const Coordinates& aStart, const Coordinates& aEnd, SelectionMode aMode = SelectionMode::Normal); + std::string GetWordUnderCursorPublic() const; + std::string GetWordAtPublic(const Coordinates& aCoords) const; + ImVec2 GetCursorScreenPositionPublic() const { return mCursorScreenPos; } + bool HasCursorScreenPosition() const { return mCursorScreenPosValid; } + void SelectWordUnderCursor(); + void SelectAll(); + bool HasSelection() const; + + void Copy(); + void Cut(); + void Paste(); + void Delete(); + + bool CanUndo() const; + bool CanRedo() const; + void Undo(int aSteps = 1); + void Redo(int aSteps = 1); + + static const Palette& GetDarkPalette(); + static const Palette& GetLightPalette(); + static const Palette& GetRetroBluePalette(); + +private: + typedef std::vector> RegexList; + + struct EditorState + { + Coordinates mSelectionStart; + Coordinates mSelectionEnd; + Coordinates mCursorPosition; + }; + + class UndoRecord + { + public: + UndoRecord() {} + ~UndoRecord() {} + + UndoRecord( + const std::string& aAdded, + const TextEditor::Coordinates aAddedStart, + const TextEditor::Coordinates aAddedEnd, + + const std::string& aRemoved, + const TextEditor::Coordinates aRemovedStart, + const TextEditor::Coordinates aRemovedEnd, + + TextEditor::EditorState& aBefore, + TextEditor::EditorState& aAfter); + + void Undo(TextEditor* aEditor); + void Redo(TextEditor* aEditor); + + std::string mAdded; + Coordinates mAddedStart; + Coordinates mAddedEnd; + + std::string mRemoved; + Coordinates mRemovedStart; + Coordinates mRemovedEnd; + + EditorState mBefore; + EditorState mAfter; + }; + + typedef std::vector UndoBuffer; + + void ProcessInputs(); + void Colorize(int aFromLine = 0, int aCount = -1); + void ColorizeRange(int aFromLine = 0, int aToLine = 0); + void ColorizeInternal(); + float TextDistanceToLineStart(const Coordinates& aFrom) const; + void EnsureCursorVisible(); + int GetPageSize() const; + std::string GetText(const Coordinates& aStart, const Coordinates& aEnd) const; + Coordinates GetActualCursorCoordinates() const; + Coordinates SanitizeCoordinates(const Coordinates& aValue) const; + void Advance(Coordinates& aCoordinates) const; + void DeleteRange(const Coordinates& aStart, const Coordinates& aEnd); + int InsertTextAt(Coordinates& aWhere, const char* aValue); + void AddUndo(UndoRecord& aValue); + Coordinates ScreenPosToCoordinates(const ImVec2& aPosition) const; + Coordinates FindWordStart(const Coordinates& aFrom) const; + Coordinates FindWordEnd(const Coordinates& aFrom) const; + Coordinates FindNextWord(const Coordinates& aFrom) const; + int GetCharacterIndex(const Coordinates& aCoordinates) const; + int GetCharacterColumn(int aLine, int aIndex) const; + int GetLineCharacterCount(int aLine) const; + int GetLineMaxColumn(int aLine) const; + bool IsOnWordBoundary(const Coordinates& aAt) const; + void RemoveLine(int aStart, int aEnd); + void RemoveLine(int aIndex); + Line& InsertLine(int aIndex); + void EnterCharacter(ImWchar aChar, bool aShift); + void Backspace(); + void DeleteSelection(); + std::string GetWordUnderCursor() const; + std::string GetWordAt(const Coordinates& aCoords) const; + ImU32 GetGlyphColor(const Glyph& aGlyph) const; + + void HandleKeyboardInputs(); + void HandleMouseInputs(); + void Render(); + + float mLineSpacing; + Lines mLines; + EditorState mState; + UndoBuffer mUndoBuffer; + int mUndoIndex; + + int mTabSize; + bool mOverwrite; + bool mReadOnly; + bool mWithinRender; + bool mScrollToCursor; + bool mScrollToTop; + bool mTextChanged; + bool mColorizerEnabled; + float mTextStart; // position (in pixels) where a code line starts relative to the left of the TextEditor. + int mLeftMargin; + bool mCursorPositionChanged; + int mColorRangeMin, mColorRangeMax; + SelectionMode mSelectionMode; + bool mHandleKeyboardInputs; + bool mHandleMouseInputs; + bool mAllowTabInput; + bool mSmartTabDelete; + ImVec2 mCursorScreenPos; + bool mCursorScreenPosValid; + bool mIgnoreImGuiChild; + bool mShowWhitespaces; + + Palette mPaletteBase; + Palette mPalette; + LanguageDefinition mLanguageDefinition; + RegexList mRegexList; + + bool mCheckComments; + Breakpoints mBreakpoints; + ErrorMarkers mErrorMarkers; + ImVec2 mCharAdvance; + Coordinates mInteractiveStart, mInteractiveEnd; + std::string mLineBuffer; + uint64_t mStartTime; + + float mLastClick; +}; diff --git a/src/ThirdParty/imgui/imgui.cpp b/src/ThirdParty/imgui/imgui.cpp index 8f92d79..bc2662e 100644 --- a/src/ThirdParty/imgui/imgui.cpp +++ b/src/ThirdParty/imgui/imgui.cpp @@ -1288,6 +1288,7 @@ static const float DOCKING_TRANSPARENT_PAYLOAD_ALPHA = 0.50f; // For u static void SetCurrentWindow(ImGuiWindow* window); static ImGuiWindow* CreateNewWindow(const char* name, ImGuiWindowFlags flags); static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow* window); +static float DockAnimEaseSmooth(float t); static void AddWindowToSortBuffer(ImVector* out_sorted_windows, ImGuiWindow* window); @@ -4603,6 +4604,12 @@ ImGuiWindow::ImGuiWindow(ImGuiContext* ctx, const char* name) : DrawListInst(NUL FontWindowScale = FontWindowScaleParents = 1.0f; SettingsOffset = -1; DockOrder = -1; + DockAnimStartTime = 0.0f; + DockAnimDuration = 0.0f; + DockAnimActive = false; + DockAnimOvershoot = false; + DockAnimGrabRatio = ImVec2(0.5f, 0.5f); + DockAnimOvershootStrength = 1.0f; DrawList = &DrawListInst; DrawList->_OwnerName = Name; DrawList->_SetDrawListSharedData(&Ctx->DrawListSharedData); @@ -5252,6 +5259,12 @@ ImDrawListSharedData* ImGui::GetDrawListSharedData() return &GImGui->DrawListSharedData; } +namespace ImGui +{ + ImGuiDockNode* DockContextProcessUndockNode(ImGuiContext* ctx, ImGuiDockNode* node); + void DockNodeStartMouseMovingWindow(ImGuiDockNode* node, ImGuiWindow* window); +} + void ImGui::StartMouseMovingWindow(ImGuiWindow* window) { // Set ActiveId even if the _NoMove flag is set. Without it, dragging away from a window with _NoMove would activate hover on other windows. @@ -5294,7 +5307,12 @@ void ImGui::StartMouseMovingWindowOrNode(ImGuiWindow* window, ImGuiDockNode* nod const bool clicked = IsMouseClicked(0); const bool dragging = IsMouseDragging(0); if (can_undock_node && dragging) - DockContextQueueUndockNode(&g, node); // Will lead to DockNodeStartMouseMovingWindow() -> StartMouseMovingWindow() being called next frame + { + ImGuiDockNode* undocked = DockContextProcessUndockNode(&g, node); + if (undocked && undocked->Windows.Size > 0) + DockNodeStartMouseMovingWindow(undocked, undocked->Windows[0]); + return; + } else if (!can_undock_node && (clicked || dragging) && g.MovingWindow != window) StartMouseMovingWindow(window); } @@ -5348,7 +5366,30 @@ void ImGui::UpdateMouseMovingWindowNewFrame() const bool window_disappeared = (!moving_window->WasActive && !moving_window->Active); if (g.IO.MouseDown[0] && IsMousePosValid(&g.IO.MousePos) && !window_disappeared) { - ImVec2 pos = g.IO.MousePos - g.ActiveIdClickOffset; + ImVec2 pos_target = g.IO.MousePos - g.ActiveIdClickOffset; + ImVec2 pos = pos_target; + if (moving_window->DockAnimActive && moving_window->DockAnimOvershoot) + { + const float elapsed = (float)(g.Time - moving_window->DockAnimStartTime); + const float duration = ImMax(0.001f, moving_window->DockAnimDuration); + float t = ImSaturate(elapsed / duration); + const float ease_out = DockAnimEaseSmooth(t); + const float s = 1.70158f * moving_window->DockAnimOvershootStrength; + const float t1 = t - 1.0f; + float ease_t = 1.0f + (t1 * t1) * ((s + 1.0f) * t1 + s); + ImVec2 anim_size = ImLerp(moving_window->DockAnimFromSize, moving_window->DockAnimToSize, ease_t); + if (anim_size.x > 0.0f && anim_size.y > 0.0f) + { + moving_window->SizeFull = anim_size; + moving_window->Size = anim_size; + ImVec2 pos_target_anim = g.IO.MousePos - moving_window->DockAnimGrabRatio * anim_size; + pos = ImLerp(moving_window->DockAnimFromPos, pos_target_anim, ease_out); + } + else + { + pos = ImLerp(moving_window->DockAnimFromPos, pos_target, ease_out); + } + } if (moving_window->Pos.x != pos.x || moving_window->Pos.y != pos.y) { SetWindowPos(moving_window, pos, ImGuiCond_Always); @@ -8141,6 +8182,29 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) } window->Pos = ImTrunc(window->Pos); + if (window->DockAnimActive) + { + const float elapsed = (float)(g.Time - window->DockAnimStartTime); + const float duration = ImMax(0.001f, window->DockAnimDuration); + float t = ImSaturate(elapsed / duration); + float ease_t = DockAnimEaseSmooth(t); + if (window->DockAnimOvershoot) + { + const float s = 1.70158f * window->DockAnimOvershootStrength; + const float t1 = t - 1.0f; + ease_t = 1.0f + (t1 * t1) * ((s + 1.0f) * t1 + s); + } + const ImVec2 from_center = window->DockAnimFromPos + window->DockAnimFromSize * 0.5f; + const ImVec2 to_center = window->DockAnimToPos + window->DockAnimToSize * 0.5f; + const ImVec2 center = ImLerp(from_center, to_center, t); + const ImVec2 size = ImLerp(window->DockAnimFromSize, window->DockAnimToSize, ease_t); + window->SizeFull = size; + window->Pos = center - size * 0.5f; + window->Size = (window->Collapsed && !(flags & ImGuiWindowFlags_ChildWindow)) ? window->TitleBarRect().GetSize() : window->SizeFull; + if (t >= 1.0f) + window->DockAnimActive = false; + } + // Lock window rounding for the frame (so that altering them doesn't cause inconsistencies) // Large values tend to lead to variety of artifacts and are not recommended. if (window->ViewportOwned || window->DockIsActive) @@ -17540,6 +17604,7 @@ namespace ImGui static void DockContextRemoveNode(ImGuiContext* ctx, ImGuiDockNode* node, bool merge_sibling_into_parent_node); static void DockContextQueueNotifyRemovedNode(ImGuiContext* ctx, ImGuiDockNode* node); static void DockContextProcessDock(ImGuiContext* ctx, ImGuiDockRequest* req); + ImGuiDockNode* DockContextProcessUndockNode(ImGuiContext* ctx, ImGuiDockNode* node); static void DockContextPruneUnusedSettingsNodes(ImGuiContext* ctx); static ImGuiDockNode* DockContextBindNodeToWindow(ImGuiContext* ctx, ImGuiWindow* window); static void DockContextBuildNodesFromSettings(ImGuiContext* ctx, ImGuiDockNodeSettings* node_settings_array, int node_settings_count); @@ -17562,7 +17627,7 @@ namespace ImGui static void DockNodeRemoveTabBar(ImGuiDockNode* node); static void DockNodeWindowMenuUpdate(ImGuiDockNode* node, ImGuiTabBar* tab_bar); static void DockNodeUpdateVisibleFlag(ImGuiDockNode* node); - static void DockNodeStartMouseMovingWindow(ImGuiDockNode* node, ImGuiWindow* window); + void DockNodeStartMouseMovingWindow(ImGuiDockNode* node, ImGuiWindow* window); static bool DockNodeIsDropAllowed(ImGuiWindow* host_window, ImGuiWindow* payload_window); static void DockNodePreviewDockSetup(ImGuiWindow* host_window, ImGuiDockNode* host_node, ImGuiWindow* payload_window, ImGuiDockNode* payload_node, ImGuiDockPreviewData* preview_data, bool is_explicit_target, bool is_outer_docking); static void DockNodePreviewDockRender(ImGuiWindow* host_window, ImGuiDockNode* host_node, ImGuiWindow* payload_window, const ImGuiDockPreviewData* preview_data); @@ -18003,6 +18068,27 @@ void ImGui::DockContextQueueNotifyRemovedNode(ImGuiContext* ctx, ImGuiDockNode* req.Type = ImGuiDockRequestType_None; } +static float DockAnimEaseSmooth(float t) +{ + t = ImSaturate(t); + return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f); +} + +static void DockWindowQueueScaleAnim(ImGuiContext* ctx, ImGuiWindow* window, const ImVec2& from_pos, const ImVec2& from_size, const ImVec2& to_pos, const ImVec2& to_size, bool overshoot, float duration, float overshoot_strength) +{ + if (to_size.x <= 0.0f || to_size.y <= 0.0f) + return; + window->DockAnimFromPos = from_pos; + window->DockAnimFromSize = from_size; + window->DockAnimToPos = to_pos; + window->DockAnimToSize = to_size; + window->DockAnimStartTime = (float)ctx->Time; + window->DockAnimDuration = duration; + window->DockAnimActive = true; + window->DockAnimOvershoot = overshoot; + window->DockAnimOvershootStrength = overshoot_strength; +} + void ImGui::DockContextProcessDock(ImGuiContext* ctx, ImGuiDockRequest* req) { IM_ASSERT((req->Type == ImGuiDockRequestType_Dock && req->DockPayload != NULL) || (req->Type == ImGuiDockRequestType_Split && req->DockPayload == NULL)); @@ -18014,6 +18100,15 @@ void ImGui::DockContextProcessDock(ImGuiContext* ctx, ImGuiDockRequest* req) ImGuiWindow* payload_window = req->DockPayload; // Optional ImGuiWindow* target_window = req->DockTargetWindow; ImGuiDockNode* node = req->DockTargetNode; + ImVector payload_windows; + if (payload_window) + { + payload_windows.push_back(payload_window); + } + else if (req->DockTargetNode == NULL && req->DockTargetWindow != NULL && req->DockSplitDir != ImGuiDir_None) + { + // no-op, payload_node path handled below + } if (payload_window) IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextProcessDock node 0x%08X target '%s' dock window '%s', split_dir %d\n", node ? node->ID : 0, target_window ? target_window->Name : "NULL", payload_window->Name, req->DockSplitDir); else @@ -18030,6 +18125,11 @@ void ImGui::DockContextProcessDock(ImGuiContext* ctx, ImGuiDockRequest* req) next_selected_id = payload_node->TabBar->NextSelectedTabId ? payload_node->TabBar->NextSelectedTabId : payload_node->TabBar->SelectedTabId; if (payload_node == NULL) next_selected_id = payload_window->TabId; + if (payload_node) + { + for (ImGuiWindow* window : payload_node->Windows) + payload_windows.push_back(window); + } } // FIXME-DOCK: When we are trying to dock an existing single-window node into a loose window, transfer Node ID as well @@ -18138,6 +18238,24 @@ void ImGui::DockContextProcessDock(ImGuiContext* ctx, ImGuiDockRequest* req) // Update selection immediately if (ImGuiTabBar* tab_bar = node->TabBar) tab_bar->NextSelectedTabId = next_selected_id; + const ImVec2 target_pos = node->Pos; + const ImVec2 target_size = node->Size; + for (ImGuiWindow* window : payload_windows) + { + const ImVec2 from_pos = window->Pos; + const ImVec2 from_size = window->SizeFull; + const ImVec2 size_delta = target_size - from_size; + const ImVec2 pos_delta = target_pos - from_pos; + const float dist = size_delta.x * size_delta.x + size_delta.y * size_delta.y + pos_delta.x * pos_delta.x + pos_delta.y * pos_delta.y; + if (dist > 1.0f) + DockWindowQueueScaleAnim(ctx, window, from_pos, from_size, target_pos, target_size, true, 0.20f, 0.75f); + else + { + const ImVec2 scaled = target_size * 0.92f; + const ImVec2 centered = target_pos + (target_size - scaled) * 0.5f; + DockWindowQueueScaleAnim(ctx, window, centered, scaled, target_pos, target_size, true, 0.20f, 0.75f); + } + } MarkIniSettingsDirty(); } @@ -18168,6 +18286,12 @@ void ImGui::DockContextProcessUndockWindow(ImGuiContext* ctx, ImGuiWindow* windo { ImGuiContext& g = *ctx; IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextProcessUndockWindow window '%s', clear_persistent_docking_ref = %d\n", window->Name, clear_persistent_docking_ref); + const ImVec2 from_pos = window->Pos; + const ImVec2 from_size = window->SizeFull; + if (from_size.x > 0.0f && from_size.y > 0.0f) + window->DockAnimGrabRatio = g.ActiveIdClickOffset / from_size; + else + window->DockAnimGrabRatio = ImVec2(0.5f, 0.5f); if (window->DockNode) DockNodeRemoveWindow(window->DockNode, window, clear_persistent_docking_ref ? 0 : window->DockId); else @@ -18176,11 +18300,17 @@ void ImGui::DockContextProcessUndockWindow(ImGuiContext* ctx, ImGuiWindow* windo window->DockIsActive = false; window->DockNodeIsVisible = window->DockTabIsVisible = false; window->Size = window->SizeFull = FixLargeWindowsWhenUndocking(window->SizeFull, window->Viewport); + ImVec2 to_pos = window->Pos; + ImVec2 to_size = window->SizeFull; + ImVec2 size_delta = to_size - from_size; + ImVec2 pos_delta = to_pos - from_pos; + const float dist = size_delta.x * size_delta.x + size_delta.y * size_delta.y + pos_delta.x * pos_delta.x + pos_delta.y * pos_delta.y; + DockWindowQueueScaleAnim(ctx, window, from_pos, from_size, to_pos, to_size, true, 0.28f, 1.05f); MarkIniSettingsDirty(); } -void ImGui::DockContextProcessUndockNode(ImGuiContext* ctx, ImGuiDockNode* node) +ImGuiDockNode* ImGui::DockContextProcessUndockNode(ImGuiContext* ctx, ImGuiDockNode* node) { ImGuiContext& g = *ctx; IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextProcessUndockNode node %08X\n", node->ID); @@ -18219,6 +18349,7 @@ void ImGui::DockContextProcessUndockNode(ImGuiContext* ctx, ImGuiDockNode* node) node->Size = FixLargeWindowsWhenUndocking(node->Size, node->Windows[0]->Viewport); node->WantMouseMove = true; MarkIniSettingsDirty(); + return node; } // This is mostly used for automation. @@ -18669,7 +18800,7 @@ static void ImGui::DockNodeUpdateVisibleFlag(ImGuiDockNode* node) node->IsVisible = is_visible; } -static void ImGui::DockNodeStartMouseMovingWindow(ImGuiDockNode* node, ImGuiWindow* window) +void ImGui::DockNodeStartMouseMovingWindow(ImGuiDockNode* node, ImGuiWindow* window) { ImGuiContext& g = *GImGui; IM_ASSERT(node->WantMouseMove == true); @@ -19711,7 +19842,9 @@ static void ImGui::DockNodePreviewDockRender(ImGuiWindow* host_window, ImGuiDock overlay_draw_lists[overlay_draw_lists_count++] = GetForegroundDrawList(root_payload->Viewport); // Draw main preview rectangle - const ImU32 overlay_col_main = GetColorU32(ImGuiCol_DockingPreview, is_transparent_payload ? 0.60f : 0.40f); + const float pulse = 0.5f + 0.5f * sinf((float)g.Time * 3.2f); + const float pulse_alpha = ImLerp(0.65f, 1.35f, pulse); + const ImU32 overlay_col_main = GetColorU32(ImGuiCol_DockingPreview, (is_transparent_payload ? 0.60f : 0.40f) * pulse_alpha); const ImU32 overlay_col_drop = GetColorU32(ImGuiCol_DockingPreview, is_transparent_payload ? 0.90f : 0.70f); const ImU32 overlay_col_drop_hovered = GetColorU32(ImGuiCol_DockingPreview, is_transparent_payload ? 1.20f : 1.00f); const ImU32 overlay_col_lines = GetColorU32(ImGuiCol_NavWindowingHighlight, is_transparent_payload ? 0.80f : 0.60f); diff --git a/src/ThirdParty/imgui/imgui_internal.h b/src/ThirdParty/imgui/imgui_internal.h index e0cd8f6..c89a5cb 100644 --- a/src/ThirdParty/imgui/imgui_internal.h +++ b/src/ThirdParty/imgui/imgui_internal.h @@ -2838,6 +2838,16 @@ struct IMGUI_API ImGuiWindow ImVec2 Pos; // Position (always rounded-up to nearest pixel) ImVec2 Size; // Current size (==SizeFull or collapsed title bar size) ImVec2 SizeFull; // Size when non collapsed + ImVec2 DockAnimFromPos; // Docking animation start position + ImVec2 DockAnimFromSize; // Docking animation start size + ImVec2 DockAnimToPos; // Docking animation target position + ImVec2 DockAnimToSize; // Docking animation target size + ImVec2 DockAnimGrabRatio; // Docking animation grab ratio during undock drag + float DockAnimStartTime; // Docking animation start time + float DockAnimDuration; // Docking animation duration + bool DockAnimActive; // Docking animation active + bool DockAnimOvershoot; // Docking animation overshoot (undock) + float DockAnimOvershootStrength; // Docking animation overshoot strength ImVec2 ContentSize; // Size of contents/scrollable client area (calculated from the extents reach of the cursor) from previous frame. Does not include window decoration or window padding. ImVec2 ContentSizeIdeal; ImVec2 ContentSizeExplicit; // Size of contents/scrollable client area explicitly request by the user via SetNextWindowContentSize(). @@ -3005,6 +3015,7 @@ struct ImGuiTabItem int LastFrameVisible; int LastFrameSelected; // This allows us to infer an ordered list of the last activated tabs with little maintenance float Offset; // Position relative to beginning of tab + float OffsetAnim; // Animated position relative to beginning of tab float Width; // Width currently displayed float ContentWidth; // Width of label + padding, stored during BeginTabItem() call (misnamed as "Content" would normally imply width of label only) float RequestedWidth; // Width optionally requested by caller, -1.0f is unused @@ -3013,7 +3024,7 @@ struct ImGuiTabItem ImS16 IndexDuringLayout; // Index only used during TabBarLayout(). Tabs gets reordered so 'Tabs[n].IndexDuringLayout == n' but may mismatch during additions. bool WantClose; // Marked as closed by SetTabItemClosed() - ImGuiTabItem() { memset(this, 0, sizeof(*this)); LastFrameVisible = LastFrameSelected = -1; RequestedWidth = -1.0f; NameOffset = -1; BeginOrder = IndexDuringLayout = -1; } + ImGuiTabItem() { memset(this, 0, sizeof(*this)); LastFrameVisible = LastFrameSelected = -1; RequestedWidth = -1.0f; NameOffset = -1; BeginOrder = IndexDuringLayout = -1; OffsetAnim = -1.0f; } }; // Storage for a tab bar (sizeof() 160 bytes) @@ -3670,7 +3681,7 @@ namespace ImGui IMGUI_API void DockContextQueueUndockWindow(ImGuiContext* ctx, ImGuiWindow* window); IMGUI_API void DockContextQueueUndockNode(ImGuiContext* ctx, ImGuiDockNode* node); IMGUI_API void DockContextProcessUndockWindow(ImGuiContext* ctx, ImGuiWindow* window, bool clear_persistent_docking_ref = true); - IMGUI_API void DockContextProcessUndockNode(ImGuiContext* ctx, ImGuiDockNode* node); + IMGUI_API ImGuiDockNode* DockContextProcessUndockNode(ImGuiContext* ctx, ImGuiDockNode* node); IMGUI_API bool DockContextCalcDropPosForDocking(ImGuiWindow* target, ImGuiDockNode* target_node, ImGuiWindow* payload_window, ImGuiDockNode* payload_node, ImGuiDir split_dir, bool split_outer, ImVec2* out_pos); IMGUI_API ImGuiDockNode*DockContextFindNodeByID(ImGuiContext* ctx, ImGuiID id); IMGUI_API void DockNodeWindowMenuHandler_Default(ImGuiContext* ctx, ImGuiDockNode* node, ImGuiTabBar* tab_bar); diff --git a/src/ThirdParty/imgui/imgui_widgets.cpp b/src/ThirdParty/imgui/imgui_widgets.cpp index 8fca84e..52c0df4 100644 --- a/src/ThirdParty/imgui/imgui_widgets.cpp +++ b/src/ThirdParty/imgui/imgui_widgets.cpp @@ -9724,6 +9724,7 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar) { ImGuiContext& g = *GImGui; tab_bar->WantLayout = false; + const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount); // Track selected tab when resizing our parent down const bool scroll_to_selected_tab = (tab_bar->BarRectPrevWidth > tab_bar->BarRect.GetWidth()); @@ -9927,11 +9928,20 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar) section_tab_index += section->TabCount; } + const float anim_t = ImSaturate(g.IO.DeltaTime * 14.0f); + for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; + if (tab->OffsetAnim < 0.0f || tab_bar_appearing) + tab->OffsetAnim = tab->Offset; + else + tab->OffsetAnim = ImLerp(tab->OffsetAnim, tab->Offset, anim_t); + } + // Clear name buffers tab_bar->TabsNames.Buf.resize(0); // If we have lost the selected tab, select the next most recently active one - const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount); if (found_selected_tab_id == false && !tab_bar_appearing) tab_bar->SelectedTabId = 0; if (tab_bar->SelectedTabId == 0 && tab_bar->NextSelectedTabId == 0 && most_recently_selected_tab != NULL) @@ -10551,10 +10561,11 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, // Layout const bool is_central_section = (tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0; size.x = tab->Width; + const float render_offset = (tab->OffsetAnim >= 0.0f) ? tab->OffsetAnim : tab->Offset; if (is_central_section) - window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_TRUNC(tab->Offset - tab_bar->ScrollingAnim), 0.0f); + window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_TRUNC(render_offset - tab_bar->ScrollingAnim), 0.0f); else - window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(tab->Offset, 0.0f); + window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(render_offset, 0.0f); ImVec2 pos = window->DC.CursorPos; ImRect bb(pos, pos + size); diff --git a/src/WinView/Window.cpp b/src/WinView/Window.cpp index 97b561d..b39b583 100644 --- a/src/WinView/Window.cpp +++ b/src/WinView/Window.cpp @@ -39,6 +39,7 @@ GLFWwindow *Window::makeWindow() { } glfwMakeContextCurrent(window); + glfwSwapInterval(0); if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cerr << "Failed to initialize GLAD\n"; diff --git a/src/main_player.cpp b/src/main_player.cpp new file mode 100644 index 0000000..456481c --- /dev/null +++ b/src/main_player.cpp @@ -0,0 +1,52 @@ +#include "Engine.h" +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#elif defined(__APPLE__) +#include +#else +#include +#endif + +static std::filesystem::path getExecutableDir() { +#if defined(_WIN32) + char pathBuf[MAX_PATH] = {}; + DWORD len = GetModuleFileNameA(nullptr, pathBuf, MAX_PATH); + if (len == 0 || len == MAX_PATH) {return {};} + return std::filesystem::path(pathBuf).parent_path(); +#elif defined(__APPLE__) + uint32_t size = 0; + if (_NSGetExecutablePath(nullptr, &size) != -1 || size == 0) {return {};} + std::string buf(size, '\0'); + if (_NSGetExecutablePath(buf.data(), &size) != 0) {return {};} + return std::filesystem::path(buf).lexically_normal().parent_path(); +#else + std::vector buf(4096, '\0'); + ssize_t len = readlink("/proc/self/exe", buf.data(), buf.size() - 1); + if (len <= 0) {return {};} + buf[static_cast(len)] = '\0'; + return std::filesystem::path(buf.data()).parent_path(); +#endif +} + +int main() { + if (auto exeDir = getExecutableDir(); !exeDir.empty()) { + std::error_code ec; + std::filesystem::current_path(exeDir, ec); + if (ec) { + std::cerr << "[WARN] Failed to set working dir to executable: " + << ec.message() << std::endl; + } + } + + Engine engine; + if (!engine.init()) {return -1;} + + engine.run(); + engine.shutdown(); + return 0; +}