From 303b835ba7f7b10dcef4df4c148432909eb61cd8 Mon Sep 17 00:00:00 2001 From: Anemunt Date: Thu, 22 Jan 2026 12:30:53 -0500 Subject: [PATCH] First Commit on new Git-Base, yey! --- .gitignore | 1 + CMakeLists.txt | 103 +- Resources/Fonts/TheSunset.ttf | Bin 0 -> 28152 bytes Resources/Fonts/Thesunsethd-Regular (1).ttf | Bin 0 -> 16296 bytes Resources/Shaders/skinned_vert.glsl | 44 + Resources/anim.ini | 125 + Resources/imgui.ini | 101 +- Resources/scripter.ini | 121 + Scripts/AnimationWindow.cpp | 282 ++ Scripts/Managed/ModuCPP.cs | 297 ++ Scripts/Managed/ModuCPP.csproj | 10 + Scripts/Managed/SampleInspector.cs | 68 + Scripts/Managed/SampleInspectorManaged.cs | 68 + .../bin/Debug/net10.0/ModuCPP.deps.json | 23 + Scripts/Managed/bin/Debug/net10.0/ModuCPP.dll | Bin 0 -> 12800 bytes Scripts/Managed/bin/Debug/net10.0/ModuCPP.pdb | Bin 0 -> 14044 bytes .../Debug/net10.0/ModuCPP.runtimeconfig.json | 14 + .../Debug/netstandard2.0/ModuCPP.deps.json | 24 + .../bin/Debug/netstandard2.0/ModuCPP.dll | Bin 0 -> 16896 bytes .../bin/Debug/netstandard2.0/ModuCPP.pdb | Bin 0 -> 11372 bytes ...oreApp,Version=v10.0.AssemblyAttributes.cs | 4 + .../obj/Debug/net10.0/ModuCPP.AssemblyInfo.cs | 22 + .../net10.0/ModuCPP.AssemblyInfoInputs.cache | 1 + ....GeneratedMSBuildEditorConfig.editorconfig | 17 + .../Debug/net10.0/ModuCPP.GlobalUsings.g.cs | 8 + .../obj/Debug/net10.0/ModuCPP.assets.cache | Bin 0 -> 151 bytes .../ModuCPP.csproj.CoreCompileInputs.cache | 1 + .../ModuCPP.csproj.FileListAbsolute.txt | 14 + Scripts/Managed/obj/Debug/net10.0/ModuCPP.dll | Bin 0 -> 12800 bytes .../net10.0/ModuCPP.genruntimeconfig.cache | 1 + Scripts/Managed/obj/Debug/net10.0/ModuCPP.pdb | Bin 0 -> 14044 bytes .../obj/Debug/net10.0/ModuCPP.sourcelink.json | 1 + .../Managed/obj/Debug/net10.0/ref/ModuCPP.dll | Bin 0 -> 8704 bytes .../obj/Debug/net10.0/refint/ModuCPP.dll | Bin 0 -> 8704 bytes ...tandard,Version=v2.0.AssemblyAttributes.cs | 4 + .../netstandard2.0/ModuCPP.AssemblyInfo.cs | 22 + .../ModuCPP.AssemblyInfoInputs.cache | 1 + ....GeneratedMSBuildEditorConfig.editorconfig | 8 + .../netstandard2.0/ModuCPP.GlobalUsings.g.cs | 8 + .../Debug/netstandard2.0/ModuCPP.assets.cache | Bin 0 -> 411 bytes .../ModuCPP.csproj.AssemblyReference.cache | Bin 0 -> 51232 bytes .../ModuCPP.csproj.CoreCompileInputs.cache | 1 + .../ModuCPP.csproj.FileListAbsolute.txt | 11 + .../obj/Debug/netstandard2.0/ModuCPP.dll | Bin 0 -> 16896 bytes .../obj/Debug/netstandard2.0/ModuCPP.pdb | Bin 0 -> 11372 bytes .../netstandard2.0/ModuCPP.sourcelink.json | 1 + .../obj/ModuCPP.csproj.nuget.dgspec.json | 70 + .../Managed/obj/ModuCPP.csproj.nuget.g.props | 15 + .../obj/ModuCPP.csproj.nuget.g.targets | 6 + Scripts/Managed/obj/project.assets.json | 247 ++ Scripts/Managed/obj/project.nuget.cache | 11 + Scripts/TopDownMovement2D.cpp | 74 + TheSunset.ttf | Bin 0 -> 28152 bytes Thesunsethd-Regular (1).ttf | Bin 0 -> 16296 bytes build.sh | 46 +- docs/Scripting.md | 24 + docs/mono-embedding.md | 17 + include/Shaders/Shader.h | 1 + src/AudioSystem.cpp | 366 +- src/AudioSystem.h | 55 + src/EditorUI.cpp | 164 +- src/EditorUI.h | 3 + src/EditorWindows/AnimationWindow.cpp | 554 +++ src/EditorWindows/BuildSettingsWindow.cpp | 253 ++ src/EditorWindows/FileBrowserWindow.cpp | 238 +- src/EditorWindows/ProjectManagerWindow.cpp | 66 +- src/EditorWindows/SceneWindows.cpp | 1437 +++++++- src/EditorWindows/ScriptingWindow.cpp | 538 +++ src/EditorWindows/ViewportWindows.cpp | 2630 +++++++++++++- src/Engine.cpp | 3052 +++++++++++++++- src/Engine.h | 181 + src/ManagedBindings.cpp | 145 + src/ManagedBindings.h | 55 + src/ManagedScriptRuntime.cpp | 452 +++ src/ManagedScriptRuntime.h | 50 + src/ModelLoader.cpp | 615 +++- src/ModelLoader.h | 61 + src/PhysicsSystem.cpp | 27 +- src/ProjectManager.cpp | 1012 ++++-- src/ProjectManager.h | 12 +- src/Rendering.cpp | 363 +- src/Rendering.h | 16 + src/SceneObject.h | 195 +- src/ScriptCompiler.cpp | 45 +- src/ScriptRuntime.cpp | 27 +- src/Shaders/Shader_Manager/Shader.cpp | 6 + .../ImGuiColorTextEdit/TextEditor.cpp | 3224 +++++++++++++++++ .../ImGuiColorTextEdit/TextEditor.h | 401 ++ src/ThirdParty/imgui/imgui.cpp | 145 +- src/ThirdParty/imgui/imgui_internal.h | 15 +- src/ThirdParty/imgui/imgui_widgets.cpp | 17 +- src/WinView/Window.cpp | 1 + src/main_player.cpp | 52 + 93 files changed, 17252 insertions(+), 1138 deletions(-) create mode 100644 Resources/Fonts/TheSunset.ttf create mode 100644 Resources/Fonts/Thesunsethd-Regular (1).ttf create mode 100644 Resources/Shaders/skinned_vert.glsl create mode 100644 Resources/anim.ini create mode 100644 Resources/scripter.ini create mode 100644 Scripts/AnimationWindow.cpp create mode 100644 Scripts/Managed/ModuCPP.cs create mode 100644 Scripts/Managed/ModuCPP.csproj create mode 100644 Scripts/Managed/SampleInspector.cs create mode 100644 Scripts/Managed/SampleInspectorManaged.cs create mode 100644 Scripts/Managed/bin/Debug/net10.0/ModuCPP.deps.json create mode 100644 Scripts/Managed/bin/Debug/net10.0/ModuCPP.dll create mode 100644 Scripts/Managed/bin/Debug/net10.0/ModuCPP.pdb create mode 100644 Scripts/Managed/bin/Debug/net10.0/ModuCPP.runtimeconfig.json create mode 100644 Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.deps.json create mode 100644 Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.dll create mode 100644 Scripts/Managed/bin/Debug/netstandard2.0/ModuCPP.pdb create mode 100644 Scripts/Managed/obj/Debug/net10.0/.NETCoreApp,Version=v10.0.AssemblyAttributes.cs create mode 100644 Scripts/Managed/obj/Debug/net10.0/ModuCPP.AssemblyInfo.cs create mode 100644 Scripts/Managed/obj/Debug/net10.0/ModuCPP.AssemblyInfoInputs.cache create mode 100644 Scripts/Managed/obj/Debug/net10.0/ModuCPP.GeneratedMSBuildEditorConfig.editorconfig create mode 100644 Scripts/Managed/obj/Debug/net10.0/ModuCPP.GlobalUsings.g.cs create mode 100644 Scripts/Managed/obj/Debug/net10.0/ModuCPP.assets.cache create mode 100644 Scripts/Managed/obj/Debug/net10.0/ModuCPP.csproj.CoreCompileInputs.cache create mode 100644 Scripts/Managed/obj/Debug/net10.0/ModuCPP.csproj.FileListAbsolute.txt create mode 100644 Scripts/Managed/obj/Debug/net10.0/ModuCPP.dll create mode 100644 Scripts/Managed/obj/Debug/net10.0/ModuCPP.genruntimeconfig.cache create mode 100644 Scripts/Managed/obj/Debug/net10.0/ModuCPP.pdb create mode 100644 Scripts/Managed/obj/Debug/net10.0/ModuCPP.sourcelink.json create mode 100644 Scripts/Managed/obj/Debug/net10.0/ref/ModuCPP.dll create mode 100644 Scripts/Managed/obj/Debug/net10.0/refint/ModuCPP.dll create mode 100644 Scripts/Managed/obj/Debug/netstandard2.0/.NETStandard,Version=v2.0.AssemblyAttributes.cs create mode 100644 Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.AssemblyInfo.cs create mode 100644 Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.AssemblyInfoInputs.cache create mode 100644 Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.GeneratedMSBuildEditorConfig.editorconfig create mode 100644 Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.GlobalUsings.g.cs create mode 100644 Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.assets.cache create mode 100644 Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.csproj.AssemblyReference.cache create mode 100644 Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.csproj.CoreCompileInputs.cache create mode 100644 Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.csproj.FileListAbsolute.txt create mode 100644 Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.dll create mode 100644 Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.pdb create mode 100644 Scripts/Managed/obj/Debug/netstandard2.0/ModuCPP.sourcelink.json create mode 100644 Scripts/Managed/obj/ModuCPP.csproj.nuget.dgspec.json create mode 100644 Scripts/Managed/obj/ModuCPP.csproj.nuget.g.props create mode 100644 Scripts/Managed/obj/ModuCPP.csproj.nuget.g.targets create mode 100644 Scripts/Managed/obj/project.assets.json create mode 100644 Scripts/Managed/obj/project.nuget.cache create mode 100644 Scripts/TopDownMovement2D.cpp create mode 100644 TheSunset.ttf create mode 100644 Thesunsethd-Regular (1).ttf create mode 100644 docs/mono-embedding.md create mode 100644 src/EditorWindows/AnimationWindow.cpp create mode 100644 src/EditorWindows/BuildSettingsWindow.cpp create mode 100644 src/EditorWindows/ScriptingWindow.cpp create mode 100644 src/ManagedBindings.cpp create mode 100644 src/ManagedBindings.h create mode 100644 src/ManagedScriptRuntime.cpp create mode 100644 src/ManagedScriptRuntime.h create mode 100644 src/ThirdParty/ImGuiColorTextEdit/TextEditor.cpp create mode 100644 src/ThirdParty/ImGuiColorTextEdit/TextEditor.h create mode 100644 src/main_player.cpp 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 0000000000000000000000000000000000000000..c90394089a7dd93736127a5e499825ec1da1204b GIT binary patch literal 28152 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 F{twVVUT6RS literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..37dc75153eae6347f04d89df0f5dfd3b94a40c72 GIT binary patch literal 12800 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 O;70`)l)-OGaQq+Sh@mk6 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7da467903cf722f8f5262981d708e9f3328fa1e7 GIT binary patch literal 16896 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..466b41251be0d3dfb2d094601a722799ee61be43 GIT binary patch literal 11372 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 LLc&MajfnpNFR#Z7 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1900c4eb90c87f33d8602768b994a9478c018856 GIT binary patch literal 151 zcmWIWc6a1qU|=YmJFA}U$y2>Y+iskejGMaa*4aP80ovxuZx@~DT#-{hb23m3v49b% mP) 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 O;70`)l)-OGaQq+Sh@mk6 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d39b56d291571747f0ddfaabc3908a209daaa716 GIT binary patch literal 8704 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 F{{g^0PEY^< literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d39b56d291571747f0ddfaabc3908a209daaa716 GIT binary patch literal 8704 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 F{{g^0PEY^< literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a8e6a3ec35c2b6d56455111e758fa1b6f6d05b24 GIT binary patch literal 411 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 YNbi&}g~RK-8@;&>cOLTh6w*UYD literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7da467903cf722f8f5262981d708e9f3328fa1e7 GIT binary patch literal 16896 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..466b41251be0d3dfb2d094601a722799ee61be43 GIT binary patch literal 11372 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 LLc&MajfnpNFR#Z7 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c90394089a7dd93736127a5e499825ec1da1204b GIT binary patch literal 28152 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 F{twVVUT6RS literal 0 HcmV?d00001 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; +}