Added OGG Support + 2.5D support
This commit is contained in:
@@ -74,6 +74,10 @@ set(ASSIMP_BUILD_DOCS OFF CACHE BOOL "Build Assimp documentation" FORCE)
|
||||
|
||||
add_subdirectory(src/ThirdParty/glfw EXCLUDE_FROM_ALL)
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(PkgConfig QUIET)
|
||||
if(PkgConfig_FOUND)
|
||||
pkg_check_modules(SNDFILE QUIET IMPORTED_TARGET sndfile)
|
||||
endif()
|
||||
|
||||
# ==================== Mono (managed scripting) ====================
|
||||
option(MODULARITY_USE_MONO "Enable Mono embedding for managed scripts" ON)
|
||||
@@ -170,7 +174,14 @@ set(ASSIMP_BUILD_TESTS OFF CACHE BOOL "Disable Assimp tests" FORCE)
|
||||
set(ASSIMP_BUILD_SAMPLES OFF CACHE BOOL "Disable Assimp samples" FORCE)
|
||||
set(ASSIMP_BUILD_ASSIMP_TOOLS OFF CACHE BOOL "Disable Assimp tools" FORCE)
|
||||
set(ASSIMP_INSTALL OFF CACHE BOOL "Disable Assimp install targets" FORCE)
|
||||
# Force vendored zlib/minizip in assimp to avoid host minizip/unzip.h dependency on clean builds.
|
||||
set(ASSIMP_BUILD_ZLIB ON CACHE BOOL "Build Assimp zlib/minizip from source" FORCE)
|
||||
set(ASSIMP_BUILD_MINIZIP ON CACHE BOOL "Build Assimp minizip from source" FORCE)
|
||||
# Some distros expose broken minizip pkg-config include dirs (/usr/include instead of /usr/include/minizip).
|
||||
# Ensure assimp does not prefer that cached result.
|
||||
set(UNZIP_FOUND FALSE CACHE INTERNAL "Force Assimp to use vendored unzip headers" FORCE)
|
||||
add_subdirectory(src/ThirdParty/assimp EXCLUDE_FROM_ALL)
|
||||
target_include_directories(assimp PRIVATE ${PROJECT_SOURCE_DIR}/src/ThirdParty/assimp/contrib/unzip)
|
||||
target_link_libraries(core PUBLIC assimp)
|
||||
target_include_directories(core PUBLIC
|
||||
${PROJECT_SOURCE_DIR}/src
|
||||
@@ -187,9 +198,13 @@ if(MODULARITY_USE_MONO)
|
||||
endif()
|
||||
target_compile_definitions(core PUBLIC MODULARITY_USE_MONO=$<BOOL:${MODULARITY_USE_MONO}>)
|
||||
target_compile_definitions(core PUBLIC MODULARITY_HAS_VULKAN=$<BOOL:${MODULARITY_HAS_VULKAN}>)
|
||||
target_compile_definitions(core PUBLIC MODULARITY_HAS_SNDFILE=$<BOOL:${SNDFILE_FOUND}>)
|
||||
if(WIN32)
|
||||
target_compile_definitions(core PUBLIC ${MODULARITY_WINDOWS_DEFINES})
|
||||
endif()
|
||||
if(SNDFILE_FOUND)
|
||||
target_link_libraries(core PUBLIC PkgConfig::SNDFILE)
|
||||
endif()
|
||||
target_compile_options(core PRIVATE ${MODULARITY_WARNING_FLAGS})
|
||||
|
||||
add_library(core_player STATIC ${ENGINE_SOURCES} ${ENGINE_HEADERS})
|
||||
@@ -210,9 +225,13 @@ if(MODULARITY_USE_MONO)
|
||||
endif()
|
||||
target_compile_definitions(core_player PUBLIC MODULARITY_USE_MONO=$<BOOL:${MODULARITY_USE_MONO}>)
|
||||
target_compile_definitions(core_player PUBLIC MODULARITY_HAS_VULKAN=$<BOOL:${MODULARITY_HAS_VULKAN}>)
|
||||
target_compile_definitions(core_player PUBLIC MODULARITY_HAS_SNDFILE=$<BOOL:${SNDFILE_FOUND}>)
|
||||
if(WIN32)
|
||||
target_compile_definitions(core_player PUBLIC ${MODULARITY_WINDOWS_DEFINES})
|
||||
endif()
|
||||
if(SNDFILE_FOUND)
|
||||
target_link_libraries(core_player PUBLIC PkgConfig::SNDFILE)
|
||||
endif()
|
||||
target_compile_options(core_player PRIVATE ${MODULARITY_WARNING_FLAGS})
|
||||
|
||||
if(MODULARITY_ENABLE_PHYSX)
|
||||
@@ -307,17 +326,24 @@ else()
|
||||
target_link_libraries(ModularityPlayer PRIVATE core_player glfw OpenGL::GL)
|
||||
endif()
|
||||
|
||||
add_custom_command(TARGET ModularityPlayer POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/Resources
|
||||
$<TARGET_FILE_DIR:ModularityPlayer>/Resources
|
||||
)
|
||||
|
||||
# Copy resources once to avoid parallel target races writing the same directory.
|
||||
if(MODULARITY_BUILD_EDITOR)
|
||||
add_custom_command(TARGET Modularity POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/Resources
|
||||
$<TARGET_FILE_DIR:Modularity>/Resources
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/Template-Projects
|
||||
$<TARGET_FILE_DIR:Modularity>/Template-Projects
|
||||
)
|
||||
else()
|
||||
add_custom_command(TARGET ModularityPlayer POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/Resources
|
||||
$<TARGET_FILE_DIR:ModularityPlayer>/Resources
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/Template-Projects
|
||||
$<TARGET_FILE_DIR:ModularityPlayer>/Template-Projects
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -374,6 +400,10 @@ install(DIRECTORY ${CMAKE_SOURCE_DIR}/Resources
|
||||
DESTINATION ./bin/
|
||||
)
|
||||
|
||||
install(DIRECTORY ${CMAKE_SOURCE_DIR}/Template-Projects
|
||||
DESTINATION ./bin/
|
||||
)
|
||||
|
||||
# ==================== Packaging (CPack) ====================
|
||||
|
||||
set(CPACK_PACKAGE_NAME "Modularity")
|
||||
|
||||
@@ -15,6 +15,7 @@ uniform mat4 projection;
|
||||
uniform mat4 bones[256];
|
||||
uniform int boneCount;
|
||||
uniform bool useSkinning;
|
||||
uniform vec4 uvRect;
|
||||
|
||||
void main()
|
||||
{
|
||||
@@ -38,7 +39,7 @@ void main()
|
||||
vec4 worldPos = model * localPos;
|
||||
FragPos = vec3(worldPos);
|
||||
Normal = mat3(transpose(inverse(model))) * localNormal;
|
||||
TexCoord = aTexCoord;
|
||||
TexCoord = uvRect.xy + aTexCoord * uvRect.zw;
|
||||
|
||||
gl_Position = projection * view * worldPos;
|
||||
}
|
||||
|
||||
@@ -10,12 +10,13 @@ out vec2 TexCoord;
|
||||
uniform mat4 model;
|
||||
uniform mat4 view;
|
||||
uniform mat4 projection;
|
||||
uniform vec4 uvRect;
|
||||
|
||||
void main()
|
||||
{
|
||||
FragPos = vec3(model * vec4(aPos, 1.0));
|
||||
Normal = mat3(transpose(inverse(model))) * aNormal;
|
||||
TexCoord = aTexCoord;
|
||||
TexCoord = uvRect.xy + aTexCoord * uvRect.zw;
|
||||
|
||||
gl_Position = projection * view * vec4(FragPos, 1.0);
|
||||
}
|
||||
|
||||
0
Template-Projects/.gitkeep
Normal file
0
Template-Projects/.gitkeep
Normal file
277
Template-Projects/3D OpenGL Template/Assets/Scenes/Main.scene
Normal file
277
Template-Projects/3D OpenGL Template/Assets/Scenes/Main.scene
Normal file
@@ -0,0 +1,277 @@
|
||||
# Scene File
|
||||
version=19
|
||||
nextId=3
|
||||
timeOfDay=0
|
||||
objectCount=3
|
||||
|
||||
[Object]
|
||||
id=0
|
||||
name=Cube 0
|
||||
type=0
|
||||
enabled=1
|
||||
layer=0
|
||||
tag=Untagged
|
||||
hasRenderer=1
|
||||
renderType=1
|
||||
hasLight=0
|
||||
hasCamera=0
|
||||
hasPostFX=0
|
||||
hasUI=0
|
||||
uiType=0
|
||||
parentId=-1
|
||||
position=0,0,0
|
||||
rotation=0,0,0
|
||||
scale=1,1,1
|
||||
hasRigidbody=0
|
||||
hasRigidbody2D=0
|
||||
hasCollider2D=0
|
||||
hasParallaxLayer2D=0
|
||||
hasCameraFollow2D=0
|
||||
hasCollider=0
|
||||
hasPlayerController=0
|
||||
hasAudioSource=0
|
||||
hasReverbZone=0
|
||||
hasGroundBakedType=0
|
||||
hasObsticleObject=0
|
||||
hasAIAgent=0
|
||||
hasAnimation=0
|
||||
hasSkeletalAnimation=0
|
||||
materialColor=1,1,1
|
||||
materialAmbient=0.2
|
||||
materialSpecular=0.5
|
||||
materialShininess=32
|
||||
materialTextureMix=0.3
|
||||
materialTextureFilter=0
|
||||
materialPath=
|
||||
albedoTex=/home/anemunt/ModularityProjects/3D OpenGL Template/Assets/container.jpg
|
||||
overlayTex=
|
||||
normalMap=
|
||||
vertexShader=
|
||||
fragmentShader=
|
||||
useOverlay=0
|
||||
additionalMaterialCount=0
|
||||
scripts=0
|
||||
lightColor=1,1,1
|
||||
lightIntensity=1
|
||||
lightRange=10
|
||||
lightEdgeFade=0.2
|
||||
lightInner=15
|
||||
lightOuter=25
|
||||
lightSize=1,1
|
||||
lightCastShadows=0
|
||||
lightSoftShadows=1
|
||||
lightShadowBias=0.02
|
||||
lightShadowSoftness=0.04
|
||||
lightEnabled=1
|
||||
cameraType=0
|
||||
cameraFov=45
|
||||
cameraNear=0.1
|
||||
cameraFar=100
|
||||
cameraPostFX=1
|
||||
cameraUse2D=0
|
||||
cameraPixelsPerUnit=100
|
||||
uiAnchor=0
|
||||
uiPosition=0,0
|
||||
uiRotation=0
|
||||
uiSize=160,40
|
||||
uiSliderValue=0.5
|
||||
uiSliderMin=0
|
||||
uiSliderMax=1
|
||||
uiLabel=UI Element
|
||||
uiColor=1,1,1,1
|
||||
uiInteractable=1
|
||||
uiSliderStyle=0
|
||||
uiButtonStyle=0
|
||||
uiStylePreset=Default
|
||||
uiTextScale=1
|
||||
uiRenderIn3D=0
|
||||
uiRenderTargetSize=512,512
|
||||
uiSpriteSheetEnabled=0
|
||||
uiSpriteSheetGrid=1,1
|
||||
uiSpriteSheetFrame=0
|
||||
uiSpriteSheetFps=12
|
||||
uiSpriteSheetLoop=1
|
||||
scriptCount=0
|
||||
children=
|
||||
|
||||
[Object]
|
||||
id=1
|
||||
name=Camera 1
|
||||
type=9
|
||||
enabled=1
|
||||
layer=0
|
||||
tag=Untagged
|
||||
hasRenderer=0
|
||||
renderType=0
|
||||
hasLight=0
|
||||
hasCamera=1
|
||||
hasPostFX=0
|
||||
hasUI=0
|
||||
uiType=0
|
||||
parentId=-1
|
||||
position=2.9,2.5,3.5
|
||||
rotation=340,40,0
|
||||
scale=1,1,1
|
||||
hasRigidbody=0
|
||||
hasRigidbody2D=0
|
||||
hasCollider2D=0
|
||||
hasParallaxLayer2D=0
|
||||
hasCameraFollow2D=0
|
||||
hasCollider=0
|
||||
hasPlayerController=0
|
||||
hasAudioSource=0
|
||||
hasReverbZone=0
|
||||
hasGroundBakedType=0
|
||||
hasObsticleObject=0
|
||||
hasAIAgent=0
|
||||
hasAnimation=0
|
||||
hasSkeletalAnimation=0
|
||||
materialColor=1,1,1
|
||||
materialAmbient=0.2
|
||||
materialSpecular=0.5
|
||||
materialShininess=32
|
||||
materialTextureMix=0.3
|
||||
materialTextureFilter=0
|
||||
materialPath=
|
||||
albedoTex=
|
||||
overlayTex=
|
||||
normalMap=
|
||||
vertexShader=
|
||||
fragmentShader=
|
||||
useOverlay=0
|
||||
additionalMaterialCount=0
|
||||
scripts=0
|
||||
lightColor=1,1,1
|
||||
lightIntensity=1
|
||||
lightRange=10
|
||||
lightEdgeFade=0.2
|
||||
lightInner=15
|
||||
lightOuter=25
|
||||
lightSize=1,1
|
||||
lightCastShadows=0
|
||||
lightSoftShadows=1
|
||||
lightShadowBias=0.02
|
||||
lightShadowSoftness=0.04
|
||||
lightEnabled=1
|
||||
cameraType=1
|
||||
cameraFov=60
|
||||
cameraNear=0.1
|
||||
cameraFar=100
|
||||
cameraPostFX=1
|
||||
cameraUse2D=0
|
||||
cameraPixelsPerUnit=100
|
||||
uiAnchor=0
|
||||
uiPosition=0,0
|
||||
uiRotation=0
|
||||
uiSize=160,40
|
||||
uiSliderValue=0.5
|
||||
uiSliderMin=0
|
||||
uiSliderMax=1
|
||||
uiLabel=UI Element
|
||||
uiColor=1,1,1,1
|
||||
uiInteractable=1
|
||||
uiSliderStyle=0
|
||||
uiButtonStyle=0
|
||||
uiStylePreset=Default
|
||||
uiTextScale=1
|
||||
uiRenderIn3D=0
|
||||
uiRenderTargetSize=512,512
|
||||
uiSpriteSheetEnabled=0
|
||||
uiSpriteSheetGrid=1,1
|
||||
uiSpriteSheetFrame=0
|
||||
uiSpriteSheetFps=12
|
||||
uiSpriteSheetLoop=1
|
||||
scriptCount=0
|
||||
children=
|
||||
|
||||
[Object]
|
||||
id=2
|
||||
name=Directional Light 2
|
||||
type=5
|
||||
enabled=1
|
||||
layer=0
|
||||
tag=Untagged
|
||||
hasRenderer=0
|
||||
renderType=0
|
||||
hasLight=1
|
||||
hasCamera=0
|
||||
hasPostFX=0
|
||||
hasUI=0
|
||||
uiType=0
|
||||
parentId=-1
|
||||
position=-3.29997,3.99999,-3.9
|
||||
rotation=241.457,35,45
|
||||
scale=0.999999,1,1
|
||||
hasRigidbody=0
|
||||
hasRigidbody2D=0
|
||||
hasCollider2D=0
|
||||
hasParallaxLayer2D=0
|
||||
hasCameraFollow2D=0
|
||||
hasCollider=0
|
||||
hasPlayerController=0
|
||||
hasAudioSource=0
|
||||
hasReverbZone=0
|
||||
hasGroundBakedType=0
|
||||
hasObsticleObject=0
|
||||
hasAIAgent=0
|
||||
hasAnimation=0
|
||||
hasSkeletalAnimation=0
|
||||
materialColor=1,1,1
|
||||
materialAmbient=0.2
|
||||
materialSpecular=0.5
|
||||
materialShininess=32
|
||||
materialTextureMix=0.3
|
||||
materialTextureFilter=0
|
||||
materialPath=
|
||||
albedoTex=
|
||||
overlayTex=
|
||||
normalMap=
|
||||
vertexShader=
|
||||
fragmentShader=
|
||||
useOverlay=0
|
||||
additionalMaterialCount=0
|
||||
scripts=0
|
||||
lightColor=1,1,1
|
||||
lightType=0
|
||||
lightIntensity=1
|
||||
lightRange=10
|
||||
lightEdgeFade=0.2
|
||||
lightInner=15
|
||||
lightOuter=25
|
||||
lightSize=1,1
|
||||
lightCastShadows=0
|
||||
lightSoftShadows=1
|
||||
lightShadowBias=0.02
|
||||
lightShadowSoftness=0.04
|
||||
lightEnabled=1
|
||||
cameraType=0
|
||||
cameraFov=45
|
||||
cameraNear=0.1
|
||||
cameraFar=100
|
||||
cameraPostFX=1
|
||||
cameraUse2D=0
|
||||
cameraPixelsPerUnit=100
|
||||
uiAnchor=0
|
||||
uiPosition=0,0
|
||||
uiRotation=0
|
||||
uiSize=160,40
|
||||
uiSliderValue=0.5
|
||||
uiSliderMin=0
|
||||
uiSliderMax=1
|
||||
uiLabel=UI Element
|
||||
uiColor=1,1,1,1
|
||||
uiInteractable=1
|
||||
uiSliderStyle=0
|
||||
uiButtonStyle=0
|
||||
uiStylePreset=Default
|
||||
uiTextScale=1
|
||||
uiRenderIn3D=0
|
||||
uiRenderTargetSize=512,512
|
||||
uiSpriteSheetEnabled=0
|
||||
uiSpriteSheetGrid=1,1
|
||||
uiSpriteSheetFrame=0
|
||||
uiSpriteSheetFps=12
|
||||
uiSpriteSheetLoop=1
|
||||
scriptCount=0
|
||||
children=
|
||||
|
||||
BIN
Template-Projects/3D OpenGL Template/Assets/container.jpg
Normal file
BIN
Template-Projects/3D OpenGL Template/Assets/container.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 181 KiB |
@@ -0,0 +1,102 @@
|
||||
# Editor UI settings
|
||||
uiStyle=Default
|
||||
uiAnimationMode=Fluid
|
||||
workspace=Default
|
||||
fileBrowserIconScale=1.0000
|
||||
fileBrowserViewMode=Grid
|
||||
fileBrowserSidebarWidth=150.0000
|
||||
fileBrowserSidebarVisible=1
|
||||
consoleWrapText=1
|
||||
showAnimationWindow=0
|
||||
showAIPathfindingWindow=0
|
||||
showSceneGizmos=1
|
||||
gizmoShowCameraOverlays=1
|
||||
gizmoShowCameraFrustumLabels=1
|
||||
gizmoShowLightOverlays=1
|
||||
gizmoShowLightIntensityLabels=1
|
||||
sceneGizmoIconScale=1.0000
|
||||
sceneGizmoOverlayScale=1.0000
|
||||
showSceneGrid3D=0
|
||||
showCanvasOverlay=0
|
||||
showUIWorldGrid=1
|
||||
showSpritePreviewPanel=1
|
||||
pixelGridSnapEnabled=1
|
||||
pixelGridSnapStep=1
|
||||
showGameProfiler=1
|
||||
collisionWireframe=0
|
||||
fpsCapEnabled=0
|
||||
fpsCap=120.0000
|
||||
cameraMoveSpeed=5.0000
|
||||
cameraSprintSpeed=10.0000
|
||||
cameraSmoothMovement=1
|
||||
cameraAcceleration=15.0000
|
||||
cameraMouseSensitivity=0.1000
|
||||
gameViewportResolutionIndex=0
|
||||
gameViewportCustomWidth=1920
|
||||
gameViewportCustomHeight=1080
|
||||
gameViewportAutoFit=1
|
||||
gameViewportZoom=1.0000
|
||||
scriptAutoCompileInterval=0.5000
|
||||
scriptAutoCompileOnSave=1
|
||||
color.Text=0.9200,0.9300,0.9700,1.0000
|
||||
color.TextDisabled=0.6000,0.6200,0.7000,1.0000
|
||||
color.WindowBg=0.1100,0.1200,0.1900,1.0000
|
||||
color.ChildBg=0.1600,0.1600,0.2400,1.0000
|
||||
color.PopupBg=0.1000,0.1100,0.1700,0.9800
|
||||
color.Border=0.2200,0.2300,0.3400,0.7000
|
||||
color.BorderShadow=0.0000,0.0000,0.0000,0.0000
|
||||
color.FrameBg=0.2000,0.2100,0.3000,1.0000
|
||||
color.FrameBgHovered=0.2600,0.2800,0.4000,1.0000
|
||||
color.FrameBgActive=0.3000,0.3400,0.4600,1.0000
|
||||
color.TitleBg=0.1100,0.1200,0.1800,1.0000
|
||||
color.TitleBgActive=0.1600,0.1700,0.2400,1.0000
|
||||
color.TitleBgCollapsed=0.0900,0.1000,0.1500,1.0000
|
||||
color.MenuBarBg=0.0900,0.1000,0.1600,1.0000
|
||||
color.ScrollbarBg=0.1100,0.1200,0.1800,1.0000
|
||||
color.ScrollbarGrab=0.2400,0.2600,0.3600,1.0000
|
||||
color.ScrollbarGrabHovered=0.3200,0.3500,0.4800,1.0000
|
||||
color.ScrollbarGrabActive=0.3600,0.4200,0.5800,1.0000
|
||||
color.CheckMark=0.4800,0.5600,0.8600,1.0000
|
||||
color.SliderGrab=0.4800,0.5600,0.8600,1.0000
|
||||
color.SliderGrabActive=0.3800,0.4600,0.7400,1.0000
|
||||
color.Button=0.2200,0.2300,0.3200,1.0000
|
||||
color.ButtonHovered=0.2800,0.3000,0.4200,1.0000
|
||||
color.ButtonActive=0.3300,0.3600,0.4800,1.0000
|
||||
color.Header=0.2200,0.2300,0.3400,1.0000
|
||||
color.HeaderHovered=0.2600,0.2800,0.3800,1.0000
|
||||
color.HeaderActive=0.2800,0.3000,0.4200,1.0000
|
||||
color.Separator=0.2200,0.2300,0.3400,1.0000
|
||||
color.SeparatorHovered=0.3400,0.3600,0.5200,1.0000
|
||||
color.SeparatorActive=0.4400,0.5000,0.7000,1.0000
|
||||
color.ResizeGrip=0.2800,0.3000,0.4200,1.0000
|
||||
color.ResizeGripHovered=0.3800,0.4400,0.6000,0.8000
|
||||
color.ResizeGripActive=0.4800,0.5600,0.8600,1.0000
|
||||
color.InputTextCursor=1.0000,1.0000,1.0000,1.0000
|
||||
color.TabHovered=0.3000,0.3400,0.4800,1.0000
|
||||
color.Tab=0.1500,0.1600,0.2400,1.0000
|
||||
color.TabSelected=0.2000,0.2200,0.3200,1.0000
|
||||
color.TabSelectedOverline=0.2600,0.5900,0.9800,1.0000
|
||||
color.TabDimmed=0.1100,0.1200,0.1800,1.0000
|
||||
color.TabDimmedSelected=0.1600,0.1800,0.2600,1.0000
|
||||
color.TabDimmedSelectedOverline=0.5000,0.5000,0.5000,0.0000
|
||||
color.DockingPreview=0.4800,0.5600,0.8600,0.4500
|
||||
color.DockingEmptyBg=0.0800,0.0900,0.1400,1.0000
|
||||
color.PlotLines=0.6100,0.6100,0.6100,1.0000
|
||||
color.PlotLinesHovered=1.0000,0.4300,0.3500,1.0000
|
||||
color.PlotHistogram=0.9000,0.7000,0.0000,1.0000
|
||||
color.PlotHistogramHovered=1.0000,0.6000,0.0000,1.0000
|
||||
color.TableHeaderBg=0.2000,0.2200,0.3200,1.0000
|
||||
color.TableBorderStrong=0.3100,0.3100,0.3500,1.0000
|
||||
color.TableBorderLight=0.2300,0.2300,0.2500,1.0000
|
||||
color.TableRowBg=0.0000,0.0000,0.0000,0.0000
|
||||
color.TableRowBgAlt=1.0000,1.0000,1.0000,0.0600
|
||||
color.TextLink=0.2600,0.5900,0.9800,1.0000
|
||||
color.TextSelectedBg=0.4800,0.5600,0.8600,0.2400
|
||||
color.TreeLines=0.4300,0.4300,0.5000,0.5000
|
||||
color.DragDropTarget=1.0000,1.0000,0.0000,0.9000
|
||||
color.DragDropTargetBg=0.0000,0.0000,0.0000,0.0000
|
||||
color.UnsavedMarker=1.0000,1.0000,1.0000,1.0000
|
||||
color.NavCursor=0.4800,0.5600,0.8600,1.0000
|
||||
color.NavWindowingHighlight=1.0000,1.0000,1.0000,0.7000
|
||||
color.NavWindowingDimBg=0.8000,0.8000,0.8000,0.2000
|
||||
color.ModalWindowDimBg=0.0500,0.0600,0.0900,0.7000
|
||||
@@ -0,0 +1,85 @@
|
||||
[Window][Sprite Timeline]
|
||||
Collapsed=0
|
||||
DockId=0x00000006
|
||||
|
||||
[Window][DockSpace]
|
||||
Pos=0,24
|
||||
Size=1908,1004
|
||||
Collapsed=0
|
||||
|
||||
[Window][Hierarchy]
|
||||
Pos=0,48
|
||||
Size=268,980
|
||||
Collapsed=0
|
||||
DockId=0x00000001,0
|
||||
|
||||
[Window][Inspector]
|
||||
Pos=1628,48
|
||||
Size=280,980
|
||||
Collapsed=0
|
||||
DockId=0x00000004,0
|
||||
|
||||
[Window][Project]
|
||||
Pos=268,799
|
||||
Size=1360,229
|
||||
Collapsed=0
|
||||
DockId=0x00000006,0
|
||||
|
||||
[Window][Environment]
|
||||
Pos=1628,48
|
||||
Size=280,980
|
||||
Collapsed=0
|
||||
DockId=0x00000004,1
|
||||
|
||||
[Window][Camera]
|
||||
Pos=0,48
|
||||
Size=268,980
|
||||
Collapsed=0
|
||||
DockId=0x00000001,1
|
||||
|
||||
[Window][Project Settings]
|
||||
Pos=268,799
|
||||
Size=1360,229
|
||||
Collapsed=0
|
||||
DockId=0x00000006,1
|
||||
|
||||
[Window][Viewport]
|
||||
Pos=268,48
|
||||
Size=1360,751
|
||||
Collapsed=0
|
||||
DockId=0x00000005,0
|
||||
|
||||
[Window][Sprite Preview]
|
||||
Pos=1628,48
|
||||
Size=280,980
|
||||
Collapsed=0
|
||||
DockId=0x00000004,2
|
||||
|
||||
[Window][Game Viewport]
|
||||
Pos=268,48
|
||||
Size=1360,751
|
||||
Collapsed=0
|
||||
DockId=0x00000005,1
|
||||
|
||||
[Window][Launcher]
|
||||
Pos=0,0
|
||||
Size=1342,841
|
||||
Collapsed=0
|
||||
|
||||
[Window][Debug##Default]
|
||||
Pos=60,60
|
||||
Size=400,400
|
||||
Collapsed=0
|
||||
|
||||
[Table][0x3AEBACB6,5]
|
||||
RefScale=16
|
||||
|
||||
[Docking][Data]
|
||||
DockSpace ID=0xD71539A0 Window=0x3DA2F1DE Pos=0,48 Size=1908,980 Split=X
|
||||
DockNode ID=0x00000001 Parent=0xD71539A0 SizeRef=268,817 Selected=0xBABDAE5E
|
||||
DockNode ID=0x00000002 Parent=0xD71539A0 SizeRef=1640,817 Split=X
|
||||
DockNode ID=0x00000003 Parent=0x00000002 SizeRef=1360,817 Split=Y
|
||||
DockNode ID=0x00000005 Parent=0x00000003 SizeRef=794,751 CentralNode=1 Selected=0xC450F867
|
||||
DockNode ID=0x00000006 Parent=0x00000003 SizeRef=794,229 Selected=0x9C21DE82
|
||||
DockNode ID=0x00000004 Parent=0x00000002 SizeRef=280,817 Selected=0x36DC96AB
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
24
Template-Projects/3D OpenGL Template/build.modu
Normal file
24
Template-Projects/3D OpenGL Template/build.modu
Normal file
@@ -0,0 +1,24 @@
|
||||
# build.modu
|
||||
platform=Linux
|
||||
architecture=x86_64
|
||||
companyName=DefaultCompany
|
||||
buildName=3D OpenGL Template
|
||||
version=0.1.0
|
||||
splashImage=
|
||||
splashEnabled=0
|
||||
splashDuration=2.5
|
||||
packageStandaloneArchive=1
|
||||
developmentBuild=0
|
||||
autoConnectProfiler=0
|
||||
scriptDebugging=0
|
||||
deepProfiling=0
|
||||
scriptsOnlyBuild=0
|
||||
serverBuild=0
|
||||
compressionMethod=Default
|
||||
rendererAmbient=0.2,0.2,0.2
|
||||
rendererShadowResolution=512
|
||||
rendererAutoReloadShaders=1
|
||||
editorCameraFov=45
|
||||
editorCameraNear=0.1
|
||||
editorCameraFar=100
|
||||
scene=Main,1
|
||||
7
Template-Projects/3D OpenGL Template/packages.modu
Normal file
7
Template-Projects/3D OpenGL Template/packages.modu
Normal file
@@ -0,0 +1,7 @@
|
||||
# Modularity package manifest
|
||||
# Add optional script-time dependencies here
|
||||
package=glm
|
||||
package=imgui
|
||||
package=imguizmo
|
||||
package=miniaudio
|
||||
git=soft-chimp-locomotion|Soft Chimp Locomotion|https://github.com/darkresident55/Soft-Chimp-Locomotion.git|Packages/soft-chimp-locomotion|Packages/soft-chimp-locomotion/.git|||
|
||||
4
Template-Projects/3D OpenGL Template/project.modu
Normal file
4
Template-Projects/3D OpenGL Template/project.modu
Normal file
@@ -0,0 +1,4 @@
|
||||
name=3D OpenGL Template
|
||||
lastScene=Main
|
||||
pipeline=3D
|
||||
renderer=OpenGL
|
||||
14
Template-Projects/3D OpenGL Template/scripts.modu
Normal file
14
Template-Projects/3D OpenGL Template/scripts.modu
Normal file
@@ -0,0 +1,14 @@
|
||||
# scripts.modu
|
||||
cppStandard=c++20
|
||||
scriptsDir=Assets/Scripts
|
||||
outDir=Library/CompiledScripts
|
||||
includeDir=/home/anemunt/Git-base/Modularity/build/src
|
||||
includeDir=/home/anemunt/Git-base/Modularity/build/include
|
||||
includeDir=/home/anemunt/Git-base/Modularity/build/src/ThirdParty
|
||||
includeDir=/home/anemunt/Git-base/Modularity/build/src/ThirdParty/glm
|
||||
define=MODU_SCRIPTING=1
|
||||
define=MODU_PROJECT_NAME="3D OpenGL Template"
|
||||
linux.linkLib=pthread
|
||||
linux.linkLib=dl
|
||||
win.linkLib=User32.lib
|
||||
win.linkLib=Advapi32.lib
|
||||
827
build.sh
827
build.sh
@@ -1,91 +1,782 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
start_time=$(date +%s)
|
||||
start_time="$(date +%s)"
|
||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
build_dir="${script_dir}/build"
|
||||
player_cache_dir="${build_dir}/player-cache"
|
||||
last_step="bootstrap"
|
||||
current_step=0
|
||||
total_steps=0
|
||||
build_started=0
|
||||
status_line_active=0
|
||||
status_current_label=""
|
||||
status_current_spinner="-"
|
||||
status_current_elapsed=0
|
||||
|
||||
finish() {
|
||||
local exit_code=$?
|
||||
local end_time=$(date +%s)
|
||||
local duration=$((end_time - start_time))
|
||||
declare -a build_warning_messages=()
|
||||
declare -a build_error_messages=()
|
||||
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
echo -e "================================\n Modularity - Native Linux Build Complete\n================================"
|
||||
echo -e "[Complete]: Your Modularity Build Completed in ${duration}s!\nThe build should be located under Modularity within another folder called 'Build'"
|
||||
else
|
||||
echo -e "================================\n Modularity - Native Linux Build Failed\n================================"
|
||||
echo "[Failed]: Your Modularity Build Failed after ${duration}s (exit code ${exit_code})."
|
||||
fi
|
||||
if [[ -t 1 ]]; then
|
||||
C_RESET=$'\033[0m'
|
||||
C_BOLD=$'\033[1m'
|
||||
C_DIM=$'\033[2m'
|
||||
C_GREEN=$'\033[32m'
|
||||
C_YELLOW=$'\033[33m'
|
||||
C_RED=$'\033[31m'
|
||||
C_CYAN=$'\033[36m'
|
||||
ICON_INFO="ℹ"
|
||||
ICON_WARN="⚠"
|
||||
ICON_ERROR="✖"
|
||||
ICON_OK="✔"
|
||||
else
|
||||
C_RESET=''
|
||||
C_BOLD=''
|
||||
C_DIM=''
|
||||
C_GREEN=''
|
||||
C_YELLOW=''
|
||||
C_RED=''
|
||||
C_CYAN=''
|
||||
ICON_INFO="[i]"
|
||||
ICON_WARN="[!]"
|
||||
ICON_ERROR="[x]"
|
||||
ICON_OK="[ok]"
|
||||
fi
|
||||
|
||||
exit $exit_code
|
||||
log_info() { printf "%s%s%s %s\n" "${C_CYAN}" "${ICON_INFO}" "${C_RESET}" "$*"; }
|
||||
log_warn() { printf "%s%s%s %s\n" "${C_YELLOW}" "${ICON_WARN}" "${C_RESET}" "$*"; record_warning "$*"; }
|
||||
log_error() { printf "%s%s%s %s\n" "${C_RED}" "${ICON_ERROR}" "${C_RESET}" "$*"; record_error "$*"; }
|
||||
log_ok() { printf "%s%s%s %s\n" "${C_GREEN}" "${ICON_OK}" "${C_RESET}" "$*"; }
|
||||
|
||||
record_warning() {
|
||||
build_warning_messages+=("$1")
|
||||
}
|
||||
|
||||
trap finish EXIT
|
||||
record_error() {
|
||||
build_error_messages+=("$1")
|
||||
}
|
||||
|
||||
echo -e "================================\n Modularity - Native Linux Builder\n================================"
|
||||
repeat_char() {
|
||||
local char="$1"
|
||||
local count="$2"
|
||||
if (( count <= 0 )); then
|
||||
return
|
||||
fi
|
||||
printf "%${count}s" "" | tr ' ' "$char"
|
||||
}
|
||||
|
||||
progress_prefix() {
|
||||
local width=24
|
||||
local filled=$((current_step * width / total_steps))
|
||||
local empty=$((width - filled))
|
||||
|
||||
printf "%s[%02d/%02d]%s [%s%s%s%s%s]" \
|
||||
"${C_DIM}" "${current_step}" "${total_steps}" "${C_RESET}" \
|
||||
"${C_GREEN}" "$(repeat_char "#" "${filled}")" "${C_DIM}" "$(repeat_char "-" "${empty}")" "${C_RESET}"
|
||||
}
|
||||
|
||||
clear_status_line() {
|
||||
if [[ -t 1 && "${status_line_active}" -eq 1 ]]; then
|
||||
printf "\r\033[2K"
|
||||
status_line_active=0
|
||||
fi
|
||||
}
|
||||
|
||||
render_status_line() {
|
||||
local label="$1"
|
||||
local spinner="$2"
|
||||
local elapsed="$3"
|
||||
|
||||
if [[ ! -t 1 ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
status_current_label="${label}"
|
||||
status_current_spinner="${spinner}"
|
||||
status_current_elapsed="${elapsed}"
|
||||
|
||||
printf "\r\033[2K%s %s%s%s %s %s(%ss)%s" \
|
||||
"$(progress_prefix)" \
|
||||
"${C_CYAN}" "${spinner}" "${C_RESET}" \
|
||||
"${label}" \
|
||||
"${C_DIM}" "${elapsed}" "${C_RESET}"
|
||||
|
||||
status_line_active=1
|
||||
}
|
||||
|
||||
emit_scrolling_event() {
|
||||
local level="$1"
|
||||
local message="$2"
|
||||
local had_status="${status_line_active}"
|
||||
|
||||
clear_status_line
|
||||
case "${level}" in
|
||||
info)
|
||||
printf "%s%s%s %s\n" "${C_CYAN}" "${ICON_INFO}" "${C_RESET}" "${message}"
|
||||
;;
|
||||
warn)
|
||||
printf "%s%s%s %s\n" "${C_YELLOW}" "${ICON_WARN}" "${C_RESET}" "${message}"
|
||||
;;
|
||||
error)
|
||||
printf "%s%s%s %s\n" "${C_RED}" "${ICON_ERROR}" "${C_RESET}" "${message}"
|
||||
;;
|
||||
ok)
|
||||
printf "%s%s%s %s\n" "${C_GREEN}" "${ICON_OK}" "${C_RESET}" "${message}"
|
||||
;;
|
||||
*)
|
||||
printf "%s\n" "${message}"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -t 1 && "${had_status}" -eq 1 ]]; then
|
||||
render_status_line "${status_current_label}" "${status_current_spinner}" "${status_current_elapsed}"
|
||||
fi
|
||||
}
|
||||
|
||||
is_interesting_info_line() {
|
||||
local lower="$1"
|
||||
[[ "${lower}" == *"built target"* ]] ||
|
||||
[[ "${lower}" == *"configuring done"* ]] ||
|
||||
[[ "${lower}" == *"generating done"* ]] ||
|
||||
[[ "${lower}" == *"build files have been written"* ]] ||
|
||||
[[ "${lower}" == *"installing:"* ]] ||
|
||||
[[ "${lower}" == *"up-to-date:"* ]] ||
|
||||
[[ "${lower}" == *"linking cxx executable"* ]] ||
|
||||
[[ "${lower}" == *"linking cxx static library"* ]] ||
|
||||
[[ "${lower}" == *"copying"* ]]
|
||||
}
|
||||
|
||||
process_build_output_line() {
|
||||
local step="$1"
|
||||
local line="$2"
|
||||
|
||||
line="${line%$'\r'}"
|
||||
if [[ -z "${line//[[:space:]]/}" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local lower="${line,,}"
|
||||
local issue_text="[${step}] ${line}"
|
||||
|
||||
if [[ "${lower}" == *"0 errors generated"* ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "${lower}" == *"warning:"* ]] || [[ "${lower}" == *" cmake warning"* ]] || [[ "${lower}" == *"warning "* ]]; then
|
||||
record_warning "${issue_text}"
|
||||
emit_scrolling_event "warn" "${issue_text}"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "${lower}" == *"error:"* ]] || [[ "${lower}" == *"fatal"* ]] || [[ "${lower}" == *"undefined reference"* ]] || [[ "${lower}" == *"collect2: error"* ]] || [[ "${lower}" == *"ld: cannot"* ]]; then
|
||||
record_error "${issue_text}"
|
||||
emit_scrolling_event "error" "${issue_text}"
|
||||
return
|
||||
fi
|
||||
|
||||
if is_interesting_info_line "${lower}"; then
|
||||
emit_scrolling_event "info" "[${step}] ${line}"
|
||||
fi
|
||||
}
|
||||
|
||||
print_issue_summary() {
|
||||
local max_items=8
|
||||
local i
|
||||
|
||||
if [[ "${#build_warning_messages[@]}" -gt 0 ]]; then
|
||||
printf "\n%sWarnings (%d):%s\n" "${C_YELLOW}" "${#build_warning_messages[@]}" "${C_RESET}"
|
||||
for ((i = 0; i < ${#build_warning_messages[@]} && i < max_items; i++)); do
|
||||
printf " %s %s\n" "${ICON_WARN}" "${build_warning_messages[$i]}"
|
||||
done
|
||||
if [[ "${#build_warning_messages[@]}" -gt "${max_items}" ]]; then
|
||||
printf " %s ... and %d more warning(s)\n" "${ICON_WARN}" "$(( ${#build_warning_messages[@]} - max_items ))"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${#build_error_messages[@]}" -gt 0 ]]; then
|
||||
printf "\n%sErrors (%d):%s\n" "${C_RED}" "${#build_error_messages[@]}" "${C_RESET}"
|
||||
for ((i = 0; i < ${#build_error_messages[@]} && i < max_items; i++)); do
|
||||
printf " %s %s\n" "${ICON_ERROR}" "${build_error_messages[$i]}"
|
||||
done
|
||||
if [[ "${#build_error_messages[@]}" -gt "${max_items}" ]]; then
|
||||
printf " %s ... and %d more error(s)\n" "${ICON_ERROR}" "$(( ${#build_error_messages[@]} - max_items ))"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
advance_step() {
|
||||
local label="$1"
|
||||
current_step=$((current_step + 1))
|
||||
last_step="${label}"
|
||||
}
|
||||
|
||||
run_step() {
|
||||
local label="$1"
|
||||
shift
|
||||
|
||||
advance_step "${label}"
|
||||
clear_status_line
|
||||
printf "\n%s %s\n" "$(progress_prefix)" "${label}"
|
||||
|
||||
"$@"
|
||||
}
|
||||
|
||||
run_long_step() {
|
||||
local label="$1"
|
||||
shift
|
||||
|
||||
advance_step "${label}"
|
||||
|
||||
if [[ ! -t 1 ]]; then
|
||||
printf "\n%s %s\n" "$(progress_prefix)" "${label}"
|
||||
"$@"
|
||||
return
|
||||
fi
|
||||
|
||||
local log_file
|
||||
log_file="$(mktemp "/tmp/modularity-build-step-${current_step}.XXXX.log")"
|
||||
local start_step
|
||||
start_step="$(date +%s)"
|
||||
local spinner_frames='-\|/'
|
||||
local spinner_index=0
|
||||
local processed_lines=0
|
||||
|
||||
set +e
|
||||
"$@" >"${log_file}" 2>&1 &
|
||||
local cmd_pid=$!
|
||||
render_status_line "${label}" "-" 0
|
||||
|
||||
while kill -0 "${cmd_pid}" >/dev/null 2>&1; do
|
||||
local total_lines
|
||||
total_lines="$(wc -l < "${log_file}" 2>/dev/null || echo 0)"
|
||||
if (( total_lines > processed_lines )); then
|
||||
while IFS= read -r line; do
|
||||
process_build_output_line "${label}" "${line}"
|
||||
done < <(sed -n "$((processed_lines + 1)),${total_lines}p" "${log_file}")
|
||||
processed_lines="${total_lines}"
|
||||
fi
|
||||
|
||||
local now elapsed frame
|
||||
now="$(date +%s)"
|
||||
elapsed=$((now - start_step))
|
||||
frame="${spinner_frames:spinner_index:1}"
|
||||
render_status_line "${label}" "${frame}" "${elapsed}"
|
||||
spinner_index=$(((spinner_index + 1) % 4))
|
||||
sleep 0.2
|
||||
done
|
||||
|
||||
wait "${cmd_pid}"
|
||||
local exit_code=$?
|
||||
set -e
|
||||
|
||||
local total_lines
|
||||
total_lines="$(wc -l < "${log_file}" 2>/dev/null || echo 0)"
|
||||
if (( total_lines > processed_lines )); then
|
||||
while IFS= read -r line; do
|
||||
process_build_output_line "${label}" "${line}"
|
||||
done < <(sed -n "$((processed_lines + 1)),${total_lines}p" "${log_file}")
|
||||
processed_lines="${total_lines}"
|
||||
fi
|
||||
|
||||
local now elapsed
|
||||
now="$(date +%s)"
|
||||
elapsed=$((now - start_step))
|
||||
|
||||
if [[ "${exit_code}" -ne 0 ]]; then
|
||||
clear_status_line
|
||||
record_error "[${label}] Step failed with exit code ${exit_code}. Log: ${log_file}"
|
||||
emit_scrolling_event "error" "$(progress_prefix) ${label} (failed)"
|
||||
log_error "Detailed log kept at: ${log_file}"
|
||||
return "${exit_code}"
|
||||
fi
|
||||
|
||||
clear_status_line
|
||||
emit_scrolling_event "ok" "$(progress_prefix) ${label} (done in ${elapsed}s)"
|
||||
rm -f "${log_file}"
|
||||
}
|
||||
|
||||
finish() {
|
||||
local exit_code="$1"
|
||||
set +e
|
||||
|
||||
if [[ "${build_started}" -eq 0 ]]; then
|
||||
return
|
||||
fi
|
||||
clear_status_line
|
||||
|
||||
local end_time
|
||||
end_time="$(date +%s)"
|
||||
local duration=$((end_time - start_time))
|
||||
echo
|
||||
|
||||
if [[ "${exit_code}" -eq 0 ]]; then
|
||||
printf "%s%s================================%s\n" "${C_RESET}" "${C_BOLD}" "${C_RESET}"
|
||||
printf "%s Modularity - Native Linux Build Complete%s\n" "${C_BOLD}" "${C_RESET}"
|
||||
printf "%s================================%s\n" "${C_BOLD}" "${C_RESET}"
|
||||
log_ok "Build completed in ${duration}s."
|
||||
log_info "Artifacts: ${build_dir}"
|
||||
print_issue_summary
|
||||
else
|
||||
printf "%s%s================================%s\n" "${C_RESET}" "${C_BOLD}" "${C_RESET}"
|
||||
printf "%s Modularity - Native Linux Build Failed%s\n" "${C_BOLD}" "${C_RESET}"
|
||||
printf "%s================================%s\n" "${C_BOLD}" "${C_RESET}"
|
||||
log_error "Build failed after ${duration}s at step: ${last_step} (exit code ${exit_code})."
|
||||
print_issue_summary
|
||||
fi
|
||||
}
|
||||
trap 'finish $?' EXIT
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./build.sh [options]
|
||||
Options:
|
||||
--clean Remove existing build directories first
|
||||
--build-type=<type> CMake build type (default: Release)
|
||||
--generator=<name> Force CMake generator (e.g. Ninja, "Unix Makefiles")
|
||||
--skip-deps Skip automatic dependency checks/install
|
||||
--help Show this help message
|
||||
EOF
|
||||
}
|
||||
|
||||
clean_build=0
|
||||
build_type="Release"
|
||||
skip_deps=0
|
||||
jobs="$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc 2>/dev/null || echo 4)"
|
||||
preferred_generator=""
|
||||
cmake_generator=""
|
||||
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "--clean" ]; then
|
||||
clean_build=1
|
||||
elif [[ "$arg" == --build-type=* ]]; then
|
||||
build_type="${arg#*=}"
|
||||
fi
|
||||
case "$arg" in
|
||||
--clean)
|
||||
clean_build=1
|
||||
;;
|
||||
--build-type=*)
|
||||
build_type="${arg#*=}"
|
||||
;;
|
||||
--generator=*)
|
||||
preferred_generator="${arg#*=}"
|
||||
;;
|
||||
--skip-deps)
|
||||
skip_deps=1
|
||||
;;
|
||||
--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_warn "Unknown argument: ${arg}"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
git submodule update --init --recursive
|
||||
pkg_manager=""
|
||||
pkg_prefix=()
|
||||
pkg_index_updated=0
|
||||
|
||||
if command -v ccache >/dev/null 2>&1; then
|
||||
export CCACHE_BASEDIR="$script_dir"
|
||||
export CCACHE_NOHASHDIR=1
|
||||
echo -e "[i]: ccache detected. Normalizing paths for cross-build cache hits."
|
||||
detect_package_manager() {
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
pkg_manager="apt"
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
pkg_manager="dnf"
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
pkg_manager="pacman"
|
||||
elif command -v zypper >/dev/null 2>&1; then
|
||||
pkg_manager="zypper"
|
||||
else
|
||||
pkg_manager=""
|
||||
fi
|
||||
|
||||
if [[ "$(id -u)" -eq 0 ]]; then
|
||||
pkg_prefix=()
|
||||
elif command -v sudo >/dev/null 2>&1; then
|
||||
pkg_prefix=(sudo)
|
||||
else
|
||||
pkg_prefix=()
|
||||
fi
|
||||
}
|
||||
|
||||
detect_cmake_generator() {
|
||||
if [[ -n "${preferred_generator}" ]]; then
|
||||
cmake_generator="${preferred_generator}"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -n "${CMAKE_GENERATOR:-}" ]]; then
|
||||
cmake_generator="${CMAKE_GENERATOR}"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "${clean_build}" -eq 0 && -f "${build_dir}/CMakeCache.txt" ]]; then
|
||||
cmake_generator="$(sed -n 's/^CMAKE_GENERATOR:INTERNAL=//p' "${build_dir}/CMakeCache.txt" | head -n1)"
|
||||
if [[ -n "${cmake_generator}" ]]; then
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v ninja >/dev/null 2>&1; then
|
||||
cmake_generator="Ninja"
|
||||
return
|
||||
fi
|
||||
|
||||
cmake_generator=""
|
||||
}
|
||||
|
||||
admin_cmd() {
|
||||
if [[ "${#pkg_prefix[@]}" -gt 0 ]]; then
|
||||
"${pkg_prefix[@]}" "$@"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
update_pkg_index_once() {
|
||||
if [[ "${pkg_index_updated}" -eq 1 ]]; then
|
||||
return
|
||||
fi
|
||||
case "${pkg_manager}" in
|
||||
apt)
|
||||
admin_cmd apt-get update
|
||||
;;
|
||||
pacman)
|
||||
admin_cmd pacman -Sy --noconfirm
|
||||
;;
|
||||
dnf|zypper)
|
||||
;;
|
||||
esac
|
||||
pkg_index_updated=1
|
||||
}
|
||||
|
||||
install_packages() {
|
||||
local -a packages=("$@")
|
||||
if [[ "${#packages[@]}" -eq 0 ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
update_pkg_index_once
|
||||
case "${pkg_manager}" in
|
||||
apt)
|
||||
admin_cmd apt-get install -y "${packages[@]}"
|
||||
;;
|
||||
dnf)
|
||||
admin_cmd dnf install -y "${packages[@]}"
|
||||
;;
|
||||
pacman)
|
||||
admin_cmd pacman -S --noconfirm --needed "${packages[@]}"
|
||||
;;
|
||||
zypper)
|
||||
admin_cmd zypper --non-interactive install --no-recommends "${packages[@]}"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
install_optional_first_hit() {
|
||||
local -a candidates=("$@")
|
||||
local candidate
|
||||
for candidate in "${candidates[@]}"; do
|
||||
if install_packages "${candidate}" >/dev/null 2>&1; then
|
||||
log_ok "Installed optional package: ${candidate}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_linux_dependencies() {
|
||||
detect_package_manager
|
||||
|
||||
if [[ -z "${pkg_manager}" ]]; then
|
||||
log_warn "No supported package manager detected (apt/dnf/pacman/zypper). Skipping auto-install."
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$(id -u)" -ne 0 && "${#pkg_prefix[@]}" -eq 0 ]]; then
|
||||
log_warn "Auto-install requires root or sudo. Skipping dependency installation."
|
||||
return
|
||||
fi
|
||||
|
||||
local need_install=0
|
||||
local -a missing=()
|
||||
local cmd
|
||||
for cmd in git cmake pkg-config c++; do
|
||||
if ! command -v "${cmd}" >/dev/null 2>&1; then
|
||||
need_install=1
|
||||
missing+=("${cmd}")
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v pkg-config >/dev/null 2>&1; then
|
||||
local module
|
||||
for module in x11 xrandr xi xinerama xcursor gl; do
|
||||
if ! pkg-config --exists "${module}"; then
|
||||
need_install=1
|
||||
missing+=("${module}-dev")
|
||||
fi
|
||||
done
|
||||
if ! pkg-config --exists vulkan; then
|
||||
need_install=1
|
||||
missing+=("vulkan-dev")
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! command -v glslc >/dev/null 2>&1; then
|
||||
need_install=1
|
||||
missing+=("glslc")
|
||||
fi
|
||||
if ! command -v vulkaninfo >/dev/null 2>&1; then
|
||||
need_install=1
|
||||
missing+=("vulkan-tools")
|
||||
fi
|
||||
|
||||
local -a required_pkgs=()
|
||||
local -a core_pkgs=()
|
||||
local -a x11_pkgs=()
|
||||
local -a graphics_pkgs=()
|
||||
local -a vulkan_pkgs=()
|
||||
local -a optional_pkgs=()
|
||||
local -a glslc_candidates=()
|
||||
case "${pkg_manager}" in
|
||||
apt)
|
||||
core_pkgs=(build-essential cmake pkg-config git zlib1g-dev)
|
||||
x11_pkgs=(libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev)
|
||||
graphics_pkgs=(libgl1-mesa-dev libegl1-mesa-dev libwayland-dev)
|
||||
vulkan_pkgs=(libvulkan-dev vulkan-tools glslang-tools)
|
||||
optional_pkgs=(ccache)
|
||||
glslc_candidates=(glslc shaderc)
|
||||
;;
|
||||
dnf)
|
||||
core_pkgs=(gcc gcc-c++ make cmake pkgconf-pkg-config git zlib-devel)
|
||||
x11_pkgs=(libX11-devel libXrandr-devel libXinerama-devel libXcursor-devel libXi-devel)
|
||||
graphics_pkgs=(mesa-libGL-devel mesa-libEGL-devel wayland-devel)
|
||||
vulkan_pkgs=(vulkan-loader-devel vulkan-tools glslang)
|
||||
optional_pkgs=(ccache)
|
||||
glslc_candidates=(shaderc shaderc-devel)
|
||||
;;
|
||||
pacman)
|
||||
core_pkgs=(base-devel cmake pkgconf git zlib)
|
||||
x11_pkgs=(libx11 libxrandr libxinerama libxcursor libxi)
|
||||
graphics_pkgs=(mesa wayland)
|
||||
vulkan_pkgs=(vulkan-headers vulkan-tools glslang)
|
||||
optional_pkgs=(ccache)
|
||||
glslc_candidates=(shaderc)
|
||||
;;
|
||||
zypper)
|
||||
core_pkgs=(gcc gcc-c++ make cmake pkg-config git zlib-devel)
|
||||
x11_pkgs=(libX11-devel libXrandr-devel libXinerama-devel libXcursor-devel libXi-devel)
|
||||
graphics_pkgs=(Mesa-libGL-devel Mesa-libEGL-devel wayland-devel)
|
||||
vulkan_pkgs=(vulkan-devel vulkan-tools glslang)
|
||||
optional_pkgs=(ccache)
|
||||
glslc_candidates=(shaderc shaderc-devel)
|
||||
;;
|
||||
esac
|
||||
required_pkgs=(
|
||||
"${core_pkgs[@]}"
|
||||
"${x11_pkgs[@]}"
|
||||
"${graphics_pkgs[@]}"
|
||||
"${vulkan_pkgs[@]}"
|
||||
"${optional_pkgs[@]}"
|
||||
)
|
||||
|
||||
log_info "Dependency hierarchy (${pkg_manager}):"
|
||||
echo " +-- Core toolchain"
|
||||
for pkg in "${core_pkgs[@]}"; do echo " | +-- ${pkg}"; done
|
||||
echo " +-- X11/Windowing"
|
||||
for pkg in "${x11_pkgs[@]}"; do echo " | +-- ${pkg}"; done
|
||||
echo " +-- OpenGL/EGL/Wayland"
|
||||
for pkg in "${graphics_pkgs[@]}"; do echo " | +-- ${pkg}"; done
|
||||
echo " +-- Vulkan stack"
|
||||
for pkg in "${vulkan_pkgs[@]}"; do echo " | +-- ${pkg}"; done
|
||||
echo " +-- Optional acceleration"
|
||||
for pkg in "${optional_pkgs[@]}"; do echo " +-- ${pkg}"; done
|
||||
|
||||
if [[ "${need_install}" -eq 0 ]]; then
|
||||
log_ok "Build dependencies already present."
|
||||
return
|
||||
fi
|
||||
|
||||
log_info "Missing prerequisites detected: ${missing[*]}"
|
||||
log_info "Installing packages using ${pkg_manager}..."
|
||||
|
||||
install_packages "${required_pkgs[@]}"
|
||||
|
||||
if ! command -v glslc >/dev/null 2>&1; then
|
||||
install_optional_first_hit "${glslc_candidates[@]}" || log_warn "glslc package candidate not found; Vulkan runtime shader compile may fail."
|
||||
fi
|
||||
|
||||
if ! command -v glslc >/dev/null 2>&1; then
|
||||
log_warn "glslc is still missing."
|
||||
fi
|
||||
if ! command -v vulkaninfo >/dev/null 2>&1; then
|
||||
log_warn "vulkaninfo is still missing."
|
||||
fi
|
||||
}
|
||||
|
||||
sync_submodules() {
|
||||
git -C "${script_dir}" submodule update --init --recursive
|
||||
}
|
||||
|
||||
configure_ccache() {
|
||||
if command -v ccache >/dev/null 2>&1; then
|
||||
export CCACHE_BASEDIR="${script_dir}"
|
||||
export CCACHE_NOHASHDIR=1
|
||||
log_info "ccache detected. Normalizing paths for cross-build cache hits."
|
||||
fi
|
||||
}
|
||||
|
||||
clean_editor_build() {
|
||||
if [[ -d "${build_dir}" ]]; then
|
||||
rm -rf "${build_dir}"
|
||||
log_ok "Removed ${build_dir}"
|
||||
fi
|
||||
}
|
||||
|
||||
clean_player_cache() {
|
||||
if [[ -d "${player_cache_dir}" ]]; then
|
||||
rm -rf "${player_cache_dir}"
|
||||
log_ok "Removed ${player_cache_dir}"
|
||||
fi
|
||||
}
|
||||
|
||||
configure_editor_build() {
|
||||
local -a generator_args=()
|
||||
if [[ ! -f "${build_dir}/CMakeCache.txt" && -n "${cmake_generator}" ]]; then
|
||||
generator_args=(-G "${cmake_generator}")
|
||||
fi
|
||||
cmake "${generator_args[@]}" -S "${script_dir}" -B "${build_dir}" -DMONO_ROOT=/usr -DCMAKE_BUILD_TYPE="${build_type}"
|
||||
}
|
||||
|
||||
build_editor_targets() {
|
||||
cmake --build "${build_dir}" --parallel "${jobs}"
|
||||
}
|
||||
|
||||
install_editor_targets() {
|
||||
cmake --install "${build_dir}" --prefix "${build_dir}/install"
|
||||
}
|
||||
|
||||
copy_third_party_libraries() {
|
||||
local target_dir="$1"
|
||||
|
||||
mkdir -p "${target_dir}/Packages/ThirdParty"
|
||||
find "${target_dir}" -type f \( -name "*.a" -o -name "*.so" -o -name "*.dylib" -o -name "*.lib" \) \
|
||||
-not -path "${target_dir}/Packages/*" -exec cp -f {} "${target_dir}/Packages/ThirdParty/" \;
|
||||
}
|
||||
|
||||
copy_engine_libraries() {
|
||||
local target_dir="$1"
|
||||
|
||||
mkdir -p "${target_dir}/Packages/Engine"
|
||||
find "${target_dir}" -type f \( -name "libcore*" -o -name "core*.lib" -o -name "core*.dll" \) \
|
||||
-not -path "${target_dir}/Packages/*" -exec cp -f {} "${target_dir}/Packages/Engine/" \;
|
||||
}
|
||||
|
||||
configure_player_build() {
|
||||
local -a generator_args=()
|
||||
if [[ ! -f "${player_cache_dir}/CMakeCache.txt" && -n "${cmake_generator}" ]]; then
|
||||
generator_args=(-G "${cmake_generator}")
|
||||
fi
|
||||
cmake "${generator_args[@]}" -S "${script_dir}" -B "${player_cache_dir}" -DMONO_ROOT=/usr -DCMAKE_BUILD_TYPE="${build_type}" -DMODULARITY_BUILD_EDITOR=OFF
|
||||
}
|
||||
|
||||
build_player_target() {
|
||||
cmake --build "${player_cache_dir}" --target ModularityPlayer --parallel "${jobs}"
|
||||
}
|
||||
|
||||
finalize_packaging() {
|
||||
rm -rf "${build_dir}/Template-Projects"
|
||||
cp -r "${script_dir}/Resources" "${build_dir}/"
|
||||
if [[ -d "${script_dir}/Template-Projects" ]]; then
|
||||
cp -r "${script_dir}/Template-Projects" "${build_dir}/"
|
||||
else
|
||||
mkdir -p "${build_dir}/Template-Projects"
|
||||
fi
|
||||
if [[ -f "${build_dir}/Resources/imgui.ini" ]]; then
|
||||
cp "${build_dir}/Resources/imgui.ini" "${build_dir}/"
|
||||
fi
|
||||
ln -sfn "${build_dir}/compile_commands.json" "${script_dir}/compile_commands.json"
|
||||
(cd "${build_dir}" && cpack)
|
||||
}
|
||||
|
||||
show_stage_hierarchy() {
|
||||
local -a stages=()
|
||||
|
||||
if [[ "${skip_deps}" -eq 0 && "$(uname -s)" == "Linux" ]]; then
|
||||
stages+=("Check/install system dependencies")
|
||||
fi
|
||||
if [[ "${clean_build}" -eq 1 ]]; then
|
||||
stages+=("Clean editor build directory")
|
||||
stages+=("Clean player cache directory")
|
||||
fi
|
||||
stages+=(
|
||||
"Sync git submodules"
|
||||
"Configure editor build"
|
||||
"Build editor + engine targets"
|
||||
"Install editor artifacts"
|
||||
"Collect editor third-party libraries"
|
||||
"Collect editor engine libraries"
|
||||
"Configure player-only cache build"
|
||||
"Build ModularityPlayer target"
|
||||
"Collect player third-party libraries"
|
||||
"Collect player engine libraries"
|
||||
"Package artifacts and resources"
|
||||
)
|
||||
|
||||
log_info "Build stage hierarchy:"
|
||||
local i
|
||||
local stage_count="${#stages[@]}"
|
||||
for ((i = 0; i < stage_count; i++)); do
|
||||
local branch="|--"
|
||||
if [[ $((i + 1)) -eq "${stage_count}" ]]; then
|
||||
branch='`--'
|
||||
fi
|
||||
printf " %s [%02d/%02d] %s\n" "${branch}" "$((i + 1))" "${stage_count}" "${stages[$i]}"
|
||||
done
|
||||
}
|
||||
|
||||
base_steps=11
|
||||
total_steps="${base_steps}"
|
||||
if [[ "${clean_build}" -eq 1 ]]; then
|
||||
total_steps=$((total_steps + 2))
|
||||
fi
|
||||
if [[ "${skip_deps}" -eq 0 && "$(uname -s)" == "Linux" ]]; then
|
||||
total_steps=$((total_steps + 1))
|
||||
fi
|
||||
|
||||
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"
|
||||
build_started=1
|
||||
detect_cmake_generator
|
||||
printf "%s================================%s\n" "${C_BOLD}" "${C_RESET}"
|
||||
printf "%s Modularity - Native Linux Builder%s\n" "${C_BOLD}" "${C_RESET}"
|
||||
printf "%s================================%s\n" "${C_BOLD}" "${C_RESET}"
|
||||
log_info "Build type: ${build_type} | Jobs: ${jobs}"
|
||||
if [[ -n "${cmake_generator}" ]]; then
|
||||
log_info "CMake generator: ${cmake_generator}"
|
||||
fi
|
||||
show_stage_hierarchy
|
||||
|
||||
if [[ "${skip_deps}" -eq 0 && "$(uname -s)" == "Linux" ]]; then
|
||||
run_step "Checking and installing system dependencies (Vulkan/OpenGL/X11/toolchain)" ensure_linux_dependencies
|
||||
elif [[ "${skip_deps}" -eq 0 ]]; then
|
||||
log_warn "Auto dependency install is only implemented for Linux."
|
||||
fi
|
||||
|
||||
mkdir -p build
|
||||
cd build
|
||||
cmake .. -DMONO_ROOT=/usr -DCMAKE_BUILD_TYPE="$build_type"
|
||||
cmake --build . -- -j"$(nproc)"
|
||||
cmake --install . --prefix install
|
||||
configure_ccache
|
||||
|
||||
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"
|
||||
if [[ "${clean_build}" -eq 1 ]]; then
|
||||
run_long_step "Cleaning editor build directory" clean_editor_build
|
||||
run_long_step "Cleaning player cache directory" clean_player_cache
|
||||
fi
|
||||
|
||||
mkdir -p "$player_cache_dir"
|
||||
cmake -S . -B "$player_cache_dir" -DMONO_ROOT=/usr -DCMAKE_BUILD_TYPE="$build_type" -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 compile_commands.json ../compile_commands.json
|
||||
cpack
|
||||
run_long_step "Syncing git submodules" sync_submodules
|
||||
run_long_step "Configuring editor build (CMake)" configure_editor_build
|
||||
run_long_step "Building editor + engine targets" build_editor_targets
|
||||
run_long_step "Installing editor artifacts" install_editor_targets
|
||||
run_long_step "Collecting editor third-party libraries" copy_third_party_libraries "${build_dir}"
|
||||
run_long_step "Collecting editor engine libraries" copy_engine_libraries "${build_dir}"
|
||||
run_long_step "Configuring player-only cache build" configure_player_build
|
||||
run_long_step "Building ModularityPlayer target" build_player_target
|
||||
run_long_step "Collecting player third-party libraries" copy_third_party_libraries "${player_cache_dir}"
|
||||
run_long_step "Collecting player engine libraries" copy_engine_libraries "${player_cache_dir}"
|
||||
run_long_step "Packaging artifacts and resources" finalize_packaging
|
||||
|
||||
@@ -1 +1 @@
|
||||
compile_commands.json
|
||||
/home/anemunt/Git-base/Modularity/build/compile_commands.json
|
||||
@@ -18,6 +18,7 @@ public:
|
||||
void setFloat(const std::string &name, float value) const;
|
||||
void setVec2(const std::string &name, const glm::vec2 &value) const;
|
||||
void setVec3(const std::string &name, const glm::vec3 &value) const;
|
||||
void setVec4(const std::string &name, const glm::vec4 &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;
|
||||
|
||||
|
||||
5584
include/ThirdParty/stb_vorbis.c
vendored
Normal file
5584
include/ThirdParty/stb_vorbis.c
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,23 @@
|
||||
#define STB_VORBIS_HEADER_ONLY
|
||||
#include "../include/ThirdParty/stb_vorbis.c"
|
||||
#define MINIAUDIO_IMPLEMENTATION
|
||||
#include "../include/ThirdParty/miniaudio.h"
|
||||
#include "AudioSystem.h"
|
||||
#include <cmath>
|
||||
#include <atomic>
|
||||
#include <array>
|
||||
#if MODULARITY_HAS_SNDFILE
|
||||
#include <sndfile.h>
|
||||
#endif
|
||||
|
||||
#undef STB_VORBIS_HEADER_ONLY
|
||||
#include "../include/ThirdParty/stb_vorbis.c"
|
||||
#undef L
|
||||
#undef C
|
||||
#undef R
|
||||
#undef PLAYBACK_MONO
|
||||
#undef PLAYBACK_LEFT
|
||||
#undef PLAYBACK_RIGHT
|
||||
|
||||
namespace {
|
||||
constexpr size_t kPreviewBuckets = 800;
|
||||
@@ -14,6 +28,58 @@ constexpr size_t kReverbAllpassCount = 2;
|
||||
constexpr float kReverbPreDelayMaxSeconds = 0.2f;
|
||||
constexpr float kReverbReflectionsMaxSeconds = 0.1f;
|
||||
|
||||
void BuildWaveformPreview(AudioClipPreview& preview, const float* samples, ma_uint64 totalFrames) {
|
||||
if (!samples || totalFrames == 0 || preview.channels == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ma_uint64 framesPerBucket = std::max<ma_uint64>(1, totalFrames / kPreviewBuckets);
|
||||
preview.waveform.assign(static_cast<size_t>(kPreviewBuckets), 0.0f);
|
||||
if (preview.channels >= 2) {
|
||||
preview.waveformLeft.assign(static_cast<size_t>(kPreviewBuckets), 0.0f);
|
||||
preview.waveformRight.assign(static_cast<size_t>(kPreviewBuckets), 0.0f);
|
||||
}
|
||||
|
||||
size_t bucketIndex = 0;
|
||||
ma_uint64 bucketCursor = 0;
|
||||
float bucketMax = 0.0f;
|
||||
float bucketMaxLeft = 0.0f;
|
||||
float bucketMaxRight = 0.0f;
|
||||
|
||||
for (ma_uint64 frame = 0; frame < totalFrames && bucketIndex < preview.waveform.size(); ++frame) {
|
||||
const size_t frameOffset = static_cast<size_t>(frame * preview.channels);
|
||||
for (ma_uint32 channel = 0; channel < preview.channels; ++channel) {
|
||||
bucketMax = std::max(bucketMax, std::fabs(samples[frameOffset + channel]));
|
||||
}
|
||||
if (preview.channels >= 2) {
|
||||
bucketMaxLeft = std::max(bucketMaxLeft, std::fabs(samples[frameOffset]));
|
||||
bucketMaxRight = std::max(bucketMaxRight, std::fabs(samples[frameOffset + 1]));
|
||||
}
|
||||
|
||||
bucketCursor++;
|
||||
if (bucketCursor >= framesPerBucket) {
|
||||
preview.waveform[bucketIndex] = std::clamp(bucketMax, 0.0f, 1.0f);
|
||||
if (preview.channels >= 2) {
|
||||
preview.waveformLeft[bucketIndex] = std::clamp(bucketMaxLeft, 0.0f, 1.0f);
|
||||
preview.waveformRight[bucketIndex] = std::clamp(bucketMaxRight, 0.0f, 1.0f);
|
||||
}
|
||||
bucketIndex++;
|
||||
bucketCursor = 0;
|
||||
bucketMax = 0.0f;
|
||||
bucketMaxLeft = 0.0f;
|
||||
bucketMaxRight = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
if (bucketIndex < preview.waveform.size() && bucketMax > 0.0f) {
|
||||
preview.waveform[bucketIndex] = std::clamp(bucketMax, 0.0f, 1.0f);
|
||||
if (preview.channels >= 2) {
|
||||
preview.waveformLeft[bucketIndex] = std::clamp(bucketMaxLeft, 0.0f, 1.0f);
|
||||
preview.waveformRight[bucketIndex] = std::clamp(bucketMaxRight, 0.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float DbToLinear(float db) {
|
||||
return std::pow(10.0f, db / 20.0f);
|
||||
}
|
||||
@@ -225,6 +291,7 @@ void AudioSystem::destroyActiveSounds() {
|
||||
for (auto& kv : activeSounds) {
|
||||
if (kv.second) {
|
||||
ma_sound_uninit(&kv.second->sound);
|
||||
releaseDecodedAudio(kv.second->decodedData);
|
||||
}
|
||||
}
|
||||
activeSounds.clear();
|
||||
@@ -255,6 +322,7 @@ bool AudioSystem::ensureSoundFor(const SceneObject& obj) {
|
||||
}
|
||||
if (it->second) {
|
||||
ma_sound_uninit(&it->second->sound);
|
||||
releaseDecodedAudio(it->second->decodedData);
|
||||
}
|
||||
activeSounds.erase(it);
|
||||
}
|
||||
@@ -270,16 +338,7 @@ bool AudioSystem::ensureSoundFor(const SceneObject& obj) {
|
||||
if (!initialized && !init()) return false;
|
||||
|
||||
auto snd = std::make_unique<ActiveSound>();
|
||||
ma_result res = ma_sound_init_from_file(
|
||||
&engine,
|
||||
obj.audioSource.clipPath.c_str(),
|
||||
MA_SOUND_FLAG_STREAM,
|
||||
reverbReady ? &reverbGroup : nullptr,
|
||||
nullptr,
|
||||
&snd->sound
|
||||
);
|
||||
if (res != MA_SUCCESS) {
|
||||
std::cerr << "AudioSystem: failed to load " << obj.audioSource.clipPath << " (" << res << ")\n";
|
||||
if (!initSoundFromPath(obj.audioSource.clipPath, MA_SOUND_FLAG_STREAM, reverbReady ? &reverbGroup : nullptr, snd->sound, snd->decodedData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -350,6 +409,7 @@ void AudioSystem::update(const std::vector<SceneObject>& objects, const Camera&
|
||||
if (eraseIt != activeSounds.end()) {
|
||||
if (eraseIt->second) {
|
||||
ma_sound_uninit(&eraseIt->second->sound);
|
||||
releaseDecodedAudio(eraseIt->second->decodedData);
|
||||
}
|
||||
activeSounds.erase(eraseIt);
|
||||
}
|
||||
@@ -369,6 +429,7 @@ void AudioSystem::update(const std::vector<SceneObject>& objects, const Camera&
|
||||
if (stillPresent.find(it->first) == stillPresent.end()) {
|
||||
if (it->second) {
|
||||
ma_sound_uninit(&it->second->sound);
|
||||
releaseDecodedAudio(it->second->decodedData);
|
||||
}
|
||||
it = activeSounds.erase(it);
|
||||
} else {
|
||||
@@ -382,9 +443,8 @@ bool AudioSystem::playPreview(const std::string& path, float volume, bool loop)
|
||||
if (!initialized && !init()) return false;
|
||||
|
||||
stopPreview();
|
||||
ma_result res = ma_sound_init_from_file(&engine, path.c_str(), MA_SOUND_FLAG_STREAM, nullptr, nullptr, &previewSound);
|
||||
if (res != MA_SUCCESS) {
|
||||
std::cerr << "AudioSystem: preview load failed for " << path << " (" << res << ")\n";
|
||||
if (!initSoundFromPath(path, MA_SOUND_FLAG_STREAM, nullptr, previewSound, previewDecodedData)) {
|
||||
std::cerr << "AudioSystem: preview load failed for " << path << "\n";
|
||||
return false;
|
||||
}
|
||||
ma_sound_set_looping(&previewSound, loop ? MA_TRUE : MA_FALSE);
|
||||
@@ -394,6 +454,7 @@ bool AudioSystem::playPreview(const std::string& path, float volume, bool loop)
|
||||
previewActive = ma_sound_start(&previewSound) == MA_SUCCESS;
|
||||
if (!previewActive) {
|
||||
ma_sound_uninit(&previewSound);
|
||||
releaseDecodedAudio(previewDecodedData);
|
||||
}
|
||||
return previewActive;
|
||||
}
|
||||
@@ -403,6 +464,7 @@ void AudioSystem::stopPreview() {
|
||||
ma_sound_stop(&previewSound);
|
||||
ma_sound_uninit(&previewSound);
|
||||
}
|
||||
releaseDecodedAudio(previewDecodedData);
|
||||
previewActive = false;
|
||||
previewPath.clear();
|
||||
}
|
||||
@@ -413,25 +475,67 @@ bool AudioSystem::isPreviewing(const std::string& path) const {
|
||||
|
||||
bool AudioSystem::getPreviewTime(const std::string& path, double& cursorSeconds, double& durationSeconds) const {
|
||||
if (!previewActive || previewPath != path) return false;
|
||||
float cur = 0.0f;
|
||||
float len = 0.0f;
|
||||
if (ma_sound_get_cursor_in_seconds(&previewSound, &cur) != MA_SUCCESS) return false;
|
||||
if (ma_sound_get_length_in_seconds(&previewSound, &len) != MA_SUCCESS) return false;
|
||||
cursorSeconds = static_cast<double>(cur);
|
||||
durationSeconds = static_cast<double>(len);
|
||||
|
||||
ma_uint32 sampleRate = 0;
|
||||
if (ma_sound_get_data_format(&previewSound, nullptr, nullptr, &sampleRate, nullptr, 0) != MA_SUCCESS || sampleRate == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ma_uint64 cursorFrames = 0;
|
||||
if (ma_sound_get_cursor_in_pcm_frames(&previewSound, &cursorFrames) != MA_SUCCESS) return false;
|
||||
cursorSeconds = static_cast<double>(cursorFrames) / static_cast<double>(sampleRate);
|
||||
|
||||
durationSeconds = 0.0;
|
||||
auto it = previewCache.find(path);
|
||||
if (it != previewCache.end() && it->second.loaded && std::isfinite(it->second.durationSeconds) && it->second.durationSeconds > 0.0) {
|
||||
durationSeconds = it->second.durationSeconds;
|
||||
} else if (previewDecodedData && previewDecodedData->sampleRate > 0 && previewDecodedData->frameCount > 0) {
|
||||
durationSeconds = static_cast<double>(previewDecodedData->frameCount) / static_cast<double>(previewDecodedData->sampleRate);
|
||||
} else {
|
||||
ma_uint64 lengthFrames = 0;
|
||||
if (ma_sound_get_length_in_pcm_frames(&previewSound, &lengthFrames) == MA_SUCCESS && lengthFrames > 0) {
|
||||
durationSeconds = static_cast<double>(lengthFrames) / static_cast<double>(sampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
if (!std::isfinite(cursorSeconds) || !std::isfinite(durationSeconds) || durationSeconds <= 0.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
cursorSeconds = std::clamp(cursorSeconds, 0.0, durationSeconds);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AudioSystem::seekPreview(const std::string& path, double seconds) {
|
||||
if (!previewActive || previewPath != path) return false;
|
||||
ma_uint32 sampleRate = 0;
|
||||
if (ma_sound_get_data_format(&previewSound, nullptr, nullptr, &sampleRate, nullptr, 0) != MA_SUCCESS) {
|
||||
|
||||
ma_uint32 sourceSampleRate = 0;
|
||||
auto it = previewCache.find(path);
|
||||
if (it != previewCache.end() && it->second.loaded && it->second.sampleRate > 0) {
|
||||
sourceSampleRate = it->second.sampleRate;
|
||||
} else if (previewDecodedData && previewDecodedData->sampleRate > 0) {
|
||||
sourceSampleRate = previewDecodedData->sampleRate;
|
||||
} else if (ma_sound_get_data_format(&previewSound, nullptr, nullptr, &sourceSampleRate, nullptr, 0) != MA_SUCCESS || sourceSampleRate == 0) {
|
||||
return false;
|
||||
}
|
||||
float lenSec = 0.0f;
|
||||
ma_sound_get_length_in_seconds(&previewSound, &lenSec);
|
||||
seconds = std::clamp(seconds, 0.0, static_cast<double>(lenSec));
|
||||
ma_uint64 targetFrame = static_cast<ma_uint64>(seconds * static_cast<double>(sampleRate));
|
||||
|
||||
double maxSeconds = 0.0;
|
||||
if (it != previewCache.end() && it->second.loaded && std::isfinite(it->second.durationSeconds) && it->second.durationSeconds > 0.0) {
|
||||
maxSeconds = it->second.durationSeconds;
|
||||
} else if (previewDecodedData && previewDecodedData->sampleRate > 0 && previewDecodedData->frameCount > 0) {
|
||||
maxSeconds = static_cast<double>(previewDecodedData->frameCount) / static_cast<double>(previewDecodedData->sampleRate);
|
||||
} else {
|
||||
ma_uint64 lengthFrames = 0;
|
||||
if (ma_sound_get_length_in_pcm_frames(&previewSound, &lengthFrames) == MA_SUCCESS && lengthFrames > 0) {
|
||||
maxSeconds = static_cast<double>(lengthFrames) / static_cast<double>(sourceSampleRate);
|
||||
}
|
||||
}
|
||||
if (maxSeconds > 0.0 && std::isfinite(maxSeconds)) {
|
||||
seconds = std::clamp(seconds, 0.0, maxSeconds);
|
||||
} else {
|
||||
seconds = std::max(0.0, seconds);
|
||||
}
|
||||
ma_uint64 targetFrame = static_cast<ma_uint64>(seconds * static_cast<double>(sourceSampleRate));
|
||||
ma_result res = ma_sound_seek_to_pcm_frame(&previewSound, targetFrame);
|
||||
return res == MA_SUCCESS;
|
||||
}
|
||||
@@ -617,12 +721,94 @@ void AudioSystem::shutdownReverbGraph() {
|
||||
currentReverb = ReverbSettings{};
|
||||
}
|
||||
|
||||
std::shared_ptr<AudioSystem::DecodedAudioData> AudioSystem::decodeClipToMemory(const std::string& path) {
|
||||
#if MODULARITY_HAS_SNDFILE
|
||||
SF_INFO info{};
|
||||
SNDFILE* file = sf_open(path.c_str(), SFM_READ, &info);
|
||||
if (!file) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (info.frames <= 0 || info.channels <= 0 || info.samplerate <= 0) {
|
||||
sf_close(file);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto decoded = std::make_shared<DecodedAudioData>();
|
||||
decoded->channels = static_cast<ma_uint32>(info.channels);
|
||||
decoded->sampleRate = static_cast<ma_uint32>(info.samplerate);
|
||||
decoded->frameCount = static_cast<ma_uint64>(info.frames);
|
||||
decoded->pcmFrames.resize(static_cast<size_t>(decoded->frameCount * decoded->channels));
|
||||
|
||||
const sf_count_t framesRead = sf_readf_float(file, decoded->pcmFrames.data(), info.frames);
|
||||
sf_close(file);
|
||||
if (framesRead <= 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
decoded->frameCount = static_cast<ma_uint64>(framesRead);
|
||||
decoded->pcmFrames.resize(static_cast<size_t>(decoded->frameCount * decoded->channels));
|
||||
if (ma_audio_buffer_ref_init(ma_format_f32, decoded->channels, decoded->pcmFrames.data(), decoded->frameCount, &decoded->buffer) != MA_SUCCESS) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
decoded->initialized = true;
|
||||
return decoded;
|
||||
#else
|
||||
(void)path;
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool AudioSystem::initSoundFromPath(const std::string& path, ma_uint32 flags, ma_sound_group* group, ma_sound& sound,
|
||||
std::shared_ptr<DecodedAudioData>& decodedData) {
|
||||
ma_result res = ma_sound_init_from_file(&engine, path.c_str(), flags, group, nullptr, &sound);
|
||||
if (res == MA_SUCCESS) {
|
||||
return true;
|
||||
}
|
||||
|
||||
decodedData = decodeClipToMemory(path);
|
||||
if (!decodedData) {
|
||||
std::cerr << "AudioSystem: miniaudio load failed for " << path << " (" << res << ")\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
res = ma_sound_init_from_data_source(&engine, &decodedData->buffer.ds, flags & ~MA_SOUND_FLAG_STREAM, group, &sound);
|
||||
if (res != MA_SUCCESS) {
|
||||
std::cerr << "AudioSystem: decoded fallback load failed for " << path << " (" << res << ")\n";
|
||||
releaseDecodedAudio(decodedData);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AudioSystem::releaseDecodedAudio(std::shared_ptr<DecodedAudioData>& decodedData) {
|
||||
if (decodedData && decodedData->initialized) {
|
||||
ma_audio_buffer_ref_uninit(&decodedData->buffer);
|
||||
decodedData->initialized = false;
|
||||
}
|
||||
decodedData.reset();
|
||||
}
|
||||
|
||||
AudioClipPreview AudioSystem::loadPreview(const std::string& path) {
|
||||
AudioClipPreview preview;
|
||||
preview.path = path;
|
||||
|
||||
ma_decoder decoder;
|
||||
if (ma_decoder_init_file(path.c_str(), nullptr, &decoder) != MA_SUCCESS) {
|
||||
auto decoded = decodeClipToMemory(path);
|
||||
if (!decoded) {
|
||||
return preview;
|
||||
}
|
||||
preview.channels = decoded->channels;
|
||||
preview.sampleRate = decoded->sampleRate;
|
||||
preview.durationSeconds = (preview.sampleRate > 0)
|
||||
? static_cast<double>(decoded->frameCount) / static_cast<double>(preview.sampleRate)
|
||||
: 0.0;
|
||||
BuildWaveformPreview(preview, decoded->pcmFrames.data(), decoded->frameCount);
|
||||
preview.loaded = true;
|
||||
releaseDecodedAudio(decoded);
|
||||
return preview;
|
||||
}
|
||||
|
||||
@@ -637,22 +823,12 @@ AudioClipPreview AudioSystem::loadPreview(const std::string& path) {
|
||||
return preview;
|
||||
}
|
||||
|
||||
const ma_uint64 framesPerBucket = std::max<ma_uint64>(1, totalFrames / kPreviewBuckets);
|
||||
preview.waveform.assign(static_cast<size_t>(kPreviewBuckets), 0.0f);
|
||||
if (preview.channels >= 2) {
|
||||
preview.waveformLeft.assign(static_cast<size_t>(kPreviewBuckets), 0.0f);
|
||||
preview.waveformRight.assign(static_cast<size_t>(kPreviewBuckets), 0.0f);
|
||||
}
|
||||
|
||||
std::vector<float> temp(kPreviewChunkFrames * preview.channels);
|
||||
std::vector<float> pcmFrames;
|
||||
pcmFrames.reserve(static_cast<size_t>(totalFrames * preview.channels));
|
||||
ma_uint64 frameCursor = 0;
|
||||
size_t bucketIndex = 0;
|
||||
ma_uint64 bucketCursor = 0;
|
||||
float bucketMax = 0.0f;
|
||||
float bucketMaxLeft = 0.0f;
|
||||
float bucketMaxRight = 0.0f;
|
||||
|
||||
while (frameCursor < totalFrames && bucketIndex < preview.waveform.size()) {
|
||||
while (frameCursor < totalFrames) {
|
||||
ma_uint64 framesToRead = std::min<ma_uint64>(kPreviewChunkFrames, totalFrames - frameCursor);
|
||||
ma_uint64 framesRead = 0;
|
||||
ma_result readResult = ma_decoder_read_pcm_frames(&decoder, temp.data(), framesToRead, &framesRead);
|
||||
@@ -660,48 +836,15 @@ AudioClipPreview AudioSystem::loadPreview(const std::string& path) {
|
||||
break;
|
||||
}
|
||||
if (framesRead == 0) break;
|
||||
|
||||
for (ma_uint64 f = 0; f < framesRead; ++f) {
|
||||
size_t frameOffset = static_cast<size_t>(f * preview.channels);
|
||||
for (ma_uint32 c = 0; c < preview.channels; ++c) {
|
||||
float sample = temp[frameOffset + c];
|
||||
bucketMax = std::max(bucketMax, std::fabs(sample));
|
||||
}
|
||||
if (preview.channels >= 2) {
|
||||
float leftSample = temp[frameOffset];
|
||||
float rightSample = temp[frameOffset + 1];
|
||||
bucketMaxLeft = std::max(bucketMaxLeft, std::fabs(leftSample));
|
||||
bucketMaxRight = std::max(bucketMaxRight, std::fabs(rightSample));
|
||||
}
|
||||
bucketCursor++;
|
||||
frameCursor++;
|
||||
|
||||
if (bucketCursor >= framesPerBucket) {
|
||||
if (bucketIndex < preview.waveform.size()) {
|
||||
preview.waveform[bucketIndex] = std::clamp(bucketMax, 0.0f, 1.0f);
|
||||
if (preview.channels >= 2) {
|
||||
preview.waveformLeft[bucketIndex] = std::clamp(bucketMaxLeft, 0.0f, 1.0f);
|
||||
preview.waveformRight[bucketIndex] = std::clamp(bucketMaxRight, 0.0f, 1.0f);
|
||||
}
|
||||
bucketIndex++;
|
||||
}
|
||||
bucketCursor = 0;
|
||||
bucketMax = 0.0f;
|
||||
bucketMaxLeft = 0.0f;
|
||||
bucketMaxRight = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bucketIndex < preview.waveform.size() && bucketMax > 0.0f) {
|
||||
preview.waveform[bucketIndex] = std::clamp(bucketMax, 0.0f, 1.0f);
|
||||
if (preview.channels >= 2) {
|
||||
preview.waveformLeft[bucketIndex] = std::clamp(bucketMaxLeft, 0.0f, 1.0f);
|
||||
preview.waveformRight[bucketIndex] = std::clamp(bucketMaxRight, 0.0f, 1.0f);
|
||||
}
|
||||
pcmFrames.insert(pcmFrames.end(), temp.begin(), temp.begin() + static_cast<std::ptrdiff_t>(framesRead * preview.channels));
|
||||
frameCursor += framesRead;
|
||||
}
|
||||
|
||||
ma_decoder_uninit(&decoder);
|
||||
if (frameCursor == 0) {
|
||||
return preview;
|
||||
}
|
||||
BuildWaveformPreview(preview, pcmFrames.data(), frameCursor);
|
||||
preview.loaded = true;
|
||||
return preview;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "../include/ThirdParty/miniaudio.h"
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <memory>
|
||||
|
||||
struct AudioClipPreview {
|
||||
bool loaded = false;
|
||||
@@ -20,6 +21,15 @@ struct AudioClipPreview {
|
||||
|
||||
class AudioSystem {
|
||||
public:
|
||||
struct DecodedAudioData {
|
||||
std::vector<float> pcmFrames;
|
||||
ma_audio_buffer_ref buffer{};
|
||||
ma_uint32 channels = 0;
|
||||
ma_uint32 sampleRate = 0;
|
||||
ma_uint64 frameCount = 0;
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
bool init();
|
||||
void shutdown();
|
||||
bool isReady() const { return initialized; }
|
||||
@@ -92,6 +102,7 @@ private:
|
||||
std::string clipPath;
|
||||
bool spatial = true;
|
||||
bool started = false; // prevents auto-restart after manual stop
|
||||
std::shared_ptr<DecodedAudioData> decodedData;
|
||||
};
|
||||
|
||||
ma_engine engine{};
|
||||
@@ -109,12 +120,17 @@ private:
|
||||
ma_sound previewSound{};
|
||||
bool previewActive = false;
|
||||
std::string previewPath;
|
||||
std::shared_ptr<DecodedAudioData> previewDecodedData;
|
||||
|
||||
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);
|
||||
std::shared_ptr<DecodedAudioData> decodeClipToMemory(const std::string& path);
|
||||
bool initSoundFromPath(const std::string& path, ma_uint32 flags, ma_sound_group* group, ma_sound& sound,
|
||||
std::shared_ptr<DecodedAudioData>& decodedData);
|
||||
void releaseDecodedAudio(std::shared_ptr<DecodedAudioData>& decodedData);
|
||||
void updateReverb(const std::vector<SceneObject>& objects, const glm::vec3& listenerPos);
|
||||
ReverbSettings getReverbTarget(const std::vector<SceneObject>& objects, const glm::vec3& listenerPos, float& outBlend) const;
|
||||
void applyReverbSettings(const ReverbSettings& target, float blend);
|
||||
|
||||
347
src/EditorUI.cpp
347
src/EditorUI.cpp
@@ -1,4 +1,69 @@
|
||||
#include "EditorUI.h"
|
||||
#include <unordered_map>
|
||||
|
||||
namespace {
|
||||
struct TouchSwipeWindowState {
|
||||
ImVec2 virtualScroll = ImVec2(0.0f, 0.0f);
|
||||
ImVec2 velocity = ImVec2(0.0f, 0.0f);
|
||||
bool initialized = false;
|
||||
bool touchedThisFrame = false;
|
||||
bool isDragging = false;
|
||||
};
|
||||
|
||||
struct TouchSwipeRuntimeState {
|
||||
std::unordered_map<ImGuiID, TouchSwipeWindowState> windowStates;
|
||||
ImGuiID activeWindowId = 0;
|
||||
ImVec2 dragStartPos = ImVec2(0.0f, 0.0f);
|
||||
ImVec2 lastPointerPos = ImVec2(0.0f, 0.0f);
|
||||
bool dragging = false;
|
||||
};
|
||||
|
||||
bool hasScrollableAxis(const ImGuiWindow* window, int axis) {
|
||||
if (!window || axis < 0 || axis > 1) {
|
||||
return false;
|
||||
}
|
||||
if ((window->Flags & ImGuiWindowFlags_NoInputs) != 0) {
|
||||
return false;
|
||||
}
|
||||
if ((window->Flags & ImGuiWindowFlags_NoScrollWithMouse) != 0) {
|
||||
return false;
|
||||
}
|
||||
return window->ScrollMax[axis] > 0.0f;
|
||||
}
|
||||
|
||||
bool isTouchScrollableWindow(const ImGuiWindow* window) {
|
||||
if (!window || !window->Active || window->Collapsed || window->SkipItems) {
|
||||
return false;
|
||||
}
|
||||
return hasScrollableAxis(window, 0) || hasScrollableAxis(window, 1);
|
||||
}
|
||||
|
||||
ImGuiWindow* findScrollableWindowFromHover(ImGuiWindow* hovered) {
|
||||
for (ImGuiWindow* window = hovered; window != nullptr; window = window->ParentWindow) {
|
||||
if (isTouchScrollableWindow(window)) {
|
||||
return window;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
float applyEdgeResistance(float value, float minValue, float maxValue, float resistance) {
|
||||
if (value < minValue) {
|
||||
return minValue + (value - minValue) * resistance;
|
||||
}
|
||||
if (value > maxValue) {
|
||||
return maxValue + (value - maxValue) * resistance;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
float computeElasticOverscrollLimit(float axisExtent, float scrollMax) {
|
||||
const float byViewport = axisExtent * 0.14f;
|
||||
const float byRange = scrollMax * 0.35f + 6.0f;
|
||||
return ImClamp(std::min(byViewport, byRange), 6.0f, 26.0f);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#pragma region File Browser
|
||||
FileBrowser::FileBrowser() {
|
||||
@@ -453,3 +518,285 @@ ImGuiID setupDockspace(const std::function<void()>& menuBarContent) {
|
||||
return dockspaceId;
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region Touch Swipe Scroll
|
||||
void updateTouchSwipeScrolling() {
|
||||
ImGuiContext* context = ImGui::GetCurrentContext();
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImGuiContext& g = *context;
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
static TouchSwipeRuntimeState runtime;
|
||||
|
||||
const bool touchScreenMode = (io.ConfigFlags & ImGuiConfigFlags_IsTouchScreen) != 0;
|
||||
|
||||
const float dt = std::max(io.DeltaTime, 1.0f / 240.0f);
|
||||
const float dragThresholdSqr = 16.0f;
|
||||
const float edgeResistance = 0.18f;
|
||||
const float wheelEdgeResistance = 0.52f;
|
||||
const float freeScrollFriction = 5.8f;
|
||||
const float overscrollReturnRate = 2.2f;
|
||||
const float overscrollVelocityDamping = 9.0f;
|
||||
const float settleVelocityEpsilon = 0.35f;
|
||||
const float settlePositionEpsilon = 0.20f;
|
||||
|
||||
for (auto& [id, state] : runtime.windowStates) {
|
||||
state.touchedThisFrame = false;
|
||||
if (id != runtime.activeWindowId) {
|
||||
state.isDragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
for (ImGuiWindow* window : g.Windows) {
|
||||
if (!isTouchScrollableWindow(window)) {
|
||||
continue;
|
||||
}
|
||||
TouchSwipeWindowState& state = runtime.windowStates[window->ID];
|
||||
state.touchedThisFrame = true;
|
||||
if (!state.initialized) {
|
||||
state.initialized = true;
|
||||
state.virtualScroll = window->Scroll;
|
||||
state.velocity = ImVec2(0.0f, 0.0f);
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool stateIsIdle = !state.isDragging &&
|
||||
std::abs(state.velocity.x) < 1.0f &&
|
||||
std::abs(state.velocity.y) < 1.0f;
|
||||
if (stateIsIdle) {
|
||||
state.virtualScroll = window->Scroll;
|
||||
}
|
||||
}
|
||||
|
||||
if (runtime.activeWindowId != 0) {
|
||||
ImGuiWindow* activeWindow = ImGui::FindWindowByID(runtime.activeWindowId);
|
||||
auto it = runtime.windowStates.find(runtime.activeWindowId);
|
||||
if (!activeWindow || !isTouchScrollableWindow(activeWindow) ||
|
||||
it == runtime.windowStates.end() || !it->second.touchedThisFrame) {
|
||||
runtime.activeWindowId = 0;
|
||||
runtime.dragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
ImVec2 wheel(io.MouseWheelH, io.MouseWheel);
|
||||
if (io.MouseWheelRequestAxisSwap) {
|
||||
wheel = ImVec2(wheel.y, 0.0f);
|
||||
}
|
||||
if ((std::abs(wheel.x) > 0.0001f || std::abs(wheel.y) > 0.0001f) && g.MovingWindow == nullptr) {
|
||||
ImGuiWindow* baseWindow = g.WheelingWindow ? g.WheelingWindow : g.HoveredWindow;
|
||||
ImGuiWindow* wheelWindow = findScrollableWindowFromHover(baseWindow);
|
||||
if (wheelWindow) {
|
||||
TouchSwipeWindowState& state = runtime.windowStates[wheelWindow->ID];
|
||||
state.touchedThisFrame = true;
|
||||
if (!state.initialized) {
|
||||
state.initialized = true;
|
||||
state.virtualScroll = wheelWindow->Scroll;
|
||||
state.velocity = ImVec2(0.0f, 0.0f);
|
||||
}
|
||||
for (int axis = 0; axis < 2; ++axis) {
|
||||
const float wheelDelta = (axis == 0) ? wheel.x : wheel.y;
|
||||
if (!hasScrollableAxis(wheelWindow, axis) || std::abs(wheelDelta) < 0.0001f) {
|
||||
continue;
|
||||
}
|
||||
const float maxScroll = wheelWindow->ScrollMax[axis];
|
||||
const float currentScroll = wheelWindow->Scroll[axis];
|
||||
const bool pushingMin = wheelDelta > 0.0f;
|
||||
const bool pushingMax = wheelDelta < 0.0f;
|
||||
const bool atMin = currentScroll <= 0.5f;
|
||||
const bool atMax = currentScroll >= maxScroll - 0.5f;
|
||||
const bool outside = state.virtualScroll[axis] < 0.0f || state.virtualScroll[axis] > maxScroll;
|
||||
if (!outside && !((pushingMin && atMin) || (pushingMax && atMax))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float maxStep = (axis == 0)
|
||||
? (wheelWindow->InnerRect.GetWidth() * 0.67f)
|
||||
: (wheelWindow->InnerRect.GetHeight() * 0.67f);
|
||||
const float baseStep = (axis == 0)
|
||||
? (2.0f * wheelWindow->FontRefSize)
|
||||
: (5.0f * wheelWindow->FontRefSize);
|
||||
const float scrollStep = ImTrunc(ImMin(baseStep, maxStep));
|
||||
const float delta = -wheelDelta * scrollStep;
|
||||
const float axisExtent = (axis == 0)
|
||||
? wheelWindow->InnerRect.GetWidth()
|
||||
: wheelWindow->InnerRect.GetHeight();
|
||||
const float overscrollLimit = computeElasticOverscrollLimit(axisExtent, maxScroll);
|
||||
const float clampedValue = ImClamp(state.virtualScroll[axis], 0.0f, maxScroll);
|
||||
const float overshoot = std::abs(state.virtualScroll[axis] - clampedValue);
|
||||
const float remainingFactor = ImClamp(
|
||||
1.0f - (overshoot / std::max(overscrollLimit, 0.001f)),
|
||||
0.12f,
|
||||
1.0f);
|
||||
|
||||
const float previousValue = state.virtualScroll[axis];
|
||||
const float stretchedValue = applyEdgeResistance(
|
||||
previousValue + delta * 0.50f * remainingFactor,
|
||||
0.0f,
|
||||
maxScroll,
|
||||
wheelEdgeResistance);
|
||||
state.virtualScroll[axis] = stretchedValue;
|
||||
const float frameVelocity = (stretchedValue - previousValue) / dt;
|
||||
state.velocity[axis] = ImLerp(state.velocity[axis], frameVelocity, 0.35f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (touchScreenMode) {
|
||||
if (io.MouseClicked[0] && runtime.activeWindowId == 0 &&
|
||||
g.ActiveId == 0 && g.MovingWindow == nullptr) {
|
||||
ImGuiWindow* hovered = findScrollableWindowFromHover(g.HoveredWindow);
|
||||
if (hovered) {
|
||||
const bool clickInTitleBar = hovered->TitleBarHeight > 0.0f &&
|
||||
hovered->TitleBarRect().Contains(io.MouseClickedPos[0]);
|
||||
if (!clickInTitleBar) {
|
||||
runtime.activeWindowId = hovered->ID;
|
||||
runtime.dragStartPos = io.MouseClickedPos[0];
|
||||
runtime.lastPointerPos = io.MousePos;
|
||||
runtime.dragging = false;
|
||||
TouchSwipeWindowState& state = runtime.windowStates[hovered->ID];
|
||||
state.isDragging = false;
|
||||
state.velocity = ImVec2(0.0f, 0.0f);
|
||||
state.virtualScroll = hovered->Scroll;
|
||||
state.initialized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!io.MouseDown[0]) {
|
||||
if (runtime.activeWindowId != 0) {
|
||||
auto it = runtime.windowStates.find(runtime.activeWindowId);
|
||||
if (it != runtime.windowStates.end()) {
|
||||
it->second.isDragging = false;
|
||||
}
|
||||
}
|
||||
runtime.activeWindowId = 0;
|
||||
runtime.dragging = false;
|
||||
} else if (runtime.activeWindowId != 0) {
|
||||
ImGuiWindow* activeWindow = ImGui::FindWindowByID(runtime.activeWindowId);
|
||||
auto it = runtime.windowStates.find(runtime.activeWindowId);
|
||||
if (activeWindow && it != runtime.windowStates.end()) {
|
||||
TouchSwipeWindowState& state = it->second;
|
||||
const ImVec2 totalDragDelta(
|
||||
io.MousePos.x - runtime.dragStartPos.x,
|
||||
io.MousePos.y - runtime.dragStartPos.y);
|
||||
if (!runtime.dragging && ImLengthSqr(totalDragDelta) >= dragThresholdSqr) {
|
||||
runtime.dragging = true;
|
||||
state.isDragging = true;
|
||||
}
|
||||
|
||||
const ImVec2 pointerDelta(
|
||||
io.MousePos.x - runtime.lastPointerPos.x,
|
||||
io.MousePos.y - runtime.lastPointerPos.y);
|
||||
runtime.lastPointerPos = io.MousePos;
|
||||
|
||||
if (runtime.dragging && g.ActiveId == 0 && g.MovingWindow == nullptr) {
|
||||
for (int axis = 0; axis < 2; ++axis) {
|
||||
if (!hasScrollableAxis(activeWindow, axis)) {
|
||||
continue;
|
||||
}
|
||||
const float maxScroll = activeWindow->ScrollMax[axis];
|
||||
const float previousValue = state.virtualScroll[axis];
|
||||
const float draggedValue = previousValue - pointerDelta[axis];
|
||||
state.virtualScroll[axis] = applyEdgeResistance(
|
||||
draggedValue, 0.0f, maxScroll, edgeResistance);
|
||||
const float frameVelocity = (state.virtualScroll[axis] - previousValue) / dt;
|
||||
state.velocity[axis] = ImLerp(state.velocity[axis], frameVelocity, 0.65f);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
runtime.activeWindowId = 0;
|
||||
runtime.dragging = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (runtime.activeWindowId != 0) {
|
||||
auto it = runtime.windowStates.find(runtime.activeWindowId);
|
||||
if (it != runtime.windowStates.end()) {
|
||||
it->second.isDragging = false;
|
||||
}
|
||||
}
|
||||
runtime.activeWindowId = 0;
|
||||
runtime.dragging = false;
|
||||
}
|
||||
|
||||
for (auto& [windowId, state] : runtime.windowStates) {
|
||||
if (!state.touchedThisFrame) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ImGuiWindow* window = ImGui::FindWindowByID(windowId);
|
||||
if (!window) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool draggingThisWindow = runtime.dragging &&
|
||||
runtime.activeWindowId == windowId &&
|
||||
io.MouseDown[0] &&
|
||||
state.isDragging;
|
||||
|
||||
for (int axis = 0; axis < 2; ++axis) {
|
||||
if (!hasScrollableAxis(window, axis)) {
|
||||
state.virtualScroll[axis] = 0.0f;
|
||||
state.velocity[axis] = 0.0f;
|
||||
continue;
|
||||
}
|
||||
|
||||
const float maxScroll = window->ScrollMax[axis];
|
||||
if (!draggingThisWindow) {
|
||||
if (!touchScreenMode &&
|
||||
state.virtualScroll[axis] >= -0.05f &&
|
||||
state.virtualScroll[axis] <= maxScroll + 0.05f) {
|
||||
state.virtualScroll[axis] = window->Scroll[axis];
|
||||
state.velocity[axis] *= 0.5f;
|
||||
}
|
||||
|
||||
float value = state.virtualScroll[axis];
|
||||
float velocity = state.velocity[axis];
|
||||
value += velocity * dt;
|
||||
|
||||
const float clampedValue = ImClamp(value, 0.0f, maxScroll);
|
||||
const float stretch = value - clampedValue;
|
||||
if (std::abs(stretch) > 0.0f) {
|
||||
const float returnBlend = 1.0f - std::exp(-overscrollReturnRate * dt);
|
||||
value = ImLerp(value, clampedValue, returnBlend);
|
||||
velocity *= std::exp(-overscrollVelocityDamping * dt);
|
||||
} else {
|
||||
velocity *= std::exp(-freeScrollFriction * dt);
|
||||
}
|
||||
|
||||
if (std::abs(velocity) < settleVelocityEpsilon &&
|
||||
std::abs(value - clampedValue) < settlePositionEpsilon) {
|
||||
value = clampedValue;
|
||||
velocity = 0.0f;
|
||||
}
|
||||
|
||||
state.virtualScroll[axis] = value;
|
||||
state.velocity[axis] = velocity;
|
||||
}
|
||||
|
||||
const float axisExtent = (axis == 0) ? window->InnerRect.GetWidth() : window->InnerRect.GetHeight();
|
||||
const float overscrollLimit = computeElasticOverscrollLimit(axisExtent, maxScroll);
|
||||
const float targetScroll = ImClamp(
|
||||
state.virtualScroll[axis],
|
||||
-overscrollLimit,
|
||||
maxScroll + overscrollLimit);
|
||||
state.virtualScroll[axis] = targetScroll;
|
||||
if (axis == 0) {
|
||||
ImGui::SetScrollX(window, targetScroll);
|
||||
} else {
|
||||
ImGui::SetScrollY(window, targetScroll);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto it = runtime.windowStates.begin(); it != runtime.windowStates.end();) {
|
||||
if (!it->second.touchedThisFrame && it->first != runtime.activeWindowId) {
|
||||
it = runtime.windowStates.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
@@ -75,4 +75,7 @@ void applySuperRoundStyle(ImGuiStyle& style);
|
||||
|
||||
// Setup ImGui dockspace for the editor and return its stable dockspace ID.
|
||||
ImGuiID setupDockspace(const std::function<void()>& menuBarContent = nullptr);
|
||||
|
||||
// Apply touch-style swipe scrolling with inertial motion and elastic edge return.
|
||||
void updateTouchSwipeScrolling();
|
||||
#pragma endregion
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -732,6 +732,7 @@ void Engine::renderHierarchyPanel() {
|
||||
if (ImGui::MenuItem("Plane")) addObject(ObjectType::Plane, "Plane");
|
||||
if (ImGui::MenuItem("Torus")) addObject(ObjectType::Torus, "Torus");
|
||||
if (ImGui::MenuItem("Sprite (Quad)")) addObject(ObjectType::Sprite, "Sprite");
|
||||
if (ImGui::MenuItem("2.5D Object")) addObject(ObjectType::Sprite25D, "2.5D Object");
|
||||
if (ImGui::MenuItem("Mirror")) addObject(ObjectType::Mirror, "Mirror");
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
@@ -1795,7 +1796,9 @@ void Engine::renderInspectorPanel() {
|
||||
ImGui::Text("Type:");
|
||||
ImGui::SameLine();
|
||||
const char* typeLabel = "Empty";
|
||||
if (obj.hasRenderer) {
|
||||
if (obj.type == ObjectType::Sprite25D) {
|
||||
typeLabel = "2.5D Object";
|
||||
} else if (obj.hasRenderer) {
|
||||
switch (obj.renderType) {
|
||||
case RenderType::Cube: typeLabel = "Cube"; break;
|
||||
case RenderType::Sphere: typeLabel = "Sphere"; break;
|
||||
@@ -1865,7 +1868,9 @@ void Engine::renderInspectorPanel() {
|
||||
if (obj.hasPostFX) {
|
||||
ImGui::TextDisabled("Transform is ignored for post-processing nodes.");
|
||||
}
|
||||
if (isUIObject(obj)) {
|
||||
if (obj.type == ObjectType::Sprite25D) {
|
||||
ImGui::TextDisabled("2.5D objects use the transform for 3D placement and the UI section for sprite content.");
|
||||
} else if (isUIObject(obj)) {
|
||||
ImGui::TextDisabled("UI objects use the UI section for positioning.");
|
||||
}
|
||||
|
||||
@@ -1919,15 +1924,19 @@ void Engine::renderInspectorPanel() {
|
||||
ImGui::PushID("UI");
|
||||
ImGui::Indent(10.0f);
|
||||
|
||||
const char* anchors[] = { "Center", "Top Left", "Top Right", "Bottom Left", "Bottom Right" };
|
||||
int anchor = static_cast<int>(obj.ui.anchor);
|
||||
if (ImGui::Combo("Anchor", &anchor, anchors, IM_ARRAYSIZE(anchors))) {
|
||||
obj.ui.anchor = static_cast<UIAnchor>(anchor);
|
||||
changed = true;
|
||||
}
|
||||
if (obj.type != ObjectType::Sprite25D) {
|
||||
const char* anchors[] = { "Center", "Top Left", "Top Right", "Bottom Left", "Bottom Right" };
|
||||
int anchor = static_cast<int>(obj.ui.anchor);
|
||||
if (ImGui::Combo("Anchor", &anchor, anchors, IM_ARRAYSIZE(anchors))) {
|
||||
obj.ui.anchor = static_cast<UIAnchor>(anchor);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (ImGui::DragFloat2("Position (px)", &obj.ui.position.x, 1.0f)) {
|
||||
changed = true;
|
||||
if (ImGui::DragFloat2("Position (px)", &obj.ui.position.x, 1.0f)) {
|
||||
changed = true;
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled("Anchor and UI position are ignored for projected 2.5D sprites.");
|
||||
}
|
||||
|
||||
if (ImGui::DragFloat("Rotation (deg)", &obj.ui.rotation, 0.5f, -360.0f, 360.0f)) {
|
||||
@@ -1949,6 +1958,9 @@ void Engine::renderInspectorPanel() {
|
||||
changed = true;
|
||||
}
|
||||
if (obj.ui.renderIn3D) {
|
||||
if (ImGui::Checkbox("Face Camera", &obj.faceCamera)) {
|
||||
changed = true;
|
||||
}
|
||||
int size[2] = { obj.ui.renderTargetSize.x, obj.ui.renderTargetSize.y };
|
||||
if (ImGui::DragInt2("Render Target (px)", size, 1.0f, 16, 4096)) {
|
||||
obj.ui.renderTargetSize.x = std::max(16, size[0]);
|
||||
@@ -3564,6 +3576,45 @@ void Engine::renderInspectorPanel() {
|
||||
materialChanged |= textureField("Detail Map", "ObjOverlay", obj.overlayTexturePath);
|
||||
materialChanged |= textureField("Normal Map", "ObjNormal", obj.normalMapPath);
|
||||
|
||||
if (obj.renderType == RenderType::Sprite) {
|
||||
if (!obj.albedoTexturePath.empty()) {
|
||||
if (ImGui::SmallButton("Import Sheet##WorldSpriteSheet")) {
|
||||
pendingSpriteSheetPath = obj.albedoTexturePath;
|
||||
std::snprintf(importSpriteSheetName, sizeof(importSpriteSheetName), "%s", obj.name.c_str());
|
||||
importSpriteSheetAsSprite2D = false;
|
||||
showImportSpriteSheetDialog = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::CollapsingHeader("Sprite Sheet", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
if (ImGui::Checkbox("Enable Sprite Sheet", &obj.ui.spriteSheetEnabled)) {
|
||||
materialChanged = true;
|
||||
}
|
||||
ImGui::BeginDisabled(!obj.ui.spriteSheetEnabled);
|
||||
if (ImGui::DragInt("Columns", &obj.ui.spriteSheetColumns, 1.0f, 1, 1024)) {
|
||||
obj.ui.spriteSheetColumns = std::max(1, obj.ui.spriteSheetColumns);
|
||||
materialChanged = true;
|
||||
}
|
||||
if (ImGui::DragInt("Rows", &obj.ui.spriteSheetRows, 1.0f, 1, 1024)) {
|
||||
obj.ui.spriteSheetRows = std::max(1, obj.ui.spriteSheetRows);
|
||||
materialChanged = true;
|
||||
}
|
||||
int frameCount = std::max(1, obj.ui.spriteSheetColumns * obj.ui.spriteSheetRows);
|
||||
if (ImGui::SliderInt("Frame", &obj.ui.spriteSheetFrame, 0, frameCount - 1)) {
|
||||
obj.ui.spriteSheetFrame = std::clamp(obj.ui.spriteSheetFrame, 0, frameCount - 1);
|
||||
materialChanged = true;
|
||||
}
|
||||
if (ImGui::DragFloat("FPS", &obj.ui.spriteSheetFps, 0.1f, 1.0f, 120.0f, "%.1f")) {
|
||||
obj.ui.spriteSheetFps = std::clamp(obj.ui.spriteSheetFps, 1.0f, 120.0f);
|
||||
materialChanged = true;
|
||||
}
|
||||
if (ImGui::Checkbox("Loop", &obj.ui.spriteSheetLoop)) {
|
||||
materialChanged = true;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("Shader");
|
||||
const char* shaderPresetOptions[] = { "Custom", "Engine Lit (Default)", "Scrolling UV" };
|
||||
@@ -4501,6 +4552,7 @@ void Engine::renderInspectorPanel() {
|
||||
addEntry("Renderer/Sprite (Quad)", true, [&]() {
|
||||
obj.hasRenderer = true;
|
||||
obj.renderType = RenderType::Sprite;
|
||||
obj.faceCamera = false;
|
||||
obj.scale = glm::vec3(1.0f, 1.0f, 0.05f);
|
||||
obj.material.ambientStrength = 1.0f;
|
||||
syncLocalTransform(obj);
|
||||
|
||||
@@ -21,6 +21,59 @@
|
||||
#include <shlobj.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
bool ProjectWorldToOverlayPoint(const glm::vec3& worldPos,
|
||||
const glm::mat4& view,
|
||||
const glm::mat4& proj,
|
||||
const ImVec2& overlayPos,
|
||||
const ImVec2& overlaySize,
|
||||
ImVec2& outScreen) {
|
||||
glm::vec4 clip = proj * view * glm::vec4(worldPos, 1.0f);
|
||||
if (clip.w <= 0.0001f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
glm::vec3 ndc = glm::vec3(clip) / clip.w;
|
||||
if (ndc.z < -1.0f || ndc.z > 1.0f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outScreen.x = overlayPos.x + (ndc.x * 0.5f + 0.5f) * overlaySize.x;
|
||||
outScreen.y = overlayPos.y + (1.0f - (ndc.y * 0.5f + 0.5f)) * overlaySize.y;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ResolveProjectedSprite25DRect(const SceneObject& obj,
|
||||
const glm::mat4& view,
|
||||
const glm::mat4& proj,
|
||||
const ImVec2& overlayPos,
|
||||
const ImVec2& overlaySize,
|
||||
ImVec2& outMin,
|
||||
ImVec2& outMax) {
|
||||
glm::mat4 invView = glm::inverse(view);
|
||||
glm::vec3 cameraRight = glm::normalize(glm::vec3(invView[0]));
|
||||
glm::vec3 cameraUp = glm::normalize(glm::vec3(invView[1]));
|
||||
glm::vec2 baseSize = glm::max(obj.ui.size, glm::vec2(1.0f));
|
||||
glm::vec3 objectScale = glm::max(glm::abs(obj.scale), glm::vec3(0.01f));
|
||||
glm::vec2 worldHalfExtents = glm::vec2(baseSize.x * objectScale.x, baseSize.y * objectScale.y) * 0.005f;
|
||||
|
||||
ImVec2 center;
|
||||
ImVec2 rightPoint;
|
||||
ImVec2 upPoint;
|
||||
if (!ProjectWorldToOverlayPoint(obj.position, view, proj, overlayPos, overlaySize, center) ||
|
||||
!ProjectWorldToOverlayPoint(obj.position + cameraRight * worldHalfExtents.x, view, proj, overlayPos, overlaySize, rightPoint) ||
|
||||
!ProjectWorldToOverlayPoint(obj.position + cameraUp * worldHalfExtents.y, view, proj, overlayPos, overlaySize, upPoint)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
float halfWidth = std::max(1.0f, std::abs(rightPoint.x - center.x));
|
||||
float halfHeight = std::max(1.0f, std::abs(upPoint.y - center.y));
|
||||
outMin = ImVec2(center.x - halfWidth, center.y - halfHeight);
|
||||
outMax = ImVec2(center.x + halfWidth, center.y + halfHeight);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma region Gizmo Toolbar
|
||||
namespace GizmoToolbar {
|
||||
enum class Icon {
|
||||
@@ -1219,6 +1272,18 @@ void Engine::renderGameViewportWindow() {
|
||||
if (useWorldUi) {
|
||||
uiWorldCamera.viewportSize = glm::vec2(overlaySize.x, overlaySize.y);
|
||||
}
|
||||
Camera projectedUiCamera = playerCam ? makeCameraFromObject(*playerCam) : Camera{};
|
||||
glm::mat4 projectedUiView(1.0f);
|
||||
glm::mat4 projectedUiProj(1.0f);
|
||||
bool hasProjectedUiCamera = false;
|
||||
if (playerCam && !playerCam->camera.use2D) {
|
||||
projectedUiView = projectedUiCamera.getViewMatrix();
|
||||
projectedUiProj = glm::perspective(glm::radians(playerCam->camera.fov),
|
||||
std::max(0.1f, overlaySize.x / std::max(1.0f, overlaySize.y)),
|
||||
playerCam->camera.nearClip,
|
||||
playerCam->camera.farClip);
|
||||
hasProjectedUiCamera = true;
|
||||
}
|
||||
auto worldToScreen = [&](const glm::vec2& world) {
|
||||
glm::vec2 local = uiWorldCamera.WorldToScreen(world);
|
||||
return ImVec2(overlayPos.x + local.x, overlayPos.y + local.y);
|
||||
@@ -1247,6 +1312,9 @@ void Engine::renderGameViewportWindow() {
|
||||
return uiWorldCamera.position * (1.0f - factor);
|
||||
};
|
||||
auto resolveUIRectWorld = [&](const SceneObject& obj, ImVec2& outMin, ImVec2& outMax) {
|
||||
if (obj.type == ObjectType::Sprite25D && hasProjectedUiCamera) {
|
||||
return ResolveProjectedSprite25DRect(obj, projectedUiView, projectedUiProj, overlayPos, overlaySize, outMin, 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);
|
||||
@@ -1257,6 +1325,7 @@ void Engine::renderGameViewportWindow() {
|
||||
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));
|
||||
return true;
|
||||
};
|
||||
auto rectOutsideOverlay = [&](const ImVec2& min, const ImVec2& max) {
|
||||
return (max.x < overlayPos.x || min.x > overlayPos.x + overlaySize.x ||
|
||||
@@ -1408,8 +1477,8 @@ void Engine::renderGameViewportWindow() {
|
||||
for (SceneObject* objPtr : uiDrawList) {
|
||||
SceneObject& obj = *objPtr;
|
||||
ImVec2 rectMin, rectMax;
|
||||
if (useWorldUi) {
|
||||
resolveUIRectWorld(obj, rectMin, rectMax);
|
||||
if (useWorldUi || obj.type == ObjectType::Sprite25D) {
|
||||
if (!resolveUIRectWorld(obj, rectMin, rectMax)) continue;
|
||||
} else {
|
||||
resolveUIRect(obj, rectMin, rectMax);
|
||||
}
|
||||
@@ -1710,11 +1779,13 @@ void Engine::renderGameViewportWindow() {
|
||||
if (selected && isUIType(*selected) && selected->ui.type != UIElementType::Canvas) {
|
||||
ImVec2 rectMin, rectMax;
|
||||
ImVec2 parentMin, parentMax;
|
||||
if (useWorldUi) {
|
||||
resolveUIRectWorld(*selected, rectMin, rectMax);
|
||||
bool haveRect = true;
|
||||
if (useWorldUi || selected->type == ObjectType::Sprite25D) {
|
||||
haveRect = resolveUIRectWorld(*selected, rectMin, rectMax);
|
||||
} else {
|
||||
resolveUIRect(*selected, rectMin, rectMax, &parentMin, &parentMax);
|
||||
}
|
||||
if (haveRect) {
|
||||
ImVec2 rectSize(rectMax.x - rectMin.x, rectMax.y - rectMin.y);
|
||||
|
||||
ImGuizmo::OPERATION op = ImGuizmo::TRANSLATE;
|
||||
@@ -1800,6 +1871,7 @@ void Engine::renderGameViewportWindow() {
|
||||
projectManager.currentProject.hasUnsavedChanges = true;
|
||||
gizmoUsed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1993,6 +2065,7 @@ struct DockDrawerState {
|
||||
bool collapsed = false;
|
||||
float openAmount = 1.0f;
|
||||
float expandedExtent = 0.0f;
|
||||
ImGuiID pendingTabFocusId = 0;
|
||||
};
|
||||
|
||||
void addRotatedText90CW(ImDrawList* drawList,
|
||||
@@ -2134,22 +2207,84 @@ DockTabInteractionState queryDockTabInteraction(const DockDrawerTarget& target,
|
||||
return out;
|
||||
}
|
||||
|
||||
void queueDrawerTabFocus(DockDrawerState& state, ImGuiTabBar* tabBar, ImGuiID tabId) {
|
||||
if (!tabBar || tabId == 0) return;
|
||||
for (int i = 0; i < tabBar->Tabs.Size; ++i) {
|
||||
ImGuiTabItem* tab = &tabBar->Tabs[i];
|
||||
if (tab->ID != tabId) continue;
|
||||
ImGui::TabBarQueueFocus(tabBar, tab);
|
||||
state.pendingTabFocusId = tabId;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void applyPendingDrawerTabFocus(DockDrawerState& state, ImGuiTabBar* tabBar) {
|
||||
if (!tabBar || state.pendingTabFocusId == 0) return;
|
||||
|
||||
bool found = false;
|
||||
for (int i = 0; i < tabBar->Tabs.Size; ++i) {
|
||||
ImGuiTabItem* tab = &tabBar->Tabs[i];
|
||||
if (tab->ID != state.pendingTabFocusId) continue;
|
||||
found = true;
|
||||
if (tabBar->SelectedTabId == tab->ID || tabBar->VisibleTabId == tab->ID) {
|
||||
state.pendingTabFocusId = 0;
|
||||
return;
|
||||
}
|
||||
ImGui::TabBarQueueFocus(tabBar, tab);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
state.pendingTabFocusId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void renderCollapsedSideDockRail(DockDrawerState& state,
|
||||
const DockDrawerTarget& target,
|
||||
DockDrawerSide side,
|
||||
float railWidth) {
|
||||
float railWidth,
|
||||
float revealAmount) {
|
||||
if (side == DockDrawerSide::Bottom) return;
|
||||
if (!target.drawerBranch || !target.splitParent) return;
|
||||
ImGuiTabBar* tabBar = target.drawerBranch->TabBar;
|
||||
if (!tabBar || tabBar->Tabs.Size <= 0) return;
|
||||
const float reveal = std::clamp(revealAmount, 0.0f, 1.0f);
|
||||
if (reveal <= 0.001f) return;
|
||||
|
||||
ImVec2 railPos = target.drawerBranch->Pos;
|
||||
const float splitMinX = target.splitParent->Pos.x;
|
||||
const float splitMaxX = target.splitParent->Pos.x + target.splitParent->Size.x;
|
||||
const float splitWidth = ImMax(1.0f, splitMaxX - splitMinX);
|
||||
const float fullRailWidth = std::clamp(ImMax(railWidth, 22.0f), 8.0f, splitWidth);
|
||||
const float visibleRailWidth = ImMax(1.0f, fullRailWidth * reveal);
|
||||
|
||||
const float branchMinX = target.drawerBranch->Pos.x;
|
||||
const float branchMaxX = target.drawerBranch->Pos.x + target.drawerBranch->Size.x;
|
||||
const float branchMinY = target.drawerBranch->Pos.y;
|
||||
const float branchMaxY = target.drawerBranch->Pos.y + target.drawerBranch->Size.y;
|
||||
|
||||
ImVec2 railPos(branchMinX, branchMinY);
|
||||
ImVec2 railSize = target.drawerBranch->Size;
|
||||
const float desiredRailWidth = ImMax(railWidth, 22.0f);
|
||||
railSize.x = ImMin(desiredRailWidth, railSize.x);
|
||||
if (side == DockDrawerSide::Right) {
|
||||
railPos.x = target.drawerBranch->Pos.x + target.drawerBranch->Size.x - railSize.x;
|
||||
railSize.x = visibleRailWidth;
|
||||
|
||||
const bool hasValidBarRect = tabBar->BarRect.GetWidth() > 1.0f && tabBar->BarRect.GetHeight() > 1.0f;
|
||||
const float hingeX = [&]() {
|
||||
if (side == DockDrawerSide::Left) {
|
||||
const float preferred = hasValidBarRect ? tabBar->BarRect.Max.x : branchMaxX;
|
||||
return std::clamp(preferred, splitMinX, splitMaxX);
|
||||
}
|
||||
const float preferred = hasValidBarRect ? tabBar->BarRect.Min.x : branchMinX;
|
||||
return std::clamp(preferred, splitMinX, splitMaxX);
|
||||
}();
|
||||
|
||||
railPos.x = (side == DockDrawerSide::Left) ? (hingeX - visibleRailWidth) : hingeX;
|
||||
railPos.x = std::clamp(railPos.x, splitMinX, splitMaxX - railSize.x);
|
||||
|
||||
float railTopY = branchMinY;
|
||||
if (hasValidBarRect) {
|
||||
railTopY = ImMax(railTopY, tabBar->BarRect.Max.y - 1.0f);
|
||||
}
|
||||
railPos.y = railTopY;
|
||||
railSize.y = ImMax(1.0f, branchMaxY - railTopY);
|
||||
|
||||
char railWindowName[64];
|
||||
std::snprintf(railWindowName, sizeof(railWindowName), "##DockRail_%c_%08X",
|
||||
@@ -2169,62 +2304,92 @@ void renderCollapsedSideDockRail(DockDrawerState& state,
|
||||
if (target.drawerBranch->HostWindow) {
|
||||
ImGui::SetNextWindowViewport(target.drawerBranch->HostWindow->ViewportId);
|
||||
}
|
||||
ImGui::SetNextWindowBgAlpha(0.94f);
|
||||
ImGui::SetNextWindowBgAlpha(0.94f * reveal);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(1.0f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
|
||||
if (ImGui::Begin(railWindowName, nullptr, railFlags)) {
|
||||
ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(1.0f, 1.0f));
|
||||
ImDrawList* draw = ImGui::GetWindowDrawList();
|
||||
const ImGuiStyle& style = ImGui::GetStyle();
|
||||
const ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
const int tabCount = tabBar->Tabs.Size;
|
||||
const float slotSpacing = style.ItemSpacing.y;
|
||||
const float minSlotHeight = ImGui::GetFrameHeight() + 8.0f;
|
||||
const float preferredSlotHeight = ImGui::GetFrameHeight() + 55.0f;
|
||||
const float availableSlotHeight = (tabCount > 0)
|
||||
? ImMax(minSlotHeight, (avail.y - slotSpacing * (tabCount - 1)) / static_cast<float>(tabCount))
|
||||
: preferredSlotHeight;
|
||||
const float slotHeight = std::clamp(preferredSlotHeight, minSlotHeight, availableSlotHeight);
|
||||
const float slotWidth = ImMax(12.0f, avail.x);
|
||||
const float usedHeight = tabCount * slotHeight + ImMax(0, tabCount - 1) * slotSpacing;
|
||||
const float topPad = ImMax(0.0f, (avail.y - usedHeight) * 0.5f);
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + topPad);
|
||||
const float slotSpacing = ImMax(1.0f, style.ItemInnerSpacing.y);
|
||||
const ImVec2 railMin = ImGui::GetWindowPos();
|
||||
const ImVec2 railMax(railMin.x + ImGui::GetWindowSize().x, railMin.y + ImGui::GetWindowSize().y);
|
||||
const ImRect railRect(railMin, railMax);
|
||||
draw->AddRectFilled(railRect.Min, railRect.Max, ImGui::GetColorU32(ImGuiCol_Tab));
|
||||
draw->AddRect(railRect.Min, railRect.Max, ImGui::GetColorU32(ImGuiCol_Border));
|
||||
|
||||
const bool tabBarFocused = (tabBar->Flags & ImGuiTabBarFlags_IsFocused) != 0;
|
||||
const float minSlotHeight = ImGui::GetFrameHeight();
|
||||
const float maxSlotHeight = ImGui::GetFrameHeight() * 3.2f;
|
||||
const float slotWidth = ImMax(3.0f, ImGui::GetContentRegionAvail().x);
|
||||
float cursorY = ImGui::GetCursorPosY() + style.FramePadding.y;
|
||||
|
||||
for (int i = 0; i < tabBar->Tabs.Size; ++i) {
|
||||
ImGuiTabItem* tab = &tabBar->Tabs[i];
|
||||
const char* tabName = ImGui::TabBarGetTabName(tabBar, tab);
|
||||
const bool selected = (tabBar->SelectedTabId == tab->ID) || (tabBar->VisibleTabId == tab->ID);
|
||||
const ImVec2 labelSize = ImGui::CalcTextSize(tabName);
|
||||
const float slotHeight = std::clamp(labelSize.x + style.FramePadding.x * 2.0f, minSlotHeight, maxSlotHeight);
|
||||
|
||||
ImGui::PushID(static_cast<int>(tab->ID));
|
||||
ImGui::SetCursorPosY(cursorY);
|
||||
ImVec2 slotPos = ImGui::GetCursorScreenPos();
|
||||
ImVec2 slotSize(slotWidth, slotHeight);
|
||||
if (ImGui::InvisibleButton("##SideTab", slotSize)) {
|
||||
ImGui::TabBarQueueFocus(tabBar, tab);
|
||||
queueDrawerTabFocus(state, tabBar, tab->ID);
|
||||
state.collapsed = false;
|
||||
}
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
if (hovered) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
}
|
||||
const ImRect slotRect(slotPos, ImVec2(slotPos.x + slotSize.x, slotPos.y + slotSize.y));
|
||||
ImU32 bg = 0;
|
||||
if (selected) bg = ImGui::GetColorU32(ImGuiCol_TabActive);
|
||||
else if (hovered) bg = ImGui::GetColorU32(ImGuiCol_TabHovered);
|
||||
else bg = ImGui::GetColorU32(ImGuiCol_Tab);
|
||||
draw->AddRectFilled(slotRect.Min, slotRect.Max, bg, style.TabRounding);
|
||||
draw->AddRect(slotRect.Min, slotRect.Max, ImGui::GetColorU32(ImGuiCol_Border), style.TabRounding);
|
||||
|
||||
const ImU32 textCol = ImGui::GetColorU32(ImGuiCol_Text);
|
||||
const ImU32 bg = selected
|
||||
? ImGui::GetColorU32(tabBarFocused ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected)
|
||||
: (hovered ? ImGui::GetColorU32(ImGuiCol_TabHovered)
|
||||
: ImGui::GetColorU32(tabBarFocused ? ImGuiCol_Tab : ImGuiCol_TabDimmed));
|
||||
const ImU32 border = ImGui::GetColorU32(ImGuiCol_Border);
|
||||
const ImU32 overline = ImGui::GetColorU32(
|
||||
tabBarFocused ? ImGuiCol_TabSelectedOverline : ImGuiCol_TabDimmedSelectedOverline);
|
||||
|
||||
ImDrawFlags roundFlags = (side == DockDrawerSide::Left)
|
||||
? (ImDrawFlags_RoundCornersTopRight | ImDrawFlags_RoundCornersBottomRight)
|
||||
: (ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersBottomLeft);
|
||||
draw->AddRectFilled(slotRect.Min, slotRect.Max, bg, style.TabRounding, roundFlags);
|
||||
draw->AddRect(slotRect.Min, slotRect.Max, border, style.TabRounding, roundFlags);
|
||||
if (selected) {
|
||||
const float overlineThickness = ImMax(1.0f, style.TabBarOverlineSize);
|
||||
if (side == DockDrawerSide::Left) {
|
||||
draw->AddRectFilled(ImVec2(slotRect.Max.x - overlineThickness, slotRect.Min.y + 1.0f),
|
||||
ImVec2(slotRect.Max.x, slotRect.Max.y - 1.0f),
|
||||
overline);
|
||||
} else {
|
||||
draw->AddRectFilled(ImVec2(slotRect.Min.x, slotRect.Min.y + 1.0f),
|
||||
ImVec2(slotRect.Min.x + overlineThickness, slotRect.Max.y - 1.0f),
|
||||
overline);
|
||||
}
|
||||
}
|
||||
|
||||
const ImU32 textCol = ImGui::GetColorU32((selected || hovered) ? ImGuiCol_Text : ImGuiCol_TextDisabled);
|
||||
const float baseFontSize = ImGui::GetFontSize();
|
||||
const float minRailFont = baseFontSize * 0.75f;
|
||||
const float maxRailFont = baseFontSize * 1.35f;
|
||||
float railFontSize = std::clamp(baseFontSize * 1.15f, minRailFont, maxRailFont);
|
||||
const float minRailFont = baseFontSize * 0.70f;
|
||||
const float maxRailFont = baseFontSize * 1.05f;
|
||||
float railFontSize = std::clamp(baseFontSize * 0.95f, minRailFont, maxRailFont);
|
||||
const ImVec2 unscaledText = ImGui::GetFont()->CalcTextSizeA(railFontSize, FLT_MAX, 0.0f, tabName);
|
||||
if (unscaledText.x > 1.0f) {
|
||||
const float fitScale = (slotRect.GetHeight() - 6.0f) / unscaledText.x;
|
||||
if (unscaledText.x > 0.5f && unscaledText.y > 0.5f) {
|
||||
const float fitToWidth = (slotRect.GetWidth() - 4.0f) / unscaledText.y;
|
||||
const float fitToHeight = (slotRect.GetHeight() - 6.0f) / unscaledText.x;
|
||||
const float fitScale = ImMin(fitToWidth, fitToHeight);
|
||||
railFontSize = std::clamp(railFontSize * fitScale, minRailFont, maxRailFont);
|
||||
}
|
||||
addRotatedText90CW(draw, ImGui::GetFont(), railFontSize, slotRect, textCol, tabName);
|
||||
if (hovered) {
|
||||
ImGui::SetItemTooltip("%s", tabName);
|
||||
}
|
||||
ImGui::PopID();
|
||||
cursorY += slotHeight + slotSpacing;
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
@@ -2289,6 +2454,7 @@ void updateDockDrawerAnimation(DockDrawerState& state,
|
||||
state.collapsed = false;
|
||||
state.openAmount = 1.0f;
|
||||
state.expandedExtent = 0.0f;
|
||||
state.pendingTabFocusId = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2299,9 +2465,11 @@ void updateDockDrawerAnimation(DockDrawerState& state,
|
||||
state.expandedExtent =
|
||||
(side == DockDrawerSide::Bottom) ? ImMax(0.0f, target.drawerBranch->Size.y)
|
||||
: ImMax(0.0f, target.drawerBranch->Size.x);
|
||||
state.pendingTabFocusId = 0;
|
||||
}
|
||||
|
||||
ImGuiTabBar* tabBar = target.drawerBranch->TabBar;
|
||||
applyPendingDrawerTabFocus(state, tabBar);
|
||||
constexpr float kCollapsedSideRailWidth = 25.0f;
|
||||
float collapsedExtent = (side == DockDrawerSide::Bottom)
|
||||
? (ImGui::GetFrameHeight() + 8.0f)
|
||||
@@ -2382,9 +2550,10 @@ void updateDockDrawerAnimation(DockDrawerState& state,
|
||||
}
|
||||
|
||||
if (side != DockDrawerSide::Bottom) {
|
||||
const bool useSideRail = state.openAmount <= 0.02f;
|
||||
const float railReveal = std::clamp(1.0f - state.openAmount, 0.0f, 1.0f);
|
||||
const bool useSideRail = railReveal > 0.01f;
|
||||
ImGuiDockNodeFlags desiredFlags = target.drawerBranch->LocalFlags;
|
||||
if (useSideRail) {
|
||||
if (state.collapsed) {
|
||||
desiredFlags |= ImGuiDockNodeFlags_HiddenTabBar;
|
||||
} else {
|
||||
desiredFlags &= ~ImGuiDockNodeFlags_HiddenTabBar;
|
||||
@@ -2393,7 +2562,7 @@ void updateDockDrawerAnimation(DockDrawerState& state,
|
||||
target.drawerBranch->SetLocalFlags(desiredFlags);
|
||||
}
|
||||
if (useSideRail) {
|
||||
renderCollapsedSideDockRail(state, target, side, collapsedExtent);
|
||||
renderCollapsedSideDockRail(state, target, side, collapsedExtent, railReveal);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2598,6 +2767,7 @@ void Engine::renderMainMenuBar() {
|
||||
if (ImGui::MenuItem("Capsule")) addObject(ObjectType::Capsule, "Capsule");
|
||||
if (ImGui::MenuItem("Plane")) addObject(ObjectType::Plane, "Plane");
|
||||
if (ImGui::MenuItem("Torus")) addObject(ObjectType::Torus, "Torus");
|
||||
if (ImGui::MenuItem("2.5D Object")) addObject(ObjectType::Sprite25D, "2.5D Object");
|
||||
if (ImGui::MenuItem("Mirror")) addObject(ObjectType::Mirror, "Mirror");
|
||||
if (ImGui::MenuItem("Camera")) addObject(ObjectType::Camera, "Camera");
|
||||
if (ImGui::MenuItem("Directional Light")) addObject(ObjectType::DirectionalLight, "Directional Light");
|
||||
@@ -3321,6 +3491,68 @@ void Engine::renderViewport() {
|
||||
blockSelection = true;
|
||||
}
|
||||
|
||||
auto drawProjected25DSceneSprites = [&]() {
|
||||
auto brightenTint = [](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);
|
||||
};
|
||||
for (auto& obj : sceneObjects) {
|
||||
if (!obj.enabled || obj.type != ObjectType::Sprite25D || !obj.hasUI || obj.ui.type != UIElementType::Sprite2D) {
|
||||
continue;
|
||||
}
|
||||
ImVec2 rectMin, rectMax;
|
||||
if (!ResolveProjectedSprite25DRect(obj, view, proj, imageMin, ImVec2(imageMax.x - imageMin.x, imageMax.y - imageMin.y), rectMin, rectMax)) {
|
||||
continue;
|
||||
}
|
||||
ImVec2 drawSize(rectMax.x - rectMin.x, rectMax.y - rectMin.y);
|
||||
if (drawSize.x <= 1.0f || drawSize.y <= 1.0f) continue;
|
||||
|
||||
unsigned int texId = 0;
|
||||
if (rendererInitialized && !obj.albedoTexturePath.empty()) {
|
||||
if (auto* tex = renderer.getTexture(obj.albedoTexturePath)) {
|
||||
texId = tex->GetID();
|
||||
}
|
||||
}
|
||||
|
||||
std::array<ImVec2, 4> uvQuad = buildSpriteSheetUvs(obj);
|
||||
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) {
|
||||
ImVec2 center((rectMin.x + rectMax.x) * 0.5f, (rectMin.y + rectMax.y) * 0.5f);
|
||||
ImVec2 half(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) {
|
||||
viewportDrawList->AddImageQuad((ImTextureID)(intptr_t)texId, p0, p1, p2, p3,
|
||||
uvQuad[0], uvQuad[1], uvQuad[2], uvQuad[3],
|
||||
ImGui::GetColorU32(tint));
|
||||
} else {
|
||||
ImU32 fill = ImGui::GetColorU32(tint);
|
||||
ImU32 border = ImGui::GetColorU32(brightenTint(tint, 0.85f));
|
||||
viewportDrawList->AddQuadFilled(p0, p1, p2, p3, fill);
|
||||
viewportDrawList->AddQuad(p0, p1, p2, p3, border, 1.5f);
|
||||
}
|
||||
} else if (texId != 0) {
|
||||
viewportDrawList->AddImage((ImTextureID)(intptr_t)texId, rectMin, rectMax, uvQuad[0], uvQuad[2], ImGui::GetColorU32(tint));
|
||||
} else {
|
||||
ImU32 fill = ImGui::GetColorU32(tint);
|
||||
ImU32 border = ImGui::GetColorU32(brightenTint(tint, 0.85f));
|
||||
viewportDrawList->AddRectFilled(rectMin, rectMax, fill, 4.0f);
|
||||
viewportDrawList->AddRect(rectMin, rectMax, border, 4.0f, 0, 1.5f);
|
||||
}
|
||||
}
|
||||
};
|
||||
drawProjected25DSceneSprites();
|
||||
|
||||
bool uiWorldCameraActive = false;
|
||||
if (worldUiEditing) {
|
||||
auto find3DCanvasId = [&](const SceneObject& target) -> int {
|
||||
@@ -3345,6 +3577,8 @@ void Engine::renderViewport() {
|
||||
editCanvas3DId = find3DCanvasId(*selected);
|
||||
}
|
||||
auto isUIType = [&](const SceneObject& target) {
|
||||
if (target.type == ObjectType::Sprite25D) return true;
|
||||
if (!worldUiEditing) return false;
|
||||
if (!target.hasUI || target.ui.type == UIElementType::None) return false;
|
||||
int canvasId = find3DCanvasId(target);
|
||||
return (canvasId < 0) || (canvasId == editCanvas3DId);
|
||||
@@ -3403,6 +3637,9 @@ void Engine::renderViewport() {
|
||||
return uiWorldCamera.position * (1.0f - factor);
|
||||
};
|
||||
auto resolveUIRectWorld = [&](const SceneObject& obj, ImVec2& outMin, ImVec2& outMax) {
|
||||
if (obj.type == ObjectType::Sprite25D) {
|
||||
return ResolveProjectedSprite25DRect(obj, view, proj, overlayPos, overlaySize, outMin, 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);
|
||||
@@ -3420,6 +3657,7 @@ void Engine::renderViewport() {
|
||||
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));
|
||||
return true;
|
||||
};
|
||||
auto rectOutsideOverlay = [&](const ImVec2& min, const ImVec2& max) {
|
||||
return (max.x < overlayPos.x || min.x > overlayPos.x + overlaySize.x ||
|
||||
@@ -3565,7 +3803,7 @@ void Engine::renderViewport() {
|
||||
for (SceneObject* objPtr : uiDrawList) {
|
||||
SceneObject& obj = *objPtr;
|
||||
ImVec2 rectMin, rectMax;
|
||||
resolveUIRectWorld(obj, rectMin, rectMax);
|
||||
if (!resolveUIRectWorld(obj, rectMin, rectMax)) continue;
|
||||
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;
|
||||
@@ -3871,7 +4109,7 @@ void Engine::renderViewport() {
|
||||
}
|
||||
|
||||
bool gizmoUsed = false;
|
||||
if (uiWorldHover && !uiWorldCameraActive && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
||||
if (worldUiEditing && uiWorldHover && !uiWorldCameraActive && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
||||
!ImGuizmo::IsUsing() && !ImGuizmo::IsOver()) {
|
||||
ImVec2 mouse = ImGui::GetIO().MousePos;
|
||||
bool additive = ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift;
|
||||
@@ -3880,7 +4118,7 @@ void Engine::renderViewport() {
|
||||
const SceneObject& obj = *it;
|
||||
if (!obj.enabled || !isUIType(obj) || obj.ui.type == UIElementType::Canvas) continue;
|
||||
ImVec2 rectMin, rectMax;
|
||||
resolveUIRectWorld(obj, rectMin, rectMax);
|
||||
if (!resolveUIRectWorld(obj, rectMin, rectMax)) continue;
|
||||
if (mouse.x >= rectMin.x && mouse.x <= rectMax.x &&
|
||||
mouse.y >= rectMin.y && mouse.y <= rectMax.y) {
|
||||
hitId = obj.id;
|
||||
@@ -3896,9 +4134,9 @@ void Engine::renderViewport() {
|
||||
}
|
||||
|
||||
SceneObject* selected = getSelectedObject();
|
||||
if (selected && isUIType(*selected) && selected->ui.type != UIElementType::Canvas) {
|
||||
if (worldUiEditing && selected && isUIType(*selected) && selected->ui.type != UIElementType::Canvas) {
|
||||
ImVec2 rectMin, rectMax;
|
||||
resolveUIRectWorld(*selected, rectMin, rectMax);
|
||||
if (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;
|
||||
@@ -3979,12 +4217,13 @@ void Engine::renderViewport() {
|
||||
gizmoUsed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
if (ImGui::IsAnyItemActive() || uiWorldCameraActive || gizmoUsed) {
|
||||
if ((worldUiEditing && ImGui::IsAnyItemActive()) || uiWorldCameraActive || gizmoUsed) {
|
||||
blockSelection = true;
|
||||
}
|
||||
}
|
||||
@@ -4963,6 +5202,7 @@ void Engine::renderViewport() {
|
||||
break;
|
||||
case ObjectType::Mirror:
|
||||
case ObjectType::Sprite:
|
||||
case ObjectType::Sprite25D:
|
||||
gizmoBoundsMin = glm::vec3(-0.5f, -0.5f, -0.02f);
|
||||
gizmoBoundsMax = glm::vec3(0.5f, 0.5f, 0.02f);
|
||||
break;
|
||||
@@ -5468,7 +5708,7 @@ void Engine::renderViewport() {
|
||||
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::Sprite2D: return obj.type == ObjectType::Sprite25D ? ObjectType::Sprite25D : ObjectType::Sprite2D;
|
||||
case UIElementType::None: break;
|
||||
}
|
||||
}
|
||||
@@ -6104,6 +6344,9 @@ void Engine::renderViewport() {
|
||||
auto ray = makeRay(mousePos);
|
||||
float closest = FLT_MAX;
|
||||
int hitId = -1;
|
||||
glm::mat4 invView = glm::inverse(view);
|
||||
glm::vec3 cameraPos = glm::vec3(invView[3]);
|
||||
glm::vec3 cameraUp = glm::normalize(glm::vec3(invView[1]));
|
||||
|
||||
for (const auto& obj : sceneObjects) {
|
||||
glm::vec3 aabbMin(-0.5f);
|
||||
@@ -6111,10 +6354,26 @@ void Engine::renderViewport() {
|
||||
|
||||
glm::mat4 model(1.0f);
|
||||
model = glm::translate(model, obj.position);
|
||||
model = glm::rotate(model, glm::radians(obj.rotation.x), glm::vec3(1, 0, 0));
|
||||
model = glm::rotate(model, glm::radians(obj.rotation.y), glm::vec3(0, 1, 0));
|
||||
model = glm::rotate(model, glm::radians(obj.rotation.z), glm::vec3(0, 0, 1));
|
||||
model = glm::scale(model, obj.scale);
|
||||
if (obj.type == ObjectType::Sprite25D || (obj.renderType == RenderType::Sprite && obj.faceCamera)) {
|
||||
glm::vec3 forward = cameraPos - obj.position;
|
||||
if (glm::dot(forward, forward) < 1e-6f) forward = glm::vec3(0.0f, 0.0f, 1.0f);
|
||||
else forward = glm::normalize(forward);
|
||||
glm::vec3 right = glm::cross(cameraUp, forward);
|
||||
if (glm::dot(right, right) < 1e-6f) {
|
||||
right = glm::cross(glm::vec3(0.0f, 0.0f, 1.0f), forward);
|
||||
}
|
||||
right = glm::normalize(right);
|
||||
glm::vec3 up = glm::normalize(glm::cross(forward, right));
|
||||
glm::vec3 scale = glm::max(glm::abs(obj.scale), glm::vec3(0.0001f));
|
||||
model[0] = glm::vec4(right * scale.x * (obj.scale.x < 0.0f ? -1.0f : 1.0f), 0.0f);
|
||||
model[1] = glm::vec4(up * scale.y * (obj.scale.y < 0.0f ? -1.0f : 1.0f), 0.0f);
|
||||
model[2] = glm::vec4(forward * scale.z * (obj.scale.z < 0.0f ? -1.0f : 1.0f), 0.0f);
|
||||
} else {
|
||||
model = glm::rotate(model, glm::radians(obj.rotation.x), glm::vec3(1, 0, 0));
|
||||
model = glm::rotate(model, glm::radians(obj.rotation.y), glm::vec3(0, 1, 0));
|
||||
model = glm::rotate(model, glm::radians(obj.rotation.z), glm::vec3(0, 0, 1));
|
||||
model = glm::scale(model, obj.scale);
|
||||
}
|
||||
|
||||
glm::mat4 invModel = glm::inverse(model);
|
||||
glm::vec3 localOrigin = glm::vec3(invModel * glm::vec4(ray.first, 1.0f));
|
||||
@@ -6139,6 +6398,7 @@ void Engine::renderViewport() {
|
||||
hit = rayAabb(localOrigin, localDir, glm::vec3(-0.5f, -0.5f, -0.02f), glm::vec3(0.5f, 0.5f, 0.02f), hitT);
|
||||
break;
|
||||
case ObjectType::Sprite:
|
||||
case ObjectType::Sprite25D:
|
||||
hit = rayAabb(localOrigin, localDir, glm::vec3(-0.5f, -0.5f, -0.02f), glm::vec3(0.5f, 0.5f, 0.02f), hitT);
|
||||
break;
|
||||
case ObjectType::Torus:
|
||||
@@ -7190,6 +7450,12 @@ void Engine::renderPlayerViewport() {
|
||||
if (useWorldUi) {
|
||||
uiWorldCamera.viewportSize = glm::vec2(overlaySize.x, overlaySize.y);
|
||||
}
|
||||
glm::mat4 projectedUiView = camera.getViewMatrix();
|
||||
glm::mat4 projectedUiProj = glm::perspective(glm::radians(runtimeFov),
|
||||
std::max(0.1f, overlaySize.x / std::max(1.0f, overlaySize.y)),
|
||||
runtimeNear,
|
||||
runtimeFar);
|
||||
bool hasProjectedUiCamera = true;
|
||||
auto worldToScreen = [&](const glm::vec2& world) {
|
||||
glm::vec2 local = uiWorldCamera.WorldToScreen(world);
|
||||
return ImVec2(overlayPos.x + local.x, overlayPos.y + local.y);
|
||||
@@ -7213,6 +7479,9 @@ void Engine::renderPlayerViewport() {
|
||||
return offset;
|
||||
};
|
||||
auto resolveUIRectWorld = [&](const SceneObject& obj, ImVec2& outMin, ImVec2& outMax) {
|
||||
if (obj.type == ObjectType::Sprite25D && hasProjectedUiCamera) {
|
||||
return ResolveProjectedSprite25DRect(obj, projectedUiView, projectedUiProj, overlayPos, overlaySize, outMin, 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);
|
||||
@@ -7223,6 +7492,7 @@ void Engine::renderPlayerViewport() {
|
||||
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));
|
||||
return true;
|
||||
};
|
||||
auto rectOutsideOverlay = [&](const ImVec2& min, const ImVec2& max) {
|
||||
return (max.x < overlayPos.x || min.x > overlayPos.x + overlaySize.x ||
|
||||
@@ -7350,8 +7620,8 @@ void Engine::renderPlayerViewport() {
|
||||
for (auto& obj : sceneObjects) {
|
||||
if (!obj.enabled || !isUIType(obj)) continue;
|
||||
ImVec2 rectMin, rectMax;
|
||||
if (useWorldUi) {
|
||||
resolveUIRectWorld(obj, rectMin, rectMax);
|
||||
if (useWorldUi || obj.type == ObjectType::Sprite25D) {
|
||||
if (!resolveUIRectWorld(obj, rectMin, rectMax)) continue;
|
||||
} else {
|
||||
resolveUIRect(obj, rectMin, rectMax);
|
||||
}
|
||||
|
||||
160
src/Engine.cpp
160
src/Engine.cpp
@@ -256,6 +256,13 @@ void ApplyObjectPreset(SceneObject& obj, ObjectType preset) {
|
||||
obj.scale = glm::vec3(1.0f, 1.0f, 0.05f);
|
||||
obj.material.ambientStrength = 1.0f;
|
||||
break;
|
||||
case ObjectType::Sprite25D:
|
||||
obj.hasUI = true;
|
||||
obj.ui.type = UIElementType::Sprite2D;
|
||||
obj.ui.label = "2.5D Object";
|
||||
obj.ui.size = glm::vec2(128.0f, 128.0f);
|
||||
obj.scale = glm::vec3(1.0f);
|
||||
break;
|
||||
case ObjectType::DirectionalLight:
|
||||
obj.hasLight = true;
|
||||
obj.light.type = LightType::Directional;
|
||||
@@ -367,7 +374,7 @@ void Engine::applyProjectPipelineDefaults(bool force) {
|
||||
}
|
||||
|
||||
int Engine::resolveSpriteSheetFrame(const SceneObject& obj) const {
|
||||
if (!obj.hasUI || !obj.ui.spriteSheetEnabled) {
|
||||
if (!obj.ui.spriteSheetEnabled) {
|
||||
return 0;
|
||||
}
|
||||
int columns = std::max(1, obj.ui.spriteSheetColumns);
|
||||
@@ -384,7 +391,7 @@ std::array<ImVec2, 4> Engine::buildSpriteSheetUvs(const SceneObject& obj) const
|
||||
ImVec2(1.0f, 0.0f),
|
||||
ImVec2(0.0f, 0.0f)
|
||||
};
|
||||
if (!obj.hasUI || !obj.ui.spriteSheetEnabled) {
|
||||
if (!obj.ui.spriteSheetEnabled) {
|
||||
return uvs;
|
||||
}
|
||||
|
||||
@@ -680,6 +687,107 @@ bool copyDirectoryRecursive(const fs::path& from, const fs::path& to, std::strin
|
||||
return true;
|
||||
}
|
||||
|
||||
fs::path resolveRecentProjectRoot(const std::string& recentPath) {
|
||||
fs::path path(recentPath);
|
||||
if (path.empty()) return {};
|
||||
if (path.extension() == ".modu") {
|
||||
return path.parent_path();
|
||||
}
|
||||
if (fs::is_directory(path)) {
|
||||
return path;
|
||||
}
|
||||
return path.has_parent_path() ? path.parent_path() : path;
|
||||
}
|
||||
|
||||
bool importInstalledPackagesFromRecent(const ProjectManager& projectManager,
|
||||
const fs::path& targetProjectRoot,
|
||||
std::string& outSourceProjectName,
|
||||
std::string& outError) {
|
||||
std::error_code ec;
|
||||
fs::path targetCanonical = fs::weakly_canonical(targetProjectRoot, ec);
|
||||
if (ec || targetCanonical.empty()) {
|
||||
ec.clear();
|
||||
targetCanonical = fs::absolute(targetProjectRoot, ec);
|
||||
ec.clear();
|
||||
}
|
||||
|
||||
for (const auto& rp : projectManager.recentProjects) {
|
||||
fs::path sourceRoot = resolveRecentProjectRoot(rp.path);
|
||||
if (sourceRoot.empty()) continue;
|
||||
|
||||
fs::path sourceCanonical = fs::weakly_canonical(sourceRoot, ec);
|
||||
if (ec || sourceCanonical.empty()) {
|
||||
ec.clear();
|
||||
sourceCanonical = fs::absolute(sourceRoot, ec);
|
||||
ec.clear();
|
||||
}
|
||||
if (!sourceCanonical.empty() && !targetCanonical.empty() && sourceCanonical == targetCanonical) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fs::path sourceManifest = sourceRoot / "packages.modu";
|
||||
if (!fs::exists(sourceManifest)) continue;
|
||||
|
||||
fs::path targetManifest = targetProjectRoot / "packages.modu";
|
||||
fs::copy_file(sourceManifest, targetManifest, fs::copy_options::overwrite_existing, ec);
|
||||
if (ec) {
|
||||
outError = "Failed to copy package manifest from " + sourceRoot.string() + ": " + ec.message();
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string copyError;
|
||||
if (!copyDirectoryRecursive(sourceRoot / "Library" / "InstalledPackages",
|
||||
targetProjectRoot / "Library" / "InstalledPackages",
|
||||
copyError)) {
|
||||
outError = copyError;
|
||||
return false;
|
||||
}
|
||||
|
||||
outSourceProjectName = rp.name.empty() ? sourceRoot.filename().string() : rp.name;
|
||||
return true;
|
||||
}
|
||||
|
||||
outError = "No recent project with a package manifest was found.";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool applyTemplateProject(const fs::path& templateProjectRoot,
|
||||
Project& project,
|
||||
const std::string& projectName,
|
||||
ProjectPipeline pipeline,
|
||||
Modularity::GraphicsBackend rendererBackend,
|
||||
std::string& outError) {
|
||||
if (templateProjectRoot.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
fs::path templateProjectFile = templateProjectRoot / "project.modu";
|
||||
if (!fs::exists(templateProjectFile)) {
|
||||
outError = "Template is missing project.modu: " + templateProjectRoot.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string copyError;
|
||||
if (!copyDirectoryRecursive(templateProjectRoot, project.projectPath, copyError)) {
|
||||
outError = "Failed to copy template project: " + copyError;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!project.load(project.projectPath / "project.modu")) {
|
||||
outError = "Failed to load copied template project.";
|
||||
return false;
|
||||
}
|
||||
|
||||
project.name = projectName;
|
||||
project.pipeline = pipeline;
|
||||
project.rendererBackend = rendererBackend;
|
||||
if (project.currentSceneName.empty()) {
|
||||
project.currentSceneName = "Main";
|
||||
}
|
||||
project.saveProjectFile();
|
||||
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;
|
||||
@@ -930,6 +1038,15 @@ void cleanExportOutput(const fs::path& exportRoot, const char* exeBaseName, std:
|
||||
}
|
||||
}
|
||||
|
||||
fs::path templatesDir = exportRoot / "Template-Projects";
|
||||
if (fs::exists(templatesDir)) {
|
||||
fs::remove_all(templatesDir, ec);
|
||||
if (ec) {
|
||||
error = "Failed to remove existing template projects.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fs::path autostart = exportRoot / "autostart.modu";
|
||||
if (fs::exists(autostart)) {
|
||||
fs::remove(autostart, ec);
|
||||
@@ -1709,6 +1826,7 @@ void Engine::run() {
|
||||
std::cerr << "[DEBUG] First frame: UI rendering complete, finalizing frame..." << std::endl;
|
||||
}
|
||||
|
||||
updateTouchSwipeScrolling();
|
||||
autosaveWorkspaceLayout();
|
||||
renderUiCanvas3DTargets();
|
||||
ImGui::Render();
|
||||
@@ -4561,6 +4679,10 @@ void Engine::startExportBuild(const fs::path& outputDir, bool runAfter) {
|
||||
result.message = copyError;
|
||||
return result;
|
||||
}
|
||||
if (!copyDirectoryRecursive(sourceRoot / "Template-Projects", exportRoot / "Template-Projects", copyError)) {
|
||||
result.message = copyError;
|
||||
return result;
|
||||
}
|
||||
|
||||
setStatus(0.78f, "Collecting precompiled packages...");
|
||||
if (!copyPrecompiledPackages(buildRoot, exportRoot / "Packages" / "ThirdParty", copyError)) {
|
||||
@@ -4985,6 +5107,34 @@ void Engine::createNewProject(const char* name, const char* location) {
|
||||
}
|
||||
#endif
|
||||
if (newProject.create()) {
|
||||
if (!projectManager.newProjectTemplatePath.empty()) {
|
||||
std::string templateError;
|
||||
if (!applyTemplateProject(fs::path(projectManager.newProjectTemplatePath),
|
||||
newProject,
|
||||
name,
|
||||
newProject.pipeline,
|
||||
newProject.rendererBackend,
|
||||
templateError)) {
|
||||
projectManager.errorMessage = templateError;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool importedPackages = false;
|
||||
std::string importedFromProject;
|
||||
if (projectManager.newProjectImportLastPackages) {
|
||||
std::string importError;
|
||||
if (importInstalledPackagesFromRecent(projectManager,
|
||||
newProject.projectPath,
|
||||
importedFromProject,
|
||||
importError)) {
|
||||
importedPackages = true;
|
||||
} else if (!importError.empty()) {
|
||||
addConsoleMessage("Installed package import skipped: " + importError,
|
||||
ConsoleMessageType::Warning);
|
||||
}
|
||||
}
|
||||
|
||||
projectManager.currentProject = newProject;
|
||||
projectManager.addToRecentProjects(name,
|
||||
(newProject.projectPath / "project.modu").string());
|
||||
@@ -5033,6 +5183,9 @@ void Engine::createNewProject(const char* name, const char* location) {
|
||||
addConsoleMessage("Created new project: " + std::string(name), ConsoleMessageType::Success);
|
||||
addConsoleMessage("Project location: " + newProject.projectPath.string(), ConsoleMessageType::Info);
|
||||
addConsoleMessage("Pipeline: " + std::string(isProject2DPipeline() ? "2D" : "3D"), ConsoleMessageType::Info);
|
||||
if (importedPackages) {
|
||||
addConsoleMessage("Imported installed packages from: " + importedFromProject, ConsoleMessageType::Info);
|
||||
}
|
||||
|
||||
saveCurrentScene();
|
||||
loadBuildSettings();
|
||||
@@ -6175,6 +6328,9 @@ void Engine::setupImGui() {
|
||||
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||
#if defined(__ANDROID__)
|
||||
io.ConfigFlags |= ImGuiConfigFlags_IsTouchScreen;
|
||||
#endif
|
||||
if (usingVulkan()) {
|
||||
io.ConfigFlags &= ~ImGuiConfigFlags_ViewportsEnable;
|
||||
} else {
|
||||
|
||||
@@ -3,10 +3,36 @@
|
||||
#include "ModelLoader.h"
|
||||
#include <cmath>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
|
||||
ObjectType GetLegacyTypeFromComponents(const SceneObject& obj);
|
||||
|
||||
namespace {
|
||||
std::string TrimCopy(const std::string& value) {
|
||||
const size_t first = value.find_first_not_of(" \t\r\n");
|
||||
if (first == std::string::npos) return "";
|
||||
const size_t last = value.find_last_not_of(" \t\r\n");
|
||||
return value.substr(first, last - first + 1);
|
||||
}
|
||||
|
||||
std::string GetPlatformDefaultProjectsPath() {
|
||||
#ifdef _WIN32
|
||||
const char* userProfile = std::getenv("USERPROFILE");
|
||||
if (userProfile && *userProfile) {
|
||||
return (fs::path(userProfile) / "Documents" / "ModularityProjects").string();
|
||||
}
|
||||
#else
|
||||
const char* home = std::getenv("HOME");
|
||||
if (home && *home) {
|
||||
return (fs::path(home) / "ModularityProjects").string();
|
||||
}
|
||||
#endif
|
||||
return (fs::current_path() / "Projects").string();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Project implementation
|
||||
Project::Project(const std::string& projectName, const fs::path& basePath)
|
||||
: name(projectName) {
|
||||
@@ -192,9 +218,9 @@ ProjectManager::ProjectManager() {
|
||||
|
||||
fs::create_directories(appDataPath);
|
||||
loadRecentProjects();
|
||||
loadLauncherSettings();
|
||||
|
||||
std::string defaultPath = (fs::current_path() / "Projects").string();
|
||||
strncpy(newProjectLocation, defaultPath.c_str(), sizeof(newProjectLocation) - 1);
|
||||
std::snprintf(newProjectLocation, sizeof(newProjectLocation), "%s", defaultProjectLocation);
|
||||
}
|
||||
|
||||
void ProjectManager::loadRecentProjects() {
|
||||
@@ -269,6 +295,45 @@ void ProjectManager::saveRecentProjects() {
|
||||
file.close();
|
||||
}
|
||||
|
||||
void ProjectManager::loadLauncherSettings() {
|
||||
defaultProjectLocation[0] = '\0';
|
||||
fs::path settingsFile = appDataPath / "launcher_settings.modu";
|
||||
|
||||
if (fs::exists(settingsFile)) {
|
||||
std::ifstream file(settingsFile);
|
||||
std::string line;
|
||||
while (std::getline(file, line)) {
|
||||
const std::string cleaned = TrimCopy(line);
|
||||
if (cleaned.empty() || cleaned[0] == '#') continue;
|
||||
|
||||
const size_t eq = cleaned.find('=');
|
||||
if (eq == std::string::npos) continue;
|
||||
|
||||
const std::string key = TrimCopy(cleaned.substr(0, eq));
|
||||
const std::string value = TrimCopy(cleaned.substr(eq + 1));
|
||||
if (key == "defaultProjectLocation" && !value.empty()) {
|
||||
std::snprintf(defaultProjectLocation, sizeof(defaultProjectLocation), "%s", value.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultProjectLocation[0] == '\0') {
|
||||
const std::string fallback = GetPlatformDefaultProjectsPath();
|
||||
std::snprintf(defaultProjectLocation, sizeof(defaultProjectLocation), "%s", fallback.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectManager::saveLauncherSettings() const {
|
||||
fs::path settingsFile = appDataPath / "launcher_settings.modu";
|
||||
std::ofstream file(settingsFile);
|
||||
if (!file.is_open()) {
|
||||
return;
|
||||
}
|
||||
|
||||
file << "# Modularity launcher settings\n";
|
||||
file << "defaultProjectLocation=" << defaultProjectLocation << "\n";
|
||||
}
|
||||
|
||||
void ProjectManager::addToRecentProjects(const std::string& name, const std::string& path) {
|
||||
std::string absolutePath = path;
|
||||
try {
|
||||
@@ -339,6 +404,7 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
|
||||
file << "tag=" << obj.tag << "\n";
|
||||
file << "hasRenderer=" << (obj.hasRenderer ? 1 : 0) << "\n";
|
||||
file << "renderType=" << static_cast<int>(obj.renderType) << "\n";
|
||||
file << "faceCamera=" << (obj.faceCamera ? 1 : 0) << "\n";
|
||||
file << "hasLight=" << (obj.hasLight ? 1 : 0) << "\n";
|
||||
file << "hasCamera=" << (obj.hasCamera ? 1 : 0) << "\n";
|
||||
file << "hasPostFX=" << (obj.hasPostFX ? 1 : 0) << "\n";
|
||||
@@ -784,6 +850,10 @@ void ApplyLegacyTypePreset(SceneObject& obj, ObjectType legacyType) {
|
||||
obj.hasRenderer = true;
|
||||
obj.renderType = RenderType::Sprite;
|
||||
break;
|
||||
case ObjectType::Sprite25D:
|
||||
obj.hasUI = true;
|
||||
obj.ui.type = UIElementType::Sprite2D;
|
||||
break;
|
||||
case ObjectType::DirectionalLight:
|
||||
obj.hasLight = true;
|
||||
obj.light.type = LightType::Directional;
|
||||
@@ -856,6 +926,7 @@ const std::unordered_map<std::string, KeyHandler>& GetSceneObjectKeyHandlers() {
|
||||
obj.hasRenderer = true;
|
||||
}
|
||||
}},
|
||||
{"faceCamera", +[](SceneObject& obj, const std::string& value) { obj.faceCamera = std::stoi(value) != 0; }},
|
||||
{"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; }},
|
||||
@@ -1188,6 +1259,9 @@ const std::unordered_map<std::string, KeyHandler>& GetSceneObjectKeyHandlers() {
|
||||
} // namespace
|
||||
|
||||
ObjectType GetLegacyTypeFromComponents(const SceneObject& obj) {
|
||||
if (obj.type == ObjectType::Sprite25D) {
|
||||
return ObjectType::Sprite25D;
|
||||
}
|
||||
if (obj.hasRenderer) {
|
||||
switch (obj.renderType) {
|
||||
case RenderType::Cube: return ObjectType::Cube;
|
||||
@@ -1209,7 +1283,7 @@ ObjectType GetLegacyTypeFromComponents(const SceneObject& obj) {
|
||||
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::Sprite2D: return obj.type == ObjectType::Sprite25D ? ObjectType::Sprite25D : ObjectType::Sprite2D;
|
||||
case UIElementType::None: break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,11 +45,15 @@ public:
|
||||
fs::path appDataPath;
|
||||
char newProjectName[128] = "";
|
||||
char newProjectLocation[512] = "";
|
||||
char defaultProjectLocation[512] = "";
|
||||
char openProjectPath[512] = "";
|
||||
bool showNewProjectDialog = false;
|
||||
bool showOpenProjectDialog = false;
|
||||
int newProjectPipelineMode = 0;
|
||||
int newProjectRendererMode = 0;
|
||||
bool newProjectImportLastPackages = true;
|
||||
std::string newProjectTemplatePath;
|
||||
std::string newProjectTemplateName;
|
||||
std::string errorMessage;
|
||||
Project currentProject;
|
||||
|
||||
@@ -57,6 +61,8 @@ public:
|
||||
|
||||
void loadRecentProjects();
|
||||
void saveRecentProjects();
|
||||
void loadLauncherSettings();
|
||||
void saveLauncherSettings() const;
|
||||
void addToRecentProjects(const std::string& name, const std::string& path);
|
||||
bool loadProject(const std::string& path);
|
||||
};
|
||||
|
||||
@@ -8,6 +8,65 @@
|
||||
#define TINYOBJLOADER_IMPLEMENTATION
|
||||
#include "../include/ThirdParty/tiny_obj_loader.h"
|
||||
|
||||
namespace {
|
||||
glm::vec4 BuildSpriteUvRect(const SceneObject& obj) {
|
||||
if (!obj.ui.spriteSheetEnabled) {
|
||||
return glm::vec4(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
}
|
||||
|
||||
const int columns = std::max(1, obj.ui.spriteSheetColumns);
|
||||
const int rows = std::max(1, obj.ui.spriteSheetRows);
|
||||
const int total = std::max(1, columns * rows);
|
||||
const int frame = std::clamp(obj.ui.spriteSheetFrame, 0, total - 1);
|
||||
const int col = frame % columns;
|
||||
const int row = frame / columns;
|
||||
|
||||
const float u0 = static_cast<float>(col) / static_cast<float>(columns);
|
||||
const float v0 = static_cast<float>(row) / static_cast<float>(rows);
|
||||
const float uSize = 1.0f / static_cast<float>(columns);
|
||||
const float vSize = 1.0f / static_cast<float>(rows);
|
||||
return glm::vec4(u0, v0, uSize, vSize);
|
||||
}
|
||||
|
||||
glm::mat4 BuildSceneObjectModelMatrix(const SceneObject& obj, const glm::vec3* cameraPosition = nullptr, const glm::vec3* cameraUp = nullptr) {
|
||||
glm::mat4 model = glm::mat4(1.0f);
|
||||
model = glm::translate(model, obj.position);
|
||||
|
||||
if (obj.renderType == RenderType::Sprite && obj.faceCamera && cameraPosition != nullptr) {
|
||||
glm::vec3 forward = *cameraPosition - obj.position;
|
||||
if (glm::dot(forward, forward) < 1e-6f) {
|
||||
forward = glm::vec3(0.0f, 0.0f, 1.0f);
|
||||
} else {
|
||||
forward = glm::normalize(forward);
|
||||
}
|
||||
|
||||
glm::vec3 up = (cameraUp != nullptr && glm::dot(*cameraUp, *cameraUp) > 1e-6f)
|
||||
? glm::normalize(*cameraUp)
|
||||
: glm::vec3(0.0f, 1.0f, 0.0f);
|
||||
glm::vec3 right = glm::cross(up, forward);
|
||||
if (glm::dot(right, right) < 1e-6f) {
|
||||
up = glm::vec3(0.0f, 0.0f, 1.0f);
|
||||
right = glm::cross(up, forward);
|
||||
}
|
||||
right = glm::normalize(right);
|
||||
up = glm::normalize(glm::cross(forward, right));
|
||||
|
||||
const glm::vec3 signedScale = obj.scale;
|
||||
const glm::vec3 absScale = glm::max(glm::abs(signedScale), glm::vec3(0.0001f));
|
||||
model[0] = glm::vec4(right * absScale.x * (signedScale.x < 0.0f ? -1.0f : 1.0f), 0.0f);
|
||||
model[1] = glm::vec4(up * absScale.y * (signedScale.y < 0.0f ? -1.0f : 1.0f), 0.0f);
|
||||
model[2] = glm::vec4(forward * absScale.z * (signedScale.z < 0.0f ? -1.0f : 1.0f), 0.0f);
|
||||
return model;
|
||||
}
|
||||
|
||||
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);
|
||||
return model;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Global OBJ loader instance
|
||||
OBJLoader g_objLoader;
|
||||
|
||||
@@ -1189,12 +1248,7 @@ void Renderer::renderSkybox(const glm::mat4& view, const glm::mat4& proj) {
|
||||
}
|
||||
|
||||
void Renderer::renderObject(const SceneObject& obj) {
|
||||
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, 0, 0));
|
||||
model = glm::rotate(model, glm::radians(obj.rotation.y), glm::vec3(0, 1, 0));
|
||||
model = glm::rotate(model, glm::radians(obj.rotation.z), glm::vec3(0, 0, 1));
|
||||
model = glm::scale(model, obj.scale);
|
||||
glm::mat4 model = BuildSceneObjectModelMatrix(obj);
|
||||
|
||||
bool hasMaterialAsset = !obj.materialPath.empty();
|
||||
bool hasCustomShader = !obj.vertexShaderPath.empty() || !obj.fragmentShaderPath.empty();
|
||||
@@ -1207,6 +1261,7 @@ void Renderer::renderObject(const SceneObject& obj) {
|
||||
shader->setFloat("specularStrength", obj.material.specularStrength);
|
||||
shader->setFloat("shininess", obj.material.shininess);
|
||||
shader->setFloat("mixAmount", obj.material.textureMix);
|
||||
shader->setVec4("uvRect", BuildSpriteUvRect(obj));
|
||||
shader->setBool("unlit", obj.renderType == RenderType::Mirror || obj.renderType == RenderType::Sprite || missingMaterialAndShader);
|
||||
|
||||
Texture* baseTex = texture1;
|
||||
@@ -1327,14 +1382,8 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
}
|
||||
return f;
|
||||
};
|
||||
auto buildModelMatrix = [](const SceneObject& obj) {
|
||||
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);
|
||||
return model;
|
||||
auto buildModelMatrix = [&](const SceneObject& obj) {
|
||||
return BuildSceneObjectModelMatrix(obj, &camera.position, &camera.up);
|
||||
};
|
||||
auto selectMeshForObject = [&](const SceneObject& obj) -> Mesh* {
|
||||
if (obj.renderType == RenderType::Cube) return cubeMesh;
|
||||
@@ -1683,6 +1732,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
shader->setFloat("specularStrength", obj.material.specularStrength);
|
||||
shader->setFloat("shininess", obj.material.shininess);
|
||||
shader->setFloat("mixAmount", obj.material.textureMix);
|
||||
shader->setVec4("uvRect", BuildSpriteUvRect(obj));
|
||||
|
||||
if (obj.hasSkeletalAnimation && obj.skeletal.enabled) {
|
||||
int safeLimit = std::max(0, boneLimit);
|
||||
@@ -2112,12 +2162,7 @@ void Renderer::renderCollisionOverlay(const Camera& camera, const std::vector<Sc
|
||||
|
||||
if (!meshToDraw) continue;
|
||||
|
||||
glm::mat4 model = glm::mat4(1.0f);
|
||||
model = glm::translate(model, position);
|
||||
model = glm::rotate(model, glm::radians(rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
|
||||
model = glm::rotate(model, glm::radians(rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
|
||||
model = glm::rotate(model, glm::radians(rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
model = glm::scale(model, scale);
|
||||
glm::mat4 model = BuildSceneObjectModelMatrix(obj, &camera.position, &camera.up);
|
||||
active->setMat4("model", model);
|
||||
|
||||
meshToDraw->draw();
|
||||
@@ -2355,12 +2400,7 @@ void Renderer::renderSelectionOutline(const Camera& camera, const std::vector<Sc
|
||||
glCullFace(GL_BACK);
|
||||
}
|
||||
|
||||
glm::mat4 model = glm::mat4(1.0f);
|
||||
model = glm::translate(model, drawItem.obj->position);
|
||||
model = glm::rotate(model, glm::radians(drawItem.obj->rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
|
||||
model = glm::rotate(model, glm::radians(drawItem.obj->rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
|
||||
model = glm::rotate(model, glm::radians(drawItem.obj->rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
model = glm::scale(model, drawItem.obj->scale);
|
||||
glm::mat4 model = BuildSceneObjectModelMatrix(*drawItem.obj, &camera.position, &camera.up);
|
||||
|
||||
maskShader->use();
|
||||
maskShader->setMat4("view", view);
|
||||
|
||||
@@ -24,7 +24,8 @@ enum class ObjectType {
|
||||
UISlider = 18,
|
||||
UIButton = 19,
|
||||
UIText = 20,
|
||||
Empty = 21
|
||||
Empty = 21,
|
||||
Sprite25D = 22
|
||||
};
|
||||
|
||||
enum class RenderType {
|
||||
@@ -460,6 +461,7 @@ public:
|
||||
std::string tag = "Untagged";
|
||||
bool hasRenderer = false;
|
||||
RenderType renderType = RenderType::None;
|
||||
bool faceCamera = false;
|
||||
bool hasLight = false;
|
||||
bool hasCamera = false;
|
||||
bool hasPostFX = false;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
#include "ScriptCompiler.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <regex>
|
||||
#if defined(_WIN32)
|
||||
@@ -32,6 +35,58 @@ namespace {
|
||||
}
|
||||
return value.substr(start, end - start);
|
||||
}
|
||||
|
||||
bool writeTextFileIfChanged(const fs::path& path, const std::string& text,
|
||||
std::string& error) {
|
||||
std::error_code ec;
|
||||
if (fs::exists(path, ec) && !ec) {
|
||||
std::ifstream existing(path, std::ios::binary);
|
||||
if (existing.is_open()) {
|
||||
std::ostringstream ss;
|
||||
ss << existing.rdbuf();
|
||||
if (ss.str() == text) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs::create_directories(path.parent_path(), ec);
|
||||
ec.clear();
|
||||
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||
if (!out.is_open()) {
|
||||
error = "Unable to write wrapper file: " + path.string();
|
||||
return false;
|
||||
}
|
||||
out << text;
|
||||
out.close();
|
||||
if (!out.good()) {
|
||||
error = "Failed to flush wrapper file: " + path.string();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<fs::file_time_type> getFileWriteTime(const fs::path& path) {
|
||||
if (path.empty()) return std::nullopt;
|
||||
std::error_code ec;
|
||||
if (!fs::exists(path, ec) || ec) return std::nullopt;
|
||||
auto t = fs::last_write_time(path, ec);
|
||||
if (ec) return std::nullopt;
|
||||
return t;
|
||||
}
|
||||
|
||||
#if !defined(_WIN32)
|
||||
std::string posixCompileDriver(bool cxx) {
|
||||
static int ccacheAvailable = -1;
|
||||
if (ccacheAvailable < 0) {
|
||||
ccacheAvailable = (std::system("command -v ccache >/dev/null 2>&1") == 0) ? 1 : 0;
|
||||
}
|
||||
if (ccacheAvailable == 1) {
|
||||
return cxx ? "ccache g++" : "ccache gcc";
|
||||
}
|
||||
return cxx ? "g++" : "gcc";
|
||||
}
|
||||
#endif
|
||||
// why does windows need all of this :sob:
|
||||
#if defined(_WIN32)
|
||||
std::string getEnvValue(const char* name) {
|
||||
@@ -457,14 +512,7 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
|
||||
#else
|
||||
secondaryObjectPath = config.outDir / relativeParent / (baseName + ".wrap.o");
|
||||
#endif
|
||||
std::error_code createErr;
|
||||
fs::create_directories(wrapperPath.parent_path(), createErr);
|
||||
|
||||
std::ofstream wrapper(wrapperPath);
|
||||
if (!wrapper.is_open()) {
|
||||
error = "Unable to write C API wrapper file: " + wrapperPath.string();
|
||||
return false;
|
||||
}
|
||||
std::ostringstream wrapper;
|
||||
|
||||
auto emitCImplDecl = [&](const char* name, const FunctionSpec& spec) {
|
||||
if (!spec.present) return;
|
||||
@@ -846,6 +894,9 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
|
||||
emitEditorBridge("RenderEditorWindow", "Modu_RenderEditorWindow", editorRenderSpec);
|
||||
emitEditorBridge("ExitRenderEditorWindow", "Modu_ExitRenderEditorWindow", editorExitSpec);
|
||||
wrapper << "}\n";
|
||||
if (!writeTextFileIfChanged(wrapperPath, wrapper.str(), error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
compileCmd << "cl /nologo /TC /MD /Zi /Od";
|
||||
@@ -861,11 +912,11 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
|
||||
linkCmd << " " << lib;
|
||||
}
|
||||
#else
|
||||
compileCmd << "gcc -std=c11 -fPIC -O0 -g";
|
||||
compileCmd << posixCompileDriver(false) << " -std=c11 -fPIC -O0 -g";
|
||||
appendPosixIncludesAndDefines(compileCmd);
|
||||
compileCmd << " -c \"" << scriptAbs.string() << "\" -o \"" << objectPath.string() << "\"";
|
||||
compileCmd << " && ";
|
||||
compileCmd << "g++ -std=" << config.cppStandard << " -fPIC -O0 -g";
|
||||
compileCmd << posixCompileDriver(true) << " -std=" << config.cppStandard << " -fPIC -O0 -g";
|
||||
appendPosixIncludesAndDefines(compileCmd);
|
||||
compileCmd << " -c \"" << wrapperPath.string() << "\" -o \"" << secondaryObjectPath.string() << "\"";
|
||||
linkCmd << "g++ -shared \"" << objectPath.string() << "\" \"" << secondaryObjectPath.string()
|
||||
@@ -895,14 +946,7 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
|
||||
fs::path sourceToCompile = scriptAbs;
|
||||
if (useWrapper) {
|
||||
wrapperPath = config.outDir / relativeParent / (baseName + ".wrap.cpp");
|
||||
std::error_code createErr;
|
||||
fs::create_directories(wrapperPath.parent_path(), createErr);
|
||||
|
||||
std::ofstream wrapper(wrapperPath);
|
||||
if (!wrapper.is_open()) {
|
||||
error = "Unable to write wrapper file: " + wrapperPath.string();
|
||||
return false;
|
||||
}
|
||||
std::ostringstream wrapper;
|
||||
|
||||
std::string includePath = scriptAbs.lexically_normal().generic_string();
|
||||
if (needsInspectorWrap) {
|
||||
@@ -980,6 +1024,9 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
|
||||
}
|
||||
|
||||
wrapper << "}\n";
|
||||
if (!writeTextFileIfChanged(wrapperPath, wrapper.str(), error)) {
|
||||
return false;
|
||||
}
|
||||
sourceToCompile = wrapperPath;
|
||||
}
|
||||
|
||||
@@ -993,7 +1040,7 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
|
||||
linkCmd << " " << lib;
|
||||
}
|
||||
#else
|
||||
compileCmd << "g++ -std=" << config.cppStandard << " -fPIC -O0 -g";
|
||||
compileCmd << posixCompileDriver(true) << " -std=" << config.cppStandard << " -fPIC -O0 -g";
|
||||
appendPosixIncludesAndDefines(compileCmd);
|
||||
compileCmd << " -c \"" << sourceToCompile.string() << "\" -o \"" << objectPath.string() << "\"";
|
||||
linkCmd << "g++ -shared \"" << objectPath.string() << "\" -o \"" << binaryPath.string() << "\"";
|
||||
@@ -1029,6 +1076,7 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
|
||||
outCommands.secondaryObjectPath = secondaryObjectPath;
|
||||
outCommands.binaryPath = binaryPath;
|
||||
outCommands.wrapperPath = wrapperPath;
|
||||
outCommands.sourcePath = scriptAbs;
|
||||
outCommands.usedWrapper = useWrapper;
|
||||
return true;
|
||||
}
|
||||
@@ -1075,13 +1123,72 @@ bool ScriptCompiler::compile(const ScriptBuildCommands& commands, ScriptCompileO
|
||||
fs::create_directories(commands.binaryPath.parent_path(), ec);
|
||||
}
|
||||
|
||||
if (!runCommand(commands.compile + " 2>&1", output.compileLog)) {
|
||||
error = "Compile failed";
|
||||
return false;
|
||||
bool needsCompile = true;
|
||||
bool needsLink = true;
|
||||
|
||||
const auto sourceTime = getFileWriteTime(commands.sourcePath);
|
||||
const auto wrapperTime = getFileWriteTime(commands.wrapperPath);
|
||||
const auto objectTime = getFileWriteTime(commands.objectPath);
|
||||
const bool hasSecondaryObject = !commands.secondaryObjectPath.empty();
|
||||
const auto secondaryObjectTime = hasSecondaryObject
|
||||
? getFileWriteTime(commands.secondaryObjectPath)
|
||||
: std::optional<fs::file_time_type>{};
|
||||
const auto binaryTime = getFileWriteTime(commands.binaryPath);
|
||||
|
||||
std::optional<fs::file_time_type> newestInput = sourceTime;
|
||||
if (wrapperTime) {
|
||||
newestInput = newestInput ? std::max(*newestInput, *wrapperTime) : wrapperTime;
|
||||
}
|
||||
if (!runCommand(commands.link + " 2>&1", output.linkLog)) {
|
||||
error = "Link failed";
|
||||
return false;
|
||||
|
||||
if (objectTime && newestInput) {
|
||||
needsCompile = (*objectTime < *newestInput);
|
||||
if (hasSecondaryObject) {
|
||||
if (!secondaryObjectTime) {
|
||||
needsCompile = true;
|
||||
} else {
|
||||
needsCompile = needsCompile || (*secondaryObjectTime < *newestInput);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
needsCompile = true;
|
||||
}
|
||||
|
||||
if (!needsCompile) {
|
||||
output.compileLog += "Skipped compile (up-to-date)\n";
|
||||
}
|
||||
|
||||
if (binaryTime && objectTime) {
|
||||
fs::file_time_type newestObject = *objectTime;
|
||||
if (hasSecondaryObject) {
|
||||
if (!secondaryObjectTime) {
|
||||
needsLink = true;
|
||||
} else {
|
||||
newestObject = std::max(newestObject, *secondaryObjectTime);
|
||||
needsLink = (*binaryTime < newestObject);
|
||||
}
|
||||
} else {
|
||||
needsLink = (*binaryTime < newestObject);
|
||||
}
|
||||
} else {
|
||||
needsLink = true;
|
||||
}
|
||||
|
||||
if (needsCompile) {
|
||||
if (!runCommand(commands.compile + " 2>&1", output.compileLog)) {
|
||||
error = "Compile failed";
|
||||
return false;
|
||||
}
|
||||
needsLink = true;
|
||||
}
|
||||
|
||||
if (needsLink) {
|
||||
if (!runCommand(commands.link + " 2>&1", output.linkLog)) {
|
||||
error = "Link failed";
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
output.linkLog += "Skipped link (up-to-date)\n";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ struct ScriptBuildCommands {
|
||||
fs::path secondaryObjectPath;
|
||||
fs::path binaryPath;
|
||||
fs::path wrapperPath;
|
||||
fs::path sourcePath;
|
||||
bool usedWrapper = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -134,6 +134,11 @@ void Shader::setVec3(const std::string &name, const glm::vec3 &value) const
|
||||
glUniform3fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]);
|
||||
}
|
||||
|
||||
void Shader::setVec4(const std::string &name, const glm::vec4 &value) const
|
||||
{
|
||||
glUniform4fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]);
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
17
src/ThirdParty/imgui/imgui.cpp
vendored
17
src/ThirdParty/imgui/imgui.cpp
vendored
@@ -12406,6 +12406,8 @@ static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow* window)
|
||||
{
|
||||
ImVec2 scroll = window->Scroll;
|
||||
ImVec2 decoration_size(window->DecoOuterSizeX1 + window->DecoInnerSizeX1 + window->DecoOuterSizeX2, window->DecoOuterSizeY1 + window->DecoInnerSizeY1 + window->DecoOuterSizeY2);
|
||||
const bool allow_elastic_overscroll = (window->Flags & ImGuiWindowFlags_NoScrollWithMouse) == 0 &&
|
||||
(window->Flags & ImGuiWindowFlags_NoMouseInputs) == 0;
|
||||
for (int axis = 0; axis < 2; axis++)
|
||||
{
|
||||
if (window->ScrollTarget[axis] < FLT_MAX)
|
||||
@@ -12420,9 +12422,18 @@ static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow* window)
|
||||
}
|
||||
scroll[axis] = scroll_target - center_ratio * (window->SizeFull[axis] - decoration_size[axis]);
|
||||
}
|
||||
scroll[axis] = ImRound64(ImMax(scroll[axis], 0.0f));
|
||||
if (!window->Collapsed && !window->SkipItems)
|
||||
scroll[axis] = ImMin(scroll[axis], window->ScrollMax[axis]);
|
||||
if (allow_elastic_overscroll && !window->Collapsed && !window->SkipItems)
|
||||
{
|
||||
const float axis_extent = (axis == 0) ? window->InnerRect.GetWidth() : window->InnerRect.GetHeight();
|
||||
const float overscroll_limit = ImClamp(ImMin(axis_extent * 0.14f, window->ScrollMax[axis] * 0.35f + 6.0f), 6.0f, 26.0f);
|
||||
scroll[axis] = ImClamp(ImRound64(scroll[axis]), -overscroll_limit, window->ScrollMax[axis] + overscroll_limit);
|
||||
}
|
||||
else
|
||||
{
|
||||
scroll[axis] = ImRound64(ImMax(scroll[axis], 0.0f));
|
||||
if (!window->Collapsed && !window->SkipItems)
|
||||
scroll[axis] = ImMin(scroll[axis], window->ScrollMax[axis]);
|
||||
}
|
||||
}
|
||||
return scroll;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user