diff --git a/Content/Editor/Camera/M_Camera.flax b/Content/Editor/Camera/M_Camera.flax
index 7ac9bab2d..5012b16e9 100644
--- a/Content/Editor/Camera/M_Camera.flax
+++ b/Content/Editor/Camera/M_Camera.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4dc3a7d0a8287c7c2c26b7685b726583677f96e1fe86ccb5f8518cb189b51a92
-size 29407
+oid sha256:6a2936be1789e6a7c663f84ddfea8c897fe8273cd2d29910ac37720907d7b930
+size 29533
diff --git a/Content/Editor/CubeTexturePreviewMaterial.flax b/Content/Editor/CubeTexturePreviewMaterial.flax
index b0b54b394..973a01177 100644
--- a/Content/Editor/CubeTexturePreviewMaterial.flax
+++ b/Content/Editor/CubeTexturePreviewMaterial.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:857edec8daa8bbd5bf16c839f65042df7a921154dcf48c1ac7a06aab4e40eab6
-size 30999
+oid sha256:6ecf44ea82025d0f491c68b0470e1704ca5f385bd54ad196d6912aeb2f3aee0f
+size 31125
diff --git a/Content/Editor/DebugMaterials/SingleColor/Decal.flax b/Content/Editor/DebugMaterials/SingleColor/Decal.flax
index 02126626b..6024d3961 100644
--- a/Content/Editor/DebugMaterials/SingleColor/Decal.flax
+++ b/Content/Editor/DebugMaterials/SingleColor/Decal.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fddc7cd2d8732f8eef008d0a2bdc47948b87a14849d9e6fabcfe60ddb218aa42
-size 7617
+oid sha256:d8301da35f0b45f58f5c59221bd22bf0a8500555d31ab84ec1c8cd5eca9fc101
+size 9973
diff --git a/Content/Editor/DebugMaterials/SingleColor/Particle.flax b/Content/Editor/DebugMaterials/SingleColor/Particle.flax
index bbab3ee4b..9c2c0755a 100644
--- a/Content/Editor/DebugMaterials/SingleColor/Particle.flax
+++ b/Content/Editor/DebugMaterials/SingleColor/Particle.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e06851af881852106892b2bd03df0127c84db3a601abd9d7aaac7baf40343369
-size 32040
+oid sha256:4e432328bb19eaa58caf35f60cd6495a46cca694314828010f3be401d7de9434
+size 32108
diff --git a/Content/Editor/DebugMaterials/SingleColor/Surface.flax b/Content/Editor/DebugMaterials/SingleColor/Surface.flax
index dd9785be9..0fb0d1c6f 100644
--- a/Content/Editor/DebugMaterials/SingleColor/Surface.flax
+++ b/Content/Editor/DebugMaterials/SingleColor/Surface.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:34080fbc40ed62bc5501969640403013291b3104dbb4374d9268994f3902e5a8
-size 29180
+oid sha256:1688861997cc2e8a433cdea81ee62662f45b261bc863cdb9431beed612f0aad7
+size 29306
diff --git a/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax b/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax
index 2ec83813f..e5bda89f6 100644
--- a/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax
+++ b/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:71acbd5242dd2d2f02554af0e5a95da9a5bd90d77aef80dc6fcdaefb88251d23
-size 31365
+oid sha256:42363fad30e29b0e0c4cf7ad9e02dc91040eb6821c3c28bd49996771e65893c4
+size 31559
diff --git a/Content/Editor/DebugMaterials/SingleColor/Terrain.flax b/Content/Editor/DebugMaterials/SingleColor/Terrain.flax
index ac31c7161..09a253a8d 100644
--- a/Content/Editor/DebugMaterials/SingleColor/Terrain.flax
+++ b/Content/Editor/DebugMaterials/SingleColor/Terrain.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:dcc185a2ec2e02ee9f52c806ba3756827c6d12f586c91a12b44295a95dd093dc
-size 21331
+oid sha256:eb957ea2ee358b0e611d6612703261c9837099b6d03d48484cdab1151a461d8f
+size 21004
diff --git a/Content/Editor/DefaultFontMaterial.flax b/Content/Editor/DefaultFontMaterial.flax
index 23676ca21..b4f659e34 100644
--- a/Content/Editor/DefaultFontMaterial.flax
+++ b/Content/Editor/DefaultFontMaterial.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f1500a05e700f3b12d7e3984a978773c61f4c9ecc34ec96720fae724a87bab8e
-size 29501
+oid sha256:7a46410b49a7e38c4c2569e4d8d8c8f4744cf26f79c632a53899ce753ab7c88a
+size 29627
diff --git a/Content/Editor/Gizmo/FoliageBrushMaterial.flax b/Content/Editor/Gizmo/FoliageBrushMaterial.flax
index d88a8eea3..1d8e89a65 100644
--- a/Content/Editor/Gizmo/FoliageBrushMaterial.flax
+++ b/Content/Editor/Gizmo/FoliageBrushMaterial.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:10aff82173474030b7ce41b491c803cc0de265ea0317a3aca87102099d65ca79
-size 37392
+oid sha256:f377cc4e05d89e4edbec6382a052abe571f3261bc273cd4475541b7b7051cffb
+size 37586
diff --git a/Content/Editor/Gizmo/Material.flax b/Content/Editor/Gizmo/Material.flax
index d7815f10f..cce270410 100644
--- a/Content/Editor/Gizmo/Material.flax
+++ b/Content/Editor/Gizmo/Material.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3e68032a48b3196f73c614a6302223765485e8fa3e1e278a2395c9d9ae1c5064
-size 32519
+oid sha256:271c80d9d9971d96d6ea430dfaf8f8f57d9b5f9fe1770b387f426d3c8721c3d8
+size 32713
diff --git a/Content/Editor/Gizmo/MaterialWire.flax b/Content/Editor/Gizmo/MaterialWire.flax
index 26f3956c5..4b30df5f5 100644
--- a/Content/Editor/Gizmo/MaterialWire.flax
+++ b/Content/Editor/Gizmo/MaterialWire.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:dbc08de78faa00aab330168dce166867d85dcae142081219d6a7eeb5f285ffd7
-size 31216
+oid sha256:ff3e7b3e77afa7191f1db9cf12f21908b80bb8f71e832c37e55547dc9dcab31c
+size 31410
diff --git a/Content/Editor/Gizmo/SelectionOutlineMaterial.flax b/Content/Editor/Gizmo/SelectionOutlineMaterial.flax
index 70a58006b..962179da0 100644
--- a/Content/Editor/Gizmo/SelectionOutlineMaterial.flax
+++ b/Content/Editor/Gizmo/SelectionOutlineMaterial.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:de0b19b33a2aac03a0fb7beda935c8a22cbb506f9ca9383bcce3eaac858a54e3
-size 16166
+oid sha256:fc2facc8fa980e5baa399fa7510a87d33d21bbd4c97eaab24856f6db49b13172
+size 16212
diff --git a/Content/Editor/Gizmo/VertexColorsPreviewMaterial.flax b/Content/Editor/Gizmo/VertexColorsPreviewMaterial.flax
index 01ba9f46a..305c6a4c2 100644
--- a/Content/Editor/Gizmo/VertexColorsPreviewMaterial.flax
+++ b/Content/Editor/Gizmo/VertexColorsPreviewMaterial.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5a32d40e1f13606fea29e45cb0df1a45dbcc51eb329963d8db18ff64fa66bd4e
-size 30293
+oid sha256:4458a9483e81fb0526cc395f93eeae238f4f91fa5d4889e3196b6530a8f17ec2
+size 30419
diff --git a/Content/Editor/Highlight Material.flax b/Content/Editor/Highlight Material.flax
index 7c4be9cc2..e944ae62d 100644
--- a/Content/Editor/Highlight Material.flax
+++ b/Content/Editor/Highlight Material.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d1ec249ecd34cc66f9236431705adfaccff3e9ac5825ea72eb8b18449a96ccb7
-size 29910
+oid sha256:391606b1f7563d9a8e414baf6c19f3d99694a28f3f574b7aca64a325294d8e39
+size 30104
diff --git a/Content/Editor/Icons/IconsMaterial.flax b/Content/Editor/Icons/IconsMaterial.flax
index 27e1e992f..320547f9a 100644
--- a/Content/Editor/Icons/IconsMaterial.flax
+++ b/Content/Editor/Icons/IconsMaterial.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:652de424a7e366c1dcafd42cfe552f2d4f1327cd9ef033fb554d961e0a29ae2b
-size 29827
+oid sha256:7951a44b138e60aa9dee4fdaf000eba8a7faef7b31c2e387f78b4a393d0cd0bc
+size 30021
diff --git a/Content/Editor/IconsAtlas.flax b/Content/Editor/IconsAtlas.flax
index bbac95de6..1c65c9f31 100644
--- a/Content/Editor/IconsAtlas.flax
+++ b/Content/Editor/IconsAtlas.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0f43bf2050d47a1e4d6db35e5da66c2f999236939b9d962cd0fba56dcb171852
-size 5611913
+oid sha256:8f2236762a9e54d6de50415e39b9a6b055bb88ba1ff0621787b96f87c437e520
+size 5612435
diff --git a/Content/Editor/IesProfilePreviewMaterial.flax b/Content/Editor/IesProfilePreviewMaterial.flax
index c23d9981d..c7a025108 100644
--- a/Content/Editor/IesProfilePreviewMaterial.flax
+++ b/Content/Editor/IesProfilePreviewMaterial.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6388c4a40a0de927abdd532d5bfaff5b7c651f8daa434c6f04a0579e33a1015b
-size 20399
+oid sha256:41210c6c490513503f01e8a628d80dd98e58fc0482f9966f9342700118ccd04c
+size 20445
diff --git a/Content/Editor/MaterialTemplates/Decal.shader b/Content/Editor/MaterialTemplates/Decal.shader
index 06b44d498..b933fcbb3 100644
--- a/Content/Editor/MaterialTemplates/Decal.shader
+++ b/Content/Editor/MaterialTemplates/Decal.shader
@@ -13,7 +13,7 @@
META_CB_BEGIN(0, Data)
float4x4 WorldMatrix;
float4x4 InvWorld;
-float4x4 SVPositionToWorld;
+float4x4 SvPositionToWorld;
@1META_CB_END
// Use depth buffer for per-pixel decal layering
@@ -27,12 +27,63 @@ struct MaterialInput
float3 WorldPosition;
float TwoSidedSign;
float2 TexCoord;
+ float4 TexCoord_DDX_DDY;
float3x3 TBN;
float4 SvPosition;
float3 PreSkinnedPosition;
float3 PreSkinnedNormal;
};
+// Calculates decal texcoords for a given pixel position (sampels depth buffer and projects value to decal space).
+float2 SvPositionToDecalUV(float4 svPosition)
+{
+ float2 screenUV = svPosition.xy * ScreenSize.zw;
+ svPosition.z = SAMPLE_RT(DepthBuffer, screenUV).r;
+ float4 positionHS = mul(float4(svPosition.xyz, 1), SvPositionToWorld);
+ float3 positionWS = positionHS.xyz / positionHS.w;
+ float3 positionOS = mul(float4(positionWS, 1), InvWorld).xyz;
+ return positionOS.xz + 0.5f;
+}
+
+// Manually compute ddx/ddy for decal texture cooordinates to avoid the 2x2 pixels artifacts on the edges of geometry under decal
+// [Reference: https://www.humus.name/index.php?page=3D&ID=84]
+float4 CalculateTextureDerivatives(float4 svPosition, float2 texCoord)
+{
+ float4 svDiffX = float4(1, 0, 0, 0);
+ float2 uvDiffX0 = texCoord - SvPositionToDecalUV(svPosition - svDiffX);
+ float2 uvDiffX1 = SvPositionToDecalUV(svPosition + svDiffX) - texCoord;
+ float2 dx = dot(uvDiffX0, uvDiffX0) < dot(uvDiffX1, uvDiffX1) ? uvDiffX0 : uvDiffX1;
+
+ float4 svDiffY = float4(0, 1, 0, 0);
+ float2 uvDiffY0 = texCoord - SvPositionToDecalUV(svPosition - svDiffY);
+ float2 uvDiffY1 = SvPositionToDecalUV(svPosition + svDiffY) - texCoord;
+ float2 dy = dot(uvDiffY0, uvDiffY0) < dot(uvDiffY1, uvDiffY1) ? uvDiffY0 : uvDiffY1;
+
+ return float4(dx, dy);
+}
+
+// Computes the mipmap level for a specific texture dimensions to be sampled at decal texture cooordinates.
+// [Reference: https://hugi.scene.org/online/coding/hugi%2014%20-%20comipmap.htm]
+float CalculateTextureMipmap(MaterialInput input, float2 textureSize)
+{
+ float2 dx = input.TexCoord_DDX_DDY.xy * textureSize;
+ float2 dy = input.TexCoord_DDX_DDY.zw * textureSize;
+ float d = max(dot(dx, dx), dot(dy, dy));
+ return (0.5 * 0.5) * log2(d); // Hardcoded half-mip rate reduction to avoid artifacts when decal is moved over dither texture
+}
+float CalculateTextureMipmap(MaterialInput input, Texture2D t)
+{
+ float2 textureSize;
+ t.GetDimensions(textureSize.x, textureSize.y);
+ return CalculateTextureMipmap(input, textureSize);
+}
+float CalculateTextureMipmap(MaterialInput input, TextureCube t)
+{
+ float2 textureSize;
+ t.GetDimensions(textureSize.x, textureSize.y);
+ return CalculateTextureMipmap(input, textureSize);
+}
+
// Transforms a vector from tangent space to world space
float3 TransformTangentVectorToWorld(MaterialInput input, float3 tangentVector)
{
@@ -116,7 +167,6 @@ Material GetMaterialPS(MaterialInput input)
}
// Input macro specified by the material: DECAL_BLEND_MODE
-
#define DECAL_BLEND_MODE_TRANSLUCENT 0
#define DECAL_BLEND_MODE_STAIN 1
#define DECAL_BLEND_MODE_NORMAL 2
@@ -153,7 +203,7 @@ void PS_Decal(
float2 screenUV = SvPosition.xy * ScreenSize.zw;
SvPosition.z = SAMPLE_RT(DepthBuffer, screenUV).r;
- float4 positionHS = mul(float4(SvPosition.xyz, 1), SVPositionToWorld);
+ float4 positionHS = mul(float4(SvPosition.xyz, 1), SvPositionToWorld);
float3 positionWS = positionHS.xyz / positionHS.w;
float3 positionOS = mul(float4(positionWS, 1), InvWorld).xyz;
@@ -166,8 +216,9 @@ void PS_Decal(
materialInput.TexCoord = decalUVs;
materialInput.TwoSidedSign = 1;
materialInput.SvPosition = SvPosition;
-
- // Build tangent to world transformation matrix
+ materialInput.TexCoord_DDX_DDY = CalculateTextureDerivatives(materialInput.SvPosition, materialInput.TexCoord);
+
+ // Calculate tangent-space
float3 ddxWp = ddx(positionWS);
float3 ddyWp = ddy(positionWS);
materialInput.TBN[0] = normalize(ddyWp);
diff --git a/Content/Editor/MaterialTemplates/GUI.shader b/Content/Editor/MaterialTemplates/GUI.shader
index fad7afa32..69b6d3539 100644
--- a/Content/Editor/MaterialTemplates/GUI.shader
+++ b/Content/Editor/MaterialTemplates/GUI.shader
@@ -20,6 +20,8 @@ float TimeParam;
float4 ViewInfo;
float4 ScreenSize;
float4 ViewSize;
+float3 ViewPadding0;
+float UnscaledTimeParam;
@1META_CB_END
// Shader resources
diff --git a/Content/Editor/MaterialTemplates/PostProcess.shader b/Content/Editor/MaterialTemplates/PostProcess.shader
index 927d63c8c..753ccb253 100644
--- a/Content/Editor/MaterialTemplates/PostProcess.shader
+++ b/Content/Editor/MaterialTemplates/PostProcess.shader
@@ -19,6 +19,8 @@ float4 ViewInfo;
float4 ScreenSize;
float4 TemporalAAJitter;
float4x4 InverseViewProjectionMatrix;
+float3 ViewPadding0;
+float UnscaledTimeParam;
@1META_CB_END
// Shader resources
diff --git a/Content/Editor/Particles/Particle Material Color.flax b/Content/Editor/Particles/Particle Material Color.flax
index 07d6d0587..bde834456 100644
--- a/Content/Editor/Particles/Particle Material Color.flax
+++ b/Content/Editor/Particles/Particle Material Color.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7c71a6394a3a0668f6d2b18281aaa36d83b1b150fd10a869edace21e1ffa5b07
-size 30361
+oid sha256:b600cd725f5550de72d5a2544571ca2c1ea8de1a3d45038bac273d2b6f3b04c2
+size 30429
diff --git a/Content/Editor/Particles/Smoke Material.flax b/Content/Editor/Particles/Smoke Material.flax
index d5b8cb872..7a8fdeb1a 100644
--- a/Content/Editor/Particles/Smoke Material.flax
+++ b/Content/Editor/Particles/Smoke Material.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a16a3fa5bed3bc8030c40fbe0e946f2bdec28745542bf08db1d7b4a43180f785
-size 38900
+oid sha256:96d3865771cfa47ad59e0493c8f1b6e9fd9950593b2b929c91ea2692eca71efd
+size 38495
diff --git a/Content/Editor/SpriteMaterial.flax b/Content/Editor/SpriteMaterial.flax
index 8d001b9c7..876a38a56 100644
--- a/Content/Editor/SpriteMaterial.flax
+++ b/Content/Editor/SpriteMaterial.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7ed88e5e8b513f7460e94942ad722d8b193c013434586f4382673f7dc3074e98
-size 30376
+oid sha256:5d02a6d11ea83ea6c519a1644950750e58433e6a2aea9a2daa646db1d2b0c293
+size 30502
diff --git a/Content/Editor/Terrain/Circle Brush Material.flax b/Content/Editor/Terrain/Circle Brush Material.flax
index f2f21d189..af191fdaa 100644
--- a/Content/Editor/Terrain/Circle Brush Material.flax
+++ b/Content/Editor/Terrain/Circle Brush Material.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9ab7e9c88c9896f37ac5a43efe176e13f98eafacc3117b49cd7173b31376b21d
-size 28003
+oid sha256:8e9ef5186642a38af8ebb5856a891215686576b23841aabe16b7dde7f2bcb57f
+size 27676
diff --git a/Content/Editor/Terrain/Highlight Terrain Material.flax b/Content/Editor/Terrain/Highlight Terrain Material.flax
index d1e4226f5..14bd86c35 100644
--- a/Content/Editor/Terrain/Highlight Terrain Material.flax
+++ b/Content/Editor/Terrain/Highlight Terrain Material.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:40e38d672c13c06a84366689b8ce2b346179b4aa0de3cf1fc6035b33ad036e4a
-size 21506
+oid sha256:5f1a9524d9bc7ee41df761b9fb34613fc04357377a81836fb28b3ee5a6f2dcf4
+size 21179
diff --git a/Content/Editor/TexturePreviewMaterial.flax b/Content/Editor/TexturePreviewMaterial.flax
index 0bcb9b77f..fac30b4f1 100644
--- a/Content/Editor/TexturePreviewMaterial.flax
+++ b/Content/Editor/TexturePreviewMaterial.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:93b0220308d2a3051d7e449505ca18dab7a82d46bf6a86e4ecfb9b741909f91b
-size 10698
+oid sha256:c178081b59417439b2d523486f3c7e4f7f391ff57e5ab5a565aadf3fd31f5488
+size 10744
diff --git a/Content/Editor/Wires Debug Material.flax b/Content/Editor/Wires Debug Material.flax
index bdf2bde08..8fff1a174 100644
--- a/Content/Editor/Wires Debug Material.flax
+++ b/Content/Editor/Wires Debug Material.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d7e22035ac54615fee78cad71d5e5268ff139811d4390fe9a5025ab4d234ff4e
-size 29910
+oid sha256:c31daed51b38e6aa9eeceaad85498d6ae7079f7b125c6f71f036799278c34e22
+size 30104
diff --git a/Content/Engine/DefaultDeformableMaterial.flax b/Content/Engine/DefaultDeformableMaterial.flax
index b9e972b63..8af3db999 100644
--- a/Content/Engine/DefaultDeformableMaterial.flax
+++ b/Content/Engine/DefaultDeformableMaterial.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:feac2f9ced2d8edf6a43f1c56cdfdc27bbd018a4c23a527a403c30bdb9217e63
-size 18922
+oid sha256:df767eeb060d058d502271f1cd89581c57ad339cd690cc9c588f9a34cc9344b1
+size 18985
diff --git a/Content/Engine/DefaultMaterial.flax b/Content/Engine/DefaultMaterial.flax
index a7949d435..a253452df 100644
--- a/Content/Engine/DefaultMaterial.flax
+++ b/Content/Engine/DefaultMaterial.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1db3d343643eec7c6f7c3ac33a205618480ce41b4b196ebfc6b63e00085a555f
-size 31205
+oid sha256:b00388390410aabb11f1d9b032361902d2f284daa765d536c8f2a821f659effe
+size 31331
diff --git a/Content/Engine/DefaultRadialMenu.flax b/Content/Engine/DefaultRadialMenu.flax
index db233a854..2b3d7f0de 100644
--- a/Content/Engine/DefaultRadialMenu.flax
+++ b/Content/Engine/DefaultRadialMenu.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a9e9e01ae0222d9dc0db7c51741c3e2c2595bb550fa9dd1665d11ce4fac0afc9
-size 20468
+oid sha256:2b0272c8f2df095e6609f49845a3d329daaf634e0776ca764e4c51596cac60ff
+size 20514
diff --git a/Content/Engine/DefaultTerrainMaterial.flax b/Content/Engine/DefaultTerrainMaterial.flax
index 83e892360..8d195ab98 100644
--- a/Content/Engine/DefaultTerrainMaterial.flax
+++ b/Content/Engine/DefaultTerrainMaterial.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7c890a5b43c0158d1885c3905ab02c8b0ee36a558c4770d79892837960ee07e5
-size 23628
+oid sha256:a86caeef4de5a84783ba34208701c0f272f3b4b3ff82c64c2553d6aec631e07b
+size 23301
diff --git a/Content/Engine/SingleColorMaterial.flax b/Content/Engine/SingleColorMaterial.flax
index 515c48cea..b6906b930 100644
--- a/Content/Engine/SingleColorMaterial.flax
+++ b/Content/Engine/SingleColorMaterial.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:27acfeeafdb50abe76a2feee0616ea90ee5d4fd72c678244d80f15ed4a97894a
-size 29381
+oid sha256:74d77dbfdf72c9281b0760a266adac7f1eb849f9656ea8da5cd8951f2fab5343
+size 29507
diff --git a/Content/Engine/SkyboxMaterial.flax b/Content/Engine/SkyboxMaterial.flax
index a8b1b327f..8faccf8c0 100644
--- a/Content/Engine/SkyboxMaterial.flax
+++ b/Content/Engine/SkyboxMaterial.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8e7d0fe3697a0b65eefb8756e0f9aa48bc2d505eb94710b5972097eb61b6b029
-size 30944
+oid sha256:8149367ccbef36932866e6af53fedf79931f26677db5dfcce71ba33caeff5980
+size 31070
diff --git a/Content/Shaders/SSAO.flax b/Content/Shaders/SSAO.flax
index d628818a4..d9374b133 100644
--- a/Content/Shaders/SSAO.flax
+++ b/Content/Shaders/SSAO.flax
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b0a576e8a8b818d06ff258874c8d73b273d05c2b8856ff6c14cf0accb2f23f52
-size 36832
+oid sha256:3470722e78ef45616e1d86373e32e40e219f911c88dd98313ee567cccfd10cf5
+size 36833
diff --git a/Flax.sln.DotSettings b/Flax.sln.DotSettings
index 3611934dd..f54190003 100644
--- a/Flax.sln.DotSettings
+++ b/Flax.sln.DotSettings
@@ -73,6 +73,24 @@
<Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" />
<Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" />
<Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" />
+ CCD
+ GPU
+ ID
+ <NamingElement Priority="11" Title="Class and struct public fields"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="PUBLIC"><type Name="class field" /><type Name="struct field" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement>
+ <NamingElement Priority="9" Title="Class and struct methods"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="member function" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement>
+ <NamingElement Priority="17" Title="Typedefs"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="type alias" /><type Name="typedef" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement>
+ <NamingElement Priority="14" Title="Other constants"><Descriptor Static="True" Constexpr="Indeterminate" Const="True" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="class field" /><type Name="local variable" /><type Name="struct field" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement>
+ <NamingElement Priority="13" Title="Enum members"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="scoped enumerator" /><type Name="unscoped enumerator" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement>
+ <NamingElement Priority="15" Title="Global constants"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="True" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="global variable" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement>
+ <NamingElement Priority="7" Title="Global variables"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="global variable" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement>
+ <NamingElement Priority="5" Title="Parameters"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="function parameter" /><type Name="lambda parameter" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></NamingElement>
+ <NamingElement Priority="1" Title="Classes and structs"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="__interface" /><type Name="class" /><type Name="struct" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement>
+ <NamingElement Priority="8" Title="Global functions"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="global function" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement>
+ <NamingElement Priority="16" Title="Namespaces"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="namespace" /><type Name="namespace alias" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement>
+ <NamingElement Priority="6" Title="Local variables"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="local variable" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></NamingElement>
+ <NamingElement Priority="10" Title="Class and struct fields"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="class field" /><type Name="struct field" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="_" Suffix="" Style="aaBb" /></NamingElement>
+ <NamingElement Priority="12" Title="Union members"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="union member" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement>
+ <NamingElement Priority="2" Title="Enums"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="enum" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement>
AI
ARGB
LO
@@ -213,6 +231,7 @@
YZ
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy>
C:\Users\Wojtek\AppData\Local\JetBrains\Transient\ReSharperPlatformVs15\v08_f9eacea9\SolutionCaches
+ True
True
True
True
diff --git a/Source/Editor/CustomEditorWindow.cs b/Source/Editor/CustomEditorWindow.cs
index f28a844ef..3df453ef8 100644
--- a/Source/Editor/CustomEditorWindow.cs
+++ b/Source/Editor/CustomEditorWindow.cs
@@ -18,8 +18,8 @@ namespace FlaxEditor
private readonly CustomEditorPresenter _presenter;
private CustomEditorWindow _customEditor;
- public Win(CustomEditorWindow customEditor)
- : base(Editor.Instance, false, ScrollBars.Vertical)
+ public Win(CustomEditorWindow customEditor, bool hideOnClose, ScrollBars scrollBars)
+ : base(Editor.Instance, hideOnClose, scrollBars)
{
Title = customEditor.GetType().Name;
_customEditor = customEditor;
@@ -64,9 +64,9 @@ namespace FlaxEditor
///
/// Initializes a new instance of the class.
///
- protected CustomEditorWindow()
+ protected CustomEditorWindow(bool hideOnClose = false, ScrollBars scrollBars = ScrollBars.Vertical)
{
- _win = new Win(this);
+ _win = new Win(this, hideOnClose, scrollBars);
ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin;
}
diff --git a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs
index 2daf2f6af..02f5ca7ec 100644
--- a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs
@@ -106,7 +106,6 @@ namespace FlaxEditor.CustomEditors.Editors
_linkButton = new Button
{
- BackgroundBrush = new SpriteBrush(Editor.Instance.Icons.Link32),
Parent = LinkedLabel,
Width = 18,
Height = 18,
@@ -189,6 +188,7 @@ namespace FlaxEditor.CustomEditors.Editors
_linkButton.SetColors(backgroundColor);
_linkButton.BorderColor = _linkButton.BorderColorSelected = _linkButton.BorderColorHighlighted = Color.Transparent;
_linkButton.TooltipText = LinkValues ? "Unlinks scale components from uniform scaling" : "Links scale components for uniform scaling";
+ _linkButton.BackgroundBrush = new SpriteBrush(LinkValues ? Editor.Instance.Icons.Link32 : Editor.Instance.Icons.BrokenLink32);
}
}
diff --git a/Source/Editor/CustomEditors/GUI/PropertiesList.cs b/Source/Editor/CustomEditors/GUI/PropertiesList.cs
index ef90fc706..3847a8f36 100644
--- a/Source/Editor/CustomEditors/GUI/PropertiesList.cs
+++ b/Source/Editor/CustomEditors/GUI/PropertiesList.cs
@@ -14,6 +14,9 @@ namespace FlaxEditor.CustomEditors.GUI
public class PropertiesList : PanelWithMargins
{
// TODO: sync splitter for whole presenter
+
+ private const float SplitterPadding = 15;
+ private const float EditorsMinWidthRatio = 0.4f;
///
/// The splitter size (in pixels).
@@ -25,6 +28,7 @@ namespace FlaxEditor.CustomEditors.GUI
private Rectangle _splitterRect;
private bool _splitterClicked, _mouseOverSplitter;
private bool _cursorChanged;
+ private bool _hasCustomSplitterValue;
///
/// Gets or sets the splitter value (always in range [0; 1]).
@@ -66,6 +70,26 @@ namespace FlaxEditor.CustomEditors.GUI
UpdateSplitRect();
}
+ private void AutoSizeSplitter()
+ {
+ if (_hasCustomSplitterValue || !Editor.Instance.Options.Options.Interface.AutoSizePropertiesPanelSplitter)
+ return;
+
+ Font font = Style.Current.FontMedium;
+
+ float largestWidth = 0f;
+ for (int i = 0; i < _element.Labels.Count; i++)
+ {
+ Label currentLabel = _element.Labels[i];
+ Float2 dimensions = font.MeasureText(currentLabel.Text);
+ float width = dimensions.X + currentLabel.Margin.Left + SplitterPadding;
+
+ largestWidth = Mathf.Max(largestWidth, width);
+ }
+
+ SplitterValue = Mathf.Clamp(largestWidth / Width, 0, 1 - EditorsMinWidthRatio);
+ }
+
private void UpdateSplitRect()
{
_splitterRect = new Rectangle(Mathf.Clamp(_splitterValue * Width - SplitterSize * 0.5f, 0.0f, Width), 0, SplitterSize, Height);
@@ -122,6 +146,7 @@ namespace FlaxEditor.CustomEditors.GUI
SplitterValue = location.X / Width;
Cursor = CursorType.SizeWE;
_cursorChanged = true;
+ _hasCustomSplitterValue = true;
}
else if (_mouseOverSplitter)
{
@@ -195,6 +220,7 @@ namespace FlaxEditor.CustomEditors.GUI
// Refresh
UpdateSplitRect();
PerformLayout(true);
+ AutoSizeSplitter();
}
///
diff --git a/Source/Editor/Editor.cs b/Source/Editor/Editor.cs
index 8c3256eb9..853a6eb7d 100644
--- a/Source/Editor/Editor.cs
+++ b/Source/Editor/Editor.cs
@@ -528,7 +528,11 @@ namespace FlaxEditor
var timeSinceLastSave = Time.UnscaledGameTime - _lastAutoSaveTimer;
var timeToNextSave = options.AutoSaveFrequency * 60.0f - timeSinceLastSave;
- if (timeToNextSave <= 0.0f || _autoSaveNow)
+ if (timeToNextSave <= 0.0f && GetWindows().Any(x => x.GUI.Children.Any(c => c is GUI.ContextMenu.ContextMenuBase)))
+ {
+ // Skip aut-save if any context menu is opened to wait for user to end interaction
+ }
+ else if (timeToNextSave <= 0.0f || _autoSaveNow)
{
Log("Auto save");
_lastAutoSaveTimer = Time.UnscaledGameTime;
diff --git a/Source/Editor/EditorIcons.cs b/Source/Editor/EditorIcons.cs
index 6deb65640..8688c502a 100644
--- a/Source/Editor/EditorIcons.cs
+++ b/Source/Editor/EditorIcons.cs
@@ -39,6 +39,7 @@ namespace FlaxEditor
public SpriteHandle Globe32;
public SpriteHandle CamSpeed32;
public SpriteHandle Link32;
+ public SpriteHandle BrokenLink32;
public SpriteHandle Add32;
public SpriteHandle Left32;
public SpriteHandle Right32;
@@ -94,6 +95,7 @@ namespace FlaxEditor
public SpriteHandle Search64;
public SpriteHandle Bone64;
public SpriteHandle Link64;
+ public SpriteHandle BrokenLink64;
public SpriteHandle Build64;
public SpriteHandle Add64;
public SpriteHandle ShipIt64;
diff --git a/Source/Editor/GUI/Docking/DockHintWindow.cs b/Source/Editor/GUI/Docking/DockHintWindow.cs
index cca696d9a..30ffd6287 100644
--- a/Source/Editor/GUI/Docking/DockHintWindow.cs
+++ b/Source/Editor/GUI/Docking/DockHintWindow.cs
@@ -215,8 +215,8 @@ namespace FlaxEditor.GUI.Docking
switch (state)
{
case DockState.DockFill:
- result.Location.Y += DockPanel.DefaultHeaderHeight;
- result.Size.Y -= DockPanel.DefaultHeaderHeight;
+ result.Location.Y += Editor.Instance.Options.Options.Interface.TabHeight;
+ result.Size.Y -= Editor.Instance.Options.Options.Interface.TabHeight;
break;
case DockState.DockTop:
result.Size.Y *= DockPanel.DefaultSplitterValue;
diff --git a/Source/Editor/GUI/Docking/DockPanelProxy.cs b/Source/Editor/GUI/Docking/DockPanelProxy.cs
index 5cf64266a..888b78232 100644
--- a/Source/Editor/GUI/Docking/DockPanelProxy.cs
+++ b/Source/Editor/GUI/Docking/DockPanelProxy.cs
@@ -1,6 +1,7 @@
// Copyright (c) Wojciech Figat. All rights reserved.
using FlaxEditor.GUI.ContextMenu;
+using FlaxEditor.Options;
using FlaxEngine;
using FlaxEngine.GUI;
@@ -13,12 +14,16 @@ namespace FlaxEditor.GUI.Docking
public class DockPanelProxy : ContainerControl
{
private DockPanel _panel;
+ private InterfaceOptions.TabCloseButtonVisibility closeButtonVisibility;
private double _dragEnterTime = -1;
- #if PLATFORM_WINDOWS
- private const bool HideTabForSingleTab = true;
- #else
- private const bool HideTabForSingleTab = false;
- #endif
+ private float _tabHeight = Editor.Instance.Options.Options.Interface.TabHeight;
+ private bool _useMinimumTabWidth = Editor.Instance.Options.Options.Interface.UseMinimumTabWidth;
+ private float _minimumTabWidth = Editor.Instance.Options.Options.Interface.MinimumTabWidth;
+#if PLATFORM_WINDOWS
+ private readonly bool _hideTabForSingleTab = Editor.Instance.Options.Options.Interface.HideSingleTabWindowTabBars;
+#else
+ private readonly bool _hideTabForSingleTab = false;
+#endif
///
/// The is mouse down flag (left button).
@@ -55,8 +60,8 @@ namespace FlaxEditor.GUI.Docking
///
public DockWindow StartDragAsyncWindow;
- private Rectangle HeaderRectangle => new Rectangle(0, 0, Width, DockPanel.DefaultHeaderHeight);
- private bool IsSingleFloatingWindow => HideTabForSingleTab && _panel.TabsCount == 1 && _panel.IsFloating && _panel.ChildPanelsCount == 0;
+ private Rectangle HeaderRectangle => new Rectangle(0, 0, Width, _tabHeight);
+ private bool IsSingleFloatingWindow => _hideTabForSingleTab && _panel.TabsCount == 1 && _panel.IsFloating && _panel.ChildPanelsCount == 0;
///
/// Initializes a new instance of the class.
@@ -68,6 +73,14 @@ namespace FlaxEditor.GUI.Docking
_panel = panel;
AnchorPreset = AnchorPresets.StretchAll;
Offsets = Margin.Zero;
+
+ Editor.Instance.Options.OptionsChanged += OnEditorOptionsChanged;
+ OnEditorOptionsChanged(Editor.Instance.Options.Options);
+ }
+
+ private void OnEditorOptionsChanged(EditorOptions options)
+ {
+ closeButtonVisibility = options.Interface.ShowTabCloseButton;
}
private DockWindow GetTabAtPos(Float2 position, out bool closeButton)
@@ -78,11 +91,11 @@ namespace FlaxEditor.GUI.Docking
var tabsCount = _panel.TabsCount;
if (tabsCount == 1)
{
- var crossRect = new Rectangle(Width - DockPanel.DefaultButtonsSize - DockPanel.DefaultButtonsMargin, (DockPanel.DefaultHeaderHeight - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize);
+ var crossRect = new Rectangle(Width - DockPanel.DefaultButtonsSize - DockPanel.DefaultButtonsMargin, (HeaderRectangle.Height - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize);
if (HeaderRectangle.Contains(position))
{
- closeButton = crossRect.Contains(position);
result = _panel.GetTab(0);
+ closeButton = crossRect.Contains(position) && IsCloseButtonVisible(result, closeButtonVisibility);
}
}
else
@@ -91,15 +104,17 @@ namespace FlaxEditor.GUI.Docking
for (int i = 0; i < tabsCount; i++)
{
var tab = _panel.GetTab(i);
- var titleSize = tab.TitleSize;
- var iconWidth = tab.Icon.IsValid ? DockPanel.DefaultButtonsSize + DockPanel.DefaultLeftTextMargin : 0;
- var width = titleSize.X + DockPanel.DefaultButtonsSize + 2 * DockPanel.DefaultButtonsMargin + DockPanel.DefaultLeftTextMargin + DockPanel.DefaultRightTextMargin + iconWidth;
- var tabRect = new Rectangle(x, 0, width, DockPanel.DefaultHeaderHeight);
+ float width = CalculateTabWidth(tab, closeButtonVisibility);
+
+ if (_useMinimumTabWidth && width < _minimumTabWidth)
+ width = _minimumTabWidth;
+
+ var tabRect = new Rectangle(x, 0, width, HeaderRectangle.Height);
var isMouseOver = tabRect.Contains(position);
if (isMouseOver)
{
- var crossRect = new Rectangle(x + width - DockPanel.DefaultButtonsSize - DockPanel.DefaultButtonsMargin, (DockPanel.DefaultHeaderHeight - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize);
- closeButton = crossRect.Contains(position);
+ var crossRect = new Rectangle(x + width - DockPanel.DefaultButtonsSize - DockPanel.DefaultButtonsMargin, (HeaderRectangle.Height - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize);
+ closeButton = crossRect.Contains(position) && IsCloseButtonVisible(tab, closeButtonVisibility);
result = tab;
break;
}
@@ -110,6 +125,24 @@ namespace FlaxEditor.GUI.Docking
return result;
}
+ private bool IsCloseButtonVisible(DockWindow win, InterfaceOptions.TabCloseButtonVisibility visibilityMode)
+ {
+ return visibilityMode != InterfaceOptions.TabCloseButtonVisibility.Never &&
+ (visibilityMode == InterfaceOptions.TabCloseButtonVisibility.Always ||
+ (visibilityMode == InterfaceOptions.TabCloseButtonVisibility.SelectedTab && _panel.SelectedTab == win));
+ }
+
+ private float CalculateTabWidth(DockWindow win, InterfaceOptions.TabCloseButtonVisibility visibilityMode)
+ {
+ var iconWidth = win.Icon.IsValid ? DockPanel.DefaultButtonsSize + DockPanel.DefaultLeftTextMargin : 0;
+ var width = win.TitleSize.X + DockPanel.DefaultLeftTextMargin + DockPanel.DefaultRightTextMargin + iconWidth;
+
+ if (IsCloseButtonVisible(win, visibilityMode))
+ width += 2 * DockPanel.DefaultButtonsMargin + DockPanel.DefaultButtonsSize;
+
+ return width;
+ }
+
private void GetTabRect(DockWindow win, out Rectangle bounds)
{
FlaxEngine.Assertions.Assert.IsTrue(_panel.ContainsTab(win));
@@ -127,10 +160,10 @@ namespace FlaxEditor.GUI.Docking
{
var tab = _panel.GetTab(i);
var titleSize = tab.TitleSize;
- float width = titleSize.X + DockPanel.DefaultButtonsSize + 2 * DockPanel.DefaultButtonsMargin + DockPanel.DefaultLeftTextMargin + DockPanel.DefaultRightTextMargin;
+ float width = CalculateTabWidth(tab, closeButtonVisibility);
if (tab == win)
{
- bounds = new Rectangle(x, 0, width, DockPanel.DefaultHeaderHeight);
+ bounds = new Rectangle(x, 0, width, HeaderRectangle.Height);
return;
}
x += width;
@@ -210,7 +243,7 @@ namespace FlaxEditor.GUI.Docking
{
Render2D.DrawSprite(
tab.Icon,
- new Rectangle(DockPanel.DefaultLeftTextMargin, (DockPanel.DefaultHeaderHeight - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize),
+ new Rectangle(DockPanel.DefaultLeftTextMargin, (HeaderRectangle.Height - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize),
style.Foreground);
}
@@ -219,17 +252,20 @@ namespace FlaxEditor.GUI.Docking
Render2D.DrawText(
style.FontMedium,
tab.Title,
- new Rectangle(DockPanel.DefaultLeftTextMargin + iconWidth, 0, Width - DockPanel.DefaultLeftTextMargin - DockPanel.DefaultButtonsSize - 2 * DockPanel.DefaultButtonsMargin, DockPanel.DefaultHeaderHeight),
+ new Rectangle(DockPanel.DefaultLeftTextMargin + iconWidth, 0, Width - DockPanel.DefaultLeftTextMargin - DockPanel.DefaultButtonsSize - 2 * DockPanel.DefaultButtonsMargin, HeaderRectangle.Height),
style.Foreground,
TextAlignment.Near,
TextAlignment.Center);
- // Draw cross
- var crossRect = new Rectangle(Width - DockPanel.DefaultButtonsSize - DockPanel.DefaultButtonsMargin, (DockPanel.DefaultHeaderHeight - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize);
- bool isMouseOverCross = isMouseOver && crossRect.Contains(MousePosition);
- if (isMouseOverCross)
- Render2D.FillRectangle(crossRect, (containsFocus ? style.BackgroundSelected : style.LightBackground) * 1.3f);
- Render2D.DrawSprite(style.Cross, crossRect, isMouseOverCross ? style.Foreground : style.ForegroundGrey);
+ if (IsCloseButtonVisible(tab, closeButtonVisibility))
+ {
+ // Draw cross
+ var crossRect = new Rectangle(Width - DockPanel.DefaultButtonsSize - DockPanel.DefaultButtonsMargin, (HeaderRectangle.Height - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize);
+ bool isMouseOverCross = isMouseOver && crossRect.Contains(MousePosition);
+ if (isMouseOverCross)
+ Render2D.FillRectangle(crossRect, (containsFocus ? style.BackgroundSelected : style.LightBackground) * 1.3f);
+ Render2D.DrawSprite(style.Cross, crossRect, isMouseOverCross ? style.Foreground : style.ForegroundGrey);
+ }
}
else
{
@@ -243,10 +279,14 @@ namespace FlaxEditor.GUI.Docking
// Cache data
var tab = _panel.GetTab(i);
var tabColor = Color.Black;
- var titleSize = tab.TitleSize;
var iconWidth = tab.Icon.IsValid ? DockPanel.DefaultButtonsSize + DockPanel.DefaultLeftTextMargin : 0;
- var width = titleSize.X + DockPanel.DefaultButtonsSize + 2 * DockPanel.DefaultButtonsMargin + DockPanel.DefaultLeftTextMargin + DockPanel.DefaultRightTextMargin + iconWidth;
- var tabRect = new Rectangle(x, 0, width, DockPanel.DefaultHeaderHeight);
+
+ float width = CalculateTabWidth(tab, closeButtonVisibility);
+
+ if (_useMinimumTabWidth && width < _minimumTabWidth)
+ width = _minimumTabWidth;
+
+ var tabRect = new Rectangle(x, 0, width, headerRect.Height);
var isMouseOver = tabRect.Contains(MousePosition);
var isSelected = _panel.SelectedTab == tab;
@@ -273,7 +313,7 @@ namespace FlaxEditor.GUI.Docking
{
Render2D.DrawSprite(
tab.Icon,
- new Rectangle(x + DockPanel.DefaultLeftTextMargin, (DockPanel.DefaultHeaderHeight - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize),
+ new Rectangle(x + DockPanel.DefaultLeftTextMargin, (HeaderRectangle.Height - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize),
style.Foreground);
}
@@ -282,27 +322,27 @@ namespace FlaxEditor.GUI.Docking
Render2D.DrawText(
style.FontMedium,
tab.Title,
- new Rectangle(x + DockPanel.DefaultLeftTextMargin + iconWidth, 0, 10000, DockPanel.DefaultHeaderHeight),
+ new Rectangle(x + DockPanel.DefaultLeftTextMargin + iconWidth, 0, 10000, HeaderRectangle.Height),
style.Foreground,
TextAlignment.Near,
TextAlignment.Center);
// Draw cross
- if (isSelected || isMouseOver)
+ if (IsCloseButtonVisible(tab, closeButtonVisibility))
{
- var crossRect = new Rectangle(x + width - DockPanel.DefaultButtonsSize - DockPanel.DefaultButtonsMargin, (DockPanel.DefaultHeaderHeight - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize);
+ var crossRect = new Rectangle(x + width - DockPanel.DefaultButtonsSize - DockPanel.DefaultButtonsMargin, (HeaderRectangle.Height - DockPanel.DefaultButtonsSize) / 2, DockPanel.DefaultButtonsSize, DockPanel.DefaultButtonsSize);
bool isMouseOverCross = isMouseOver && crossRect.Contains(MousePosition);
if (isMouseOverCross)
Render2D.FillRectangle(crossRect, tabColor * 1.3f);
Render2D.DrawSprite(style.Cross, crossRect, isMouseOverCross ? style.Foreground : style.ForegroundGrey);
}
- // Move
+ // Set the start position for the next tab
x += width;
}
// Draw selected tab strip
- Render2D.FillRectangle(new Rectangle(0, DockPanel.DefaultHeaderHeight - 2, Width, 2), containsFocus ? style.BackgroundSelected : style.BackgroundNormal);
+ Render2D.FillRectangle(new Rectangle(0, HeaderRectangle.Height - 2, Width, 2), containsFocus ? style.BackgroundSelected : style.BackgroundNormal);
}
}
@@ -520,7 +560,7 @@ namespace FlaxEditor.GUI.Docking
if (IsSingleFloatingWindow)
rect = new Rectangle(0, 0, Width, Height);
else
- rect = new Rectangle(0, DockPanel.DefaultHeaderHeight, Width, Height - DockPanel.DefaultHeaderHeight);
+ rect = new Rectangle(0, HeaderRectangle.Height, Width, Height - HeaderRectangle.Height);
}
private DragDropEffect TrySelectTabUnderLocation(ref Float2 location)
diff --git a/Source/Editor/GUI/ItemsListContextMenu.cs b/Source/Editor/GUI/ItemsListContextMenu.cs
index 6cb5a84fb..8fdf21e4c 100644
--- a/Source/Editor/GUI/ItemsListContextMenu.cs
+++ b/Source/Editor/GUI/ItemsListContextMenu.cs
@@ -514,20 +514,15 @@ namespace FlaxEditor.GUI
var items = ItemsPanel.Children;
for (int i = 0; i < items.Count; i++)
{
- if (items[i] is Item item && item.Visible)
+ var currentItem = items[i];
+ if (currentItem is Item item && item.Visible)
result.Add(item);
- }
- if (_categoryPanels != null)
- {
- for (int i = 0; i < _categoryPanels.Count; i++)
+ else if (currentItem is DropPanel category && (!ignoreFoldedCategories || !category.IsClosed) && currentItem.Visible)
{
- var category = _categoryPanels[i];
- if (!category.Visible || (ignoreFoldedCategories && category is DropPanel panel && panel.IsClosed))
- continue;
for (int j = 0; j < category.Children.Count; j++)
{
- if (category.Children[j] is Item item2 && item2.Visible)
- result.Add(item2);
+ if (category.Children[j] is Item categoryItem && categoryItem.Visible)
+ result.Add(categoryItem);
}
}
}
@@ -591,10 +586,6 @@ namespace FlaxEditor.GUI
var items = GetVisibleItems(!controlDown);
var focusedIndex = items.IndexOf(focusedItem);
- // If the user hasn't selected anything yet and is holding control, focus first folded item
- if (focusedIndex == -1 && controlDown)
- focusedIndex = GetVisibleItems(true).Count - 1;
-
int delta = key == KeyboardKeys.ArrowDown ? -1 : 1;
int nextIndex = Mathf.Wrap(focusedIndex - delta, 0, items.Count - 1);
var nextItem = items[nextIndex];
diff --git a/Source/Editor/GUI/NavigationBar.cs b/Source/Editor/GUI/NavigationBar.cs
index bedf4c070..eb87eaafc 100644
--- a/Source/Editor/GUI/NavigationBar.cs
+++ b/Source/Editor/GUI/NavigationBar.cs
@@ -11,6 +11,9 @@ namespace FlaxEditor.GUI
///
public class NavigationBar : Panel
{
+ private float _toolstripHeight = 0;
+ private Margin _toolstripMargin;
+
///
/// The default buttons margin.
///
@@ -50,9 +53,42 @@ namespace FlaxEditor.GUI
{
if (toolstrip == null)
return;
+
+ if (_toolstripHeight <= 0.0f)
+ {
+ // Cache initial toolstrip state
+ _toolstripHeight = toolstrip.Height;
+ _toolstripMargin = toolstrip.ItemsMargin;
+ }
+
+ // Control toolstrip bottom margin to prevent navigation bar scroll going over the buttons
+ var toolstripLocked = toolstrip.IsLayoutLocked;
+ toolstrip.IsLayoutLocked = true;
+ var toolstripHeight = _toolstripHeight;
+ var toolstripMargin = _toolstripMargin;
+ if (HScrollBar.Visible)
+ {
+ float scrollMargin = 8;
+ toolstripHeight += scrollMargin;
+ toolstripMargin.Bottom += scrollMargin;
+ }
+ toolstrip.Height = toolstripHeight;
+ toolstrip.IsLayoutLocked = toolstripLocked;
+ toolstrip.ItemsMargin = toolstripMargin;
+
var lastToolstripButton = toolstrip.LastButton;
var parentSize = Parent.Size;
Bounds = new Rectangle(lastToolstripButton.Right + 8.0f, 0, parentSize.X - X - 8.0f, toolstrip.Height);
}
+
+ ///
+ public override void PerformLayout(bool force = false)
+ {
+ base.PerformLayout(force);
+
+ // Stretch excluding toolstrip margin to fill the space
+ if (Parent is ToolStrip toolStrip)
+ Height = toolStrip.Height;
+ }
}
}
diff --git a/Source/Editor/GUI/Popups/RenamePopup.cs b/Source/Editor/GUI/Popups/RenamePopup.cs
index 353830a21..d17a4180d 100644
--- a/Source/Editor/GUI/Popups/RenamePopup.cs
+++ b/Source/Editor/GUI/Popups/RenamePopup.cs
@@ -130,6 +130,10 @@ namespace FlaxEditor.GUI
/// Created popup.
public static RenamePopup Show(Control control, Rectangle area, string value, bool isMultiline)
{
+ // hardcoded flushing layout for tree controls
+ if (control is Tree.TreeNode treeNode && treeNode.ParentTree != null)
+ treeNode.ParentTree.FlushPendingPerformLayout();
+
// Calculate the control size in the window space to handle scaled controls
var upperLeft = control.PointToWindow(area.UpperLeft);
var bottomRight = control.PointToWindow(area.BottomRight);
diff --git a/Source/Editor/GUI/ToolStrip.cs b/Source/Editor/GUI/ToolStrip.cs
index 09f61b71d..eeba67cde 100644
--- a/Source/Editor/GUI/ToolStrip.cs
+++ b/Source/Editor/GUI/ToolStrip.cs
@@ -13,15 +13,7 @@ namespace FlaxEditor.GUI
///
public class ToolStrip : ContainerControl
{
- ///
- /// The default margin vertically.
- ///
- public const int DefaultMarginV = 1;
-
- ///
- /// The default margin horizontally.
- ///
- public const int DefaultMarginH = 2;
+ private Margin _itemsMargin;
///
/// Event fired when button gets clicked with the primary mouse button.
@@ -66,10 +58,26 @@ namespace FlaxEditor.GUI
}
}
+ ///
+ /// Gets or sets the space around items.
+ ///
+ public Margin ItemsMargin
+ {
+ get => _itemsMargin;
+ set
+ {
+ if (_itemsMargin != value)
+ {
+ _itemsMargin = value;
+ PerformLayout();
+ }
+ }
+ }
+
///
/// Gets the height for the items.
///
- public float ItemsHeight => Height - 2 * DefaultMarginV;
+ public float ItemsHeight => Height - _itemsMargin.Height;
///
/// Initializes a new instance of the class.
@@ -82,6 +90,7 @@ namespace FlaxEditor.GUI
AnchorPreset = AnchorPresets.HorizontalStretchTop;
BackgroundColor = Style.Current.LightBackground;
Offsets = new Margin(0, 0, y, height * Editor.Instance.Options.Options.Interface.IconsScale);
+ _itemsMargin = new Margin(2, 2, 1, 1);
}
///
@@ -161,7 +170,7 @@ namespace FlaxEditor.GUI
protected override void PerformLayoutBeforeChildren()
{
// Arrange controls
- float x = DefaultMarginH;
+ float x = _itemsMargin.Left;
float h = ItemsHeight;
for (int i = 0; i < _children.Count; i++)
{
@@ -169,8 +178,8 @@ namespace FlaxEditor.GUI
if (c.Visible)
{
var w = c.Width;
- c.Bounds = new Rectangle(x, DefaultMarginV, w, h);
- x += w + DefaultMarginH;
+ c.Bounds = new Rectangle(x, _itemsMargin.Top, w, h);
+ x += w + _itemsMargin.Width;
}
}
}
diff --git a/Source/Editor/GUI/Tree/Tree.cs b/Source/Editor/GUI/Tree/Tree.cs
index 8df26c211..5530a1738 100644
--- a/Source/Editor/GUI/Tree/Tree.cs
+++ b/Source/Editor/GUI/Tree/Tree.cs
@@ -41,6 +41,7 @@ namespace FlaxEditor.GUI.Tree
private Margin _margin;
private bool _autoSize = true;
private bool _deferLayoutUpdate = false;
+ private TreeNode _lastSelectedNode;
///
/// The TreeNode that is being dragged over. This could have a value when not dragging.
@@ -67,7 +68,7 @@ namespace FlaxEditor.GUI.Tree
/// Gets the first selected node or null.
///
public TreeNode SelectedNode => Selection.Count > 0 ? Selection[0] : null;
-
+
///
/// Allow nodes to Draw the root tree line.
///
@@ -364,6 +365,19 @@ namespace FlaxEditor.GUI.Tree
BulkSelectUpdateExpanded(false);
}
+ ///
+ /// Flushes any pending layout perming action that has been delayed until next update to optimize performance of the complex tree hierarchy.
+ ///
+ public void FlushPendingPerformLayout()
+ {
+ if (_deferLayoutUpdate)
+ {
+ base.PerformLayout();
+ AfterDeferredLayout?.Invoke();
+ _deferLayoutUpdate = false;
+ }
+ }
+
///
public override void PerformLayout(bool force = false)
{
@@ -378,25 +392,31 @@ namespace FlaxEditor.GUI.Tree
public override void Update(float deltaTime)
{
if (_deferLayoutUpdate)
- {
- base.PerformLayout();
- AfterDeferredLayout?.Invoke();
- _deferLayoutUpdate = false;
- }
+ FlushPendingPerformLayout();
+ var window = Root;
+ bool shiftDown = window.GetKey(KeyboardKeys.Shift);
+ bool keyUpArrow = window.GetKey(KeyboardKeys.ArrowUp);
+ bool keyDownArrow = window.GetKey(KeyboardKeys.ArrowDown);
- var node = SelectedNode;
+ // Use last selection for last selected node if sift is down
+ if (Selection.Count < 2)
+ _lastSelectedNode = null;
+ else if (shiftDown)
+ _lastSelectedNode ??= Selection[^1];
+
+ // Skip root to prevent blocking input
+ if (_lastSelectedNode != null && _lastSelectedNode.IsRoot)
+ _lastSelectedNode = null;
+
+ var node = _lastSelectedNode ?? SelectedNode;
// Check if has focus and if any node is focused and it isn't a root
if (ContainsFocus && node != null && node.AutoFocus)
{
- var window = Root;
if (window.GetKeyDown(KeyboardKeys.ArrowUp) || window.GetKeyDown(KeyboardKeys.ArrowDown))
_keyUpdateTime = KeyUpdateTimeout;
if (_keyUpdateTime >= KeyUpdateTimeout && window is WindowRootControl windowRoot && windowRoot.Window.IsFocused)
{
- bool keyUpArrow = window.GetKey(KeyboardKeys.ArrowUp);
- bool keyDownArrow = window.GetKey(KeyboardKeys.ArrowDown);
-
// Check if arrow flags are different
if (keyDownArrow != keyUpArrow)
{
@@ -406,24 +426,38 @@ namespace FlaxEditor.GUI.Tree
Assert.AreNotEqual(-1, myIndex);
// Up
- TreeNode toSelect = null;
+ List toSelect = new List();
+ if (shiftDown && _supportMultiSelect)
+ {
+ toSelect.AddRange(Selection);
+ }
if (keyUpArrow)
{
if (myIndex == 0)
{
// Select parent
- toSelect = parentNode;
+ if (toSelect.Contains(parentNode))
+ toSelect.Remove(node);
+ else if (parentNode != null)
+ toSelect.Add(parentNode);
+ _lastSelectedNode = parentNode;
}
else
{
// Select previous parent child
- toSelect = nodeParent.GetChild(myIndex - 1) as TreeNode;
+ var select = nodeParent.GetChild(myIndex - 1) as TreeNode;
// Select last child if is valid and expanded and has any children
- if (toSelect != null && toSelect.IsExpanded && toSelect.HasAnyVisibleChild)
+ if (select != null && select.IsExpanded && select.HasAnyVisibleChild)
{
- toSelect = toSelect.GetChild(toSelect.ChildrenCount - 1) as TreeNode;
+ select = select.GetChild(select.ChildrenCount - 1) as TreeNode;
}
+
+ if (select == null || toSelect.Contains(select))
+ toSelect.Remove(node);
+ else
+ toSelect.Add(select);
+ _lastSelectedNode = select;
}
}
// Down
@@ -432,32 +466,48 @@ namespace FlaxEditor.GUI.Tree
if (node.IsExpanded && node.HasAnyVisibleChild)
{
// Select the first child
- toSelect = node.GetChild(0) as TreeNode;
+ var select = node.GetChild(0) as TreeNode;
+ if (select == null || toSelect.Contains(select))
+ toSelect.Remove(node);
+ else
+ toSelect.Add(select);
+ _lastSelectedNode = select;
}
else if (myIndex == nodeParent.ChildrenCount - 1)
{
// Select next node after parent
- while (parentNode != null && toSelect == null)
+ TreeNode select = null;
+ while (parentNode != null && select == null)
{
int parentIndex = parentNode.IndexInParent;
if (parentIndex != -1 && parentIndex < parentNode.Parent.ChildrenCount - 1)
{
- toSelect = parentNode.Parent.GetChild(parentIndex + 1) as TreeNode;
+ select = parentNode.Parent.GetChild(parentIndex + 1) as TreeNode;
}
parentNode = parentNode.Parent as TreeNode;
}
+ if (select == null || toSelect.Contains(select))
+ toSelect.Remove(node);
+ else
+ toSelect.Add(select);
+ _lastSelectedNode = select;
}
else
{
// Select next parent child
- toSelect = nodeParent.GetChild(myIndex + 1) as TreeNode;
+ var select = nodeParent.GetChild(myIndex + 1) as TreeNode;
+ if (select == null || toSelect.Contains(select))
+ toSelect.Remove(node);
+ else
+ toSelect.Add(select);
+ _lastSelectedNode = select;
}
}
- if (toSelect != null && toSelect.AutoFocus)
+ if (toSelect.Count > 0)
{
// Select
Select(toSelect);
- toSelect.Focus();
+ _lastSelectedNode?.Focus();
}
// Reset time
diff --git a/Source/Editor/Managed/ManagedEditor.cpp b/Source/Editor/Managed/ManagedEditor.cpp
index c020fe7ce..b4178fb89 100644
--- a/Source/Editor/Managed/ManagedEditor.cpp
+++ b/Source/Editor/Managed/ManagedEditor.cpp
@@ -11,6 +11,7 @@
#include "Engine/Scripting/ManagedCLR/MClass.h"
#include "Engine/Scripting/ManagedCLR/MException.h"
#include "Engine/Scripting/Internal/MainThreadManagedInvokeAction.h"
+#include "Engine/Platform/WindowsManager.h"
#include "Engine/Content/Assets/VisualScript.h"
#include "Engine/Content/Content.h"
#include "Engine/Level/Actor.h"
@@ -627,6 +628,14 @@ void ManagedEditor::WipeOutLeftoverSceneObjects()
ObjectsRemovalService::Flush();
}
+Array ManagedEditor::GetWindows()
+{
+ WindowsManager::WindowsLocker.Lock();
+ auto result = WindowsManager::Windows;
+ WindowsManager::WindowsLocker.Unlock();
+ return result;
+}
+
void ManagedEditor::OnEditorAssemblyLoaded(MAssembly* assembly)
{
ASSERT(!HasManagedInstance());
diff --git a/Source/Editor/Managed/ManagedEditor.h b/Source/Editor/Managed/ManagedEditor.h
index 3d4f49708..15649b97b 100644
--- a/Source/Editor/Managed/ManagedEditor.h
+++ b/Source/Editor/Managed/ManagedEditor.h
@@ -259,6 +259,7 @@ public:
API_FUNCTION(Internal) static Array GetVisualScriptLocals();
API_FUNCTION(Internal) static bool EvaluateVisualScriptLocal(VisualScript* script, API_PARAM(Ref) VisualScriptLocal& local);
API_FUNCTION(Internal) static void WipeOutLeftoverSceneObjects();
+ API_FUNCTION(Internal) static Array GetWindows();
private:
void OnEditorAssemblyLoaded(MAssembly* assembly);
diff --git a/Source/Editor/Options/InputOptions.cs b/Source/Editor/Options/InputOptions.cs
index 871156662..af919c1f3 100644
--- a/Source/Editor/Options/InputOptions.cs
+++ b/Source/Editor/Options/InputOptions.cs
@@ -652,43 +652,47 @@ namespace FlaxEditor.Options
#endregion
- #region Node editors
+ #region Node Editors
[DefaultValue(typeof(InputBinding), "Shift+W")]
- [EditorDisplay("Node editors"), EditorOrder(4500)]
+ [EditorDisplay("Node Editors"), EditorOrder(4500)]
public InputBinding NodesAlignTop = new InputBinding(KeyboardKeys.W, KeyboardKeys.Shift);
[DefaultValue(typeof(InputBinding), "Shift+A")]
- [EditorDisplay("Node editors"), EditorOrder(4510)]
+ [EditorDisplay("Node Editors"), EditorOrder(4510)]
public InputBinding NodesAlignLeft = new InputBinding(KeyboardKeys.A, KeyboardKeys.Shift);
[DefaultValue(typeof(InputBinding), "Shift+S")]
- [EditorDisplay("Node editors"), EditorOrder(4520)]
+ [EditorDisplay("Node Editors"), EditorOrder(4520)]
public InputBinding NodesAlignBottom = new InputBinding(KeyboardKeys.S, KeyboardKeys.Shift);
[DefaultValue(typeof(InputBinding), "Shift+D")]
- [EditorDisplay("Node editors"), EditorOrder(4530)]
+ [EditorDisplay("Node Editors"), EditorOrder(4530)]
public InputBinding NodesAlignRight = new InputBinding(KeyboardKeys.D, KeyboardKeys.Shift);
[DefaultValue(typeof(InputBinding), "Alt+Shift+W")]
- [EditorDisplay("Node editors"), EditorOrder(4540)]
+ [EditorDisplay("Node Editors"), EditorOrder(4540)]
public InputBinding NodesAlignMiddle = new InputBinding(KeyboardKeys.W, KeyboardKeys.Shift, KeyboardKeys.Alt);
[DefaultValue(typeof(InputBinding), "Alt+Shift+S")]
- [EditorDisplay("Node editors"), EditorOrder(4550)]
+ [EditorDisplay("Node Editors"), EditorOrder(4550)]
public InputBinding NodesAlignCenter = new InputBinding(KeyboardKeys.S, KeyboardKeys.Shift, KeyboardKeys.Alt);
[DefaultValue(typeof(InputBinding), "Q")]
- [EditorDisplay("Node editors"), EditorOrder(4560)]
+ [EditorDisplay("Node Editors"), EditorOrder(4560)]
public InputBinding NodesAutoFormat = new InputBinding(KeyboardKeys.Q);
- [DefaultValue(typeof(InputBinding), "None")]
- [EditorDisplay("Node editors"), EditorOrder(4570)]
- public InputBinding NodesDistributeHorizontal = new InputBinding(KeyboardKeys.None);
+ [DefaultValue(typeof(InputBinding), "Shift+Q")]
+ [EditorDisplay("Node Editors"), EditorOrder(4560)]
+ public InputBinding NodesStraightenConnections = new InputBinding(KeyboardKeys.Q, KeyboardKeys.Shift);
- [DefaultValue(typeof(InputBinding), "None")]
- [EditorDisplay("Node editors"), EditorOrder(4580)]
- public InputBinding NodesDistributeVertical = new InputBinding(KeyboardKeys.None);
+ [DefaultValue(typeof(InputBinding), "Alt+W")]
+ [EditorDisplay("Node Editors"), EditorOrder(4570)]
+ public InputBinding NodesDistributeHorizontal = new InputBinding(KeyboardKeys.W, KeyboardKeys.Alt);
+
+ [DefaultValue(typeof(InputBinding), "Alt+A")]
+ [EditorDisplay("Node Editors"), EditorOrder(4580)]
+ public InputBinding NodesDistributeVertical = new InputBinding(KeyboardKeys.A, KeyboardKeys.Alt);
#endregion
}
diff --git a/Source/Editor/Options/InterfaceOptions.cs b/Source/Editor/Options/InterfaceOptions.cs
index 617c1c3de..f26506c74 100644
--- a/Source/Editor/Options/InterfaceOptions.cs
+++ b/Source/Editor/Options/InterfaceOptions.cs
@@ -76,6 +76,25 @@ namespace FlaxEditor.Options
DockBottom = DockState.DockBottom
}
+ ///
+ /// Options for the visibility status of the tab close button.
+ ///
+ public enum TabCloseButtonVisibility
+ {
+ ///
+ /// Never show the close button.
+ ///
+ Never,
+ ///
+ /// Show the close button on tabs that are currently selected.
+ ///
+ SelectedTab,
+ ///
+ /// Show the close button on all tabs that can be closed.
+ ///
+ Always
+ }
+
///
/// Options for the action taken by the play button.
///
@@ -167,15 +186,6 @@ namespace FlaxEditor.Options
[EditorDisplay("Interface"), EditorOrder(10), Tooltip("Editor User Interface scale. Applied to all UI elements, windows and text. Can be used to scale the interface up on a bigger display. Editor restart required.")]
public float InterfaceScale { get; set; } = 1.0f;
-#if PLATFORM_WINDOWS
- ///
- /// Gets or sets a value indicating whether use native window title bar. Editor restart required.
- ///
- [DefaultValue(false)]
- [EditorDisplay("Interface"), EditorOrder(70), Tooltip("Determines whether use native window title bar. Editor restart required.")]
- public bool UseNativeWindowSystem { get; set; } = false;
-#endif
-
///
/// Gets or sets a value indicating whether show selected camera preview in the editor window.
///
@@ -183,20 +193,6 @@ namespace FlaxEditor.Options
[EditorDisplay("Interface"), EditorOrder(80), Tooltip("Determines whether show selected camera preview in the edit window.")]
public bool ShowSelectedCameraPreview { get; set; } = true;
- ///
- /// Gets or sets a value indicating whether center mouse position on window focus in play mode. Helps when working with games that lock mouse cursor.
- ///
- [DefaultValue(false)]
- [EditorDisplay("Interface", "Center Mouse On Game Window Focus"), EditorOrder(100), Tooltip("Determines whether center mouse position on window focus in play mode. Helps when working with games that lock mouse cursor.")]
- public bool CenterMouseOnGameWinFocus { get; set; } = false;
-
- ///
- /// Gets or sets the method window opening.
- ///
- [DefaultValue(DockStateProxy.Float)]
- [EditorDisplay("Interface", "New Window Location"), EditorOrder(150), Tooltip("Define the opening method for new windows, open in a new tab by default.")]
- public DockStateProxy NewWindowLocation { get; set; } = DockStateProxy.Float;
-
///
/// Gets or sets the editor icons scale. Editor restart required.
///
@@ -232,6 +228,13 @@ namespace FlaxEditor.Options
[EditorDisplay("Interface"), EditorOrder(310)]
public bool SeparateValueAndUnit { get; set; }
+ ///
+ /// Gets or sets the option to auto size the Properties panel splitter based on the longest property name. Editor restart recommended.
+ ///
+ [DefaultValue(false)]
+ [EditorDisplay("Interface"), EditorOrder(311)]
+ public bool AutoSizePropertiesPanelSplitter { get; set; }
+
///
/// Gets or sets tree line visibility.
///
@@ -265,6 +268,66 @@ namespace FlaxEditor.Options
[EditorDisplay("Interface"), EditorOrder(322)]
public bool ScrollToScriptOnAdd { get; set; } = true;
+#if PLATFORM_WINDOWS
+ ///
+ /// Gets or sets a value indicating whether use native window title bar. Editor restart required.
+ ///
+ [DefaultValue(false)]
+ [EditorDisplay("Tabs & Windows"), EditorOrder(70), Tooltip("Determines whether use native window title bar. Editor restart required.")]
+ public bool UseNativeWindowSystem { get; set; } = false;
+#endif
+
+#if PLATFORM_WINDOWS
+ ///
+ /// Gets or sets a value indicating whether a window containing a single tabs hides the tab bar. Editor restart recommended.
+ ///
+ [DefaultValue(true)]
+ [EditorDisplay("Tabs & Windows", "Hide Single-Tab Window Tab Bars"), EditorOrder(71)]
+ public bool HideSingleTabWindowTabBars { get; set; } = true;
+#endif
+
+ ///
+ /// Gets or sets a value indicating wether the minum tab width should be used. Editor restart required.
+ ///
+ [DefaultValue(false)]
+ [EditorDisplay("Tabs & Windows"), EditorOrder(99)]
+ public bool UseMinimumTabWidth { get; set; } = false;
+
+ ///
+ /// Gets or sets a value indicating the minimum tab width. If a tab is smaller than this width, its width will be set to this. Editor restart required.
+ ///
+ [DefaultValue(80.0f), Limit(50.0f, 150.0f)]
+ [EditorDisplay("Tabs & Windows"), EditorOrder(99), VisibleIf(nameof(UseMinimumTabWidth))]
+ public float MinimumTabWidth { get; set; } = 80.0f;
+
+ ///
+ /// Gets or sets a value indicating the height of window tabs. Editor restart required.
+ ///
+ [DefaultValue(20.0f), Limit(15.0f, 40.0f)]
+ [EditorDisplay("Tabs & Windows"), EditorOrder(100)]
+ public float TabHeight { get; set; } = 20.0f;
+
+ ///
+ /// Gets or sets a value indicating whether center mouse position on window focus in play mode. Helps when working with games that lock mouse cursor.
+ ///
+ [DefaultValue(false)]
+ [EditorDisplay("Tabs & Windows", "Center Mouse On Game Window Focus"), EditorOrder(101), Tooltip("Determines whether center mouse position on window focus in play mode. Helps when working with games that lock mouse cursor.")]
+ public bool CenterMouseOnGameWinFocus { get; set; } = false;
+
+ ///
+ /// Gets or sets the method window opening.
+ ///
+ [DefaultValue(DockStateProxy.Float)]
+ [EditorDisplay("Tabs & Windows", "New Window Location"), EditorOrder(150), Tooltip("Define the opening method for new windows, open in a new tab by default.")]
+ public DockStateProxy NewWindowLocation { get; set; } = DockStateProxy.Float;
+
+ ///
+ /// Gets or sets a value indicating when the tab close button should be visible.
+ ///
+ [DefaultValue(TabCloseButtonVisibility.SelectedTab)]
+ [EditorDisplay("Tabs & Windows"), EditorOrder(151)]
+ public TabCloseButtonVisibility ShowTabCloseButton { get; set; } = TabCloseButtonVisibility.SelectedTab;
+
///
/// Gets or sets the timestamps prefix mode for output log messages.
///
@@ -420,6 +483,12 @@ namespace FlaxEditor.Options
[DefaultValue(1), Range(1, 4)]
[EditorDisplay("Cook & Run"), EditorOrder(600)]
public int NumberOfGameClientsToLaunch = 1;
+
+ ///
+ /// Gets or sets the build configuration to use when using Cook and Run option in the editor.
+ ///
+ [EditorDisplay("Cook & Run"), EditorOrder(601), ExpandGroups, Tooltip("The build configuration to use when using Cook and Run option in the editor.")]
+ public BuildConfiguration CookAndRunBuildConfiguration { get; set; } = BuildConfiguration.Development;
///
/// Gets or sets the curvature of the line connecting to connected visject nodes.
diff --git a/Source/Editor/Surface/Archetypes/Animation.cs b/Source/Editor/Surface/Archetypes/Animation.cs
index f71f50596..9f3112905 100644
--- a/Source/Editor/Surface/Archetypes/Animation.cs
+++ b/Source/Editor/Surface/Archetypes/Animation.cs
@@ -810,7 +810,7 @@ namespace FlaxEditor.Surface.Archetypes
Create = (id, context, arch, groupArch) => new StateMachineState(id, context, arch, groupArch),
Title = "State",
Description = "The animation states machine state node",
- Flags = NodeFlags.AnimGraph | NodeFlags.NoSpawnViaGUI | NodeFlags.NoSpawnViaPaste,
+ Flags = NodeFlags.AnimGraph | NodeFlags.NoSpawnViaGUI,
Size = new Float2(100, 0),
DefaultValues = new object[]
{
diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs
index 1debf5b60..091fbe48f 100644
--- a/Source/Editor/Surface/Archetypes/Tools.cs
+++ b/Source/Editor/Surface/Archetypes/Tools.cs
@@ -199,7 +199,7 @@ namespace FlaxEditor.Surface.Archetypes
private Label _labelValue;
private FloatValueBox _timeValue;
private ColorValueBox _colorValue;
- private const int MaxStops = 8;
+ private const int MaxStops = 12;
///
public ColorGradientNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
@@ -1386,10 +1386,11 @@ namespace FlaxEditor.Surface.Archetypes
Title = "Time",
Description = "Game time constant",
Flags = NodeFlags.MaterialGraph,
- Size = new Float2(110, 20),
+ Size = new Float2(110, 40),
Elements = new[]
{
- NodeElementArchetype.Factory.Output(0, "", typeof(float), 0),
+ NodeElementArchetype.Factory.Output(0, "Time", typeof(float), 0),
+ NodeElementArchetype.Factory.Output(1, "Unscaled Time", typeof(float), 1),
}
},
new NodeArchetype
@@ -1506,7 +1507,11 @@ namespace FlaxEditor.Surface.Archetypes
0.95f,
Color.White,
- // Empty stops 2-7
+ // Empty stops 2-11
+ 0.0f, Color.Black,
+ 0.0f, Color.Black,
+ 0.0f, Color.Black,
+ 0.0f, Color.Black,
0.0f, Color.Black,
0.0f, Color.Black,
0.0f, Color.Black,
diff --git a/Source/Editor/Surface/NodeAlignmentType.cs b/Source/Editor/Surface/NodeAlignmentType.cs
index 141235783..07e1d8dc4 100644
--- a/Source/Editor/Surface/NodeAlignmentType.cs
+++ b/Source/Editor/Surface/NodeAlignmentType.cs
@@ -5,39 +5,39 @@ using FlaxEngine;
namespace FlaxEditor.Surface
{
///
- /// Node Alignment type
+ /// Node Alignment type.
///
[HideInEditor]
public enum NodeAlignmentType
{
///
- /// Align nodes vertically to top, matching top-most node
+ /// Align nodes vertically to top, matching top-most node.
///
Top,
///
- /// Align nodes vertically to middle, using average of all nodes
+ /// Align nodes vertically to middle, using average of all nodes.
///
Middle,
///
- /// Align nodes vertically to bottom, matching bottom-most node
+ /// Align nodes vertically to bottom, matching bottom-most node.
///
Bottom,
///
- /// Align nodes horizontally to left, matching left-most node
+ /// Align nodes horizontally to left, matching left-most node.
///
Left,
///
- /// Align nodes horizontally to center, using average of all nodes
+ /// Align nodes horizontally to center, using average of all nodes.
///
Center,
///
- /// Align nodes horizontally to right, matching right-most node
+ /// Align nodes horizontally to right, matching right-most node.
///
Right,
}
-}
\ No newline at end of file
+}
diff --git a/Source/Editor/Surface/VisjectSurface.ContextMenu.cs b/Source/Editor/Surface/VisjectSurface.ContextMenu.cs
index b1af9589b..2cd46593b 100644
--- a/Source/Editor/Surface/VisjectSurface.ContextMenu.cs
+++ b/Source/Editor/Surface/VisjectSurface.ContextMenu.cs
@@ -28,7 +28,7 @@ namespace FlaxEditor.Surface
///
/// The input type to process.
/// Node groups cache that can be used for reusing groups for different nodes.
- /// The cache version number. Can be used to reject any cached data after rebuilt.
+ /// The cache version number. Can be used to reject any cached data after. rebuilt.
public delegate void IterateType(ScriptType scriptType, Dictionary, GroupArchetype> cache, int version);
internal static readonly List Caches = new List(8);
@@ -412,6 +412,7 @@ namespace FlaxEditor.Surface
_cmFormatNodesMenu.Enabled = CanEdit && HasNodesSelection;
_cmFormatNodesConnectionButton = _cmFormatNodesMenu.ContextMenu.AddButton("Auto format", Editor.Instance.Options.Options.Input.NodesAutoFormat, () => { FormatGraph(SelectedNodes); });
+ _cmFormatNodesConnectionButton = _cmFormatNodesMenu.ContextMenu.AddButton("Straighten connections", Editor.Instance.Options.Options.Input.NodesStraightenConnections, () => { StraightenGraphConnections(SelectedNodes); });
_cmFormatNodesMenu.ContextMenu.AddSeparator();
_cmAlignNodesTopButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align top", Editor.Instance.Options.Options.Input.NodesAlignTop, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Top); });
diff --git a/Source/Editor/Surface/VisjectSurface.Formatting.cs b/Source/Editor/Surface/VisjectSurface.Formatting.cs
index 2ff48b290..39ac58242 100644
--- a/Source/Editor/Surface/VisjectSurface.Formatting.cs
+++ b/Source/Editor/Surface/VisjectSurface.Formatting.cs
@@ -1,9 +1,9 @@
+using FlaxEditor.Surface.Elements;
+using FlaxEditor.Surface.Undo;
+using FlaxEngine;
using System;
using System.Collections.Generic;
using System.Linq;
-using FlaxEngine;
-using FlaxEditor.Surface.Elements;
-using FlaxEditor.Surface.Undo;
namespace FlaxEditor.Surface
{
@@ -14,26 +14,26 @@ namespace FlaxEditor.Surface
private class NodeFormattingData
{
///
- /// Starting from 0 at the main nodes
+ /// Starting from 0 at the main nodes.
///
public int Layer;
///
- /// Position in the layer
+ /// Position in the layer.
///
public int Offset;
///
- /// How far the subtree needs to be moved additionally
+ /// How far the subtree needs to be moved additionally.
///
public int SubtreeOffset;
}
///
/// Formats a graph where the nodes can be disjointed.
- /// Uses the Sugiyama method
+ /// Uses the Sugiyama method.
///
- /// List of nodes
+ /// List of nodes.
public void FormatGraph(List nodes)
{
if (nodes.Count <= 1)
@@ -78,9 +78,9 @@ namespace FlaxEditor.Surface
}
///
- /// Formats a graph where all nodes are connected
+ /// Formats a graph where all nodes are connected.
///
- /// List of connected nodes
+ /// List of connected nodes.
protected void FormatConnectedGraph(List nodes)
{
if (nodes.Count <= 1)
@@ -160,11 +160,71 @@ namespace FlaxEditor.Surface
}
///
- /// Assigns a layer to every node
+ /// Straightens every connection between nodes in .
///
- /// The exta node data
- /// The end nodes
- /// The number of the maximum layer
+ /// List of nodes.
+ public void StraightenGraphConnections(List nodes)
+ {
+ if (nodes.Count <= 1)
+ return;
+
+ List undoActions = new List();
+
+ // Only process nodes that have any connection
+ List connectedNodes = nodes.Where(n => n.GetBoxes().Any(b => b.HasAnyConnection)).ToList();
+
+ if (connectedNodes.Count == 0)
+ return;
+
+ for (int i = 0; i < connectedNodes.Count - 1; i++)
+ {
+ SurfaceNode nodeA = connectedNodes[i];
+ List connectedOutputBoxes = nodeA.GetBoxes().Where(b => b.IsOutput && b.HasAnyConnection).ToList();
+
+ for (int j = 0; j < connectedOutputBoxes.Count; j++)
+ {
+ Box boxA = connectedOutputBoxes[j];
+
+ for (int b = 0; b < boxA.Connections.Count; b++)
+ {
+ Box boxB = boxA.Connections[b];
+
+ // Ensure the other node is selected
+ if (!connectedNodes.Contains(boxB.ParentNode))
+ continue;
+
+ // Node with no outgoing connections reached. Advance to next node in list
+ if (boxA == null || boxB == null)
+ continue;
+
+ SurfaceNode nodeB = boxB.ParentNode;
+
+ // Calculate the Y offset needed for nodeB to align boxB's Y to boxA's Y
+ float boxASurfaceY = boxA.PointToParent(this, Float2.Zero).Y;
+ float boxBSurfaceY = boxB.PointToParent(this, Float2.Zero).Y;
+ float deltaY = (boxASurfaceY - boxBSurfaceY) / ViewScale;
+ Float2 delta = new Float2(0f, deltaY);
+
+ nodeB.Location += delta;
+
+ if (Undo != null)
+ undoActions.Add(new MoveNodesAction(Context, new[] { nodeB.ID }, delta));
+ }
+ }
+ }
+
+ if (undoActions.Count > 0)
+ Undo?.AddAction(new MultiUndoAction(undoActions, "Straightned "));
+
+ MarkAsEdited(false);
+ }
+
+ ///
+ /// Assigns a layer to every node.
+ ///
+ /// The exta node data.
+ /// The end nodes.
+ /// The number of the maximum layer.
private int SetLayers(Dictionary nodeData, List endNodes)
{
// Longest path layering
@@ -201,12 +261,12 @@ namespace FlaxEditor.Surface
///
- /// Sets the node offsets
+ /// Sets the node offsets.
///
- /// The exta node data
- /// The end nodes
- /// The number of the maximum layer
- /// The number of the maximum offset
+ /// The exta node data.
+ /// The end nodes.
+ /// The number of the maximum layer.
+ /// The number of the maximum offset.
private int SetOffsets(Dictionary nodeData, List endNodes, int maxLayer)
{
int maxOffset = 0;
@@ -287,10 +347,10 @@ namespace FlaxEditor.Surface
/// Align given nodes on a graph using the given alignment type.
/// Ignores any potential overlap.
///
- /// List of nodes
- /// Alignemnt type
+ /// List of nodes.
+ /// Alignemnt type.
public void AlignNodes(List nodes, NodeAlignmentType alignmentType)
- {
+ {
if(nodes.Count <= 1)
return;
@@ -328,8 +388,8 @@ namespace FlaxEditor.Surface
///
/// Distribute the given nodes as equally as possible inside the bounding box, if no fit can be done it will use a default pad of 10 pixels between nodes.
///
- /// List of nodes
- /// If false will be done horizontally, if true will be done vertically
+ /// List of nodes.
+ /// If false will be done horizontally, if true will be done vertically.
public void DistributeNodes(List nodes, bool vertically)
{
if(nodes.Count <= 1)
diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs
index 1201318d8..8cbcb4a21 100644
--- a/Source/Editor/Surface/VisjectSurface.cs
+++ b/Source/Editor/Surface/VisjectSurface.cs
@@ -416,6 +416,7 @@ namespace FlaxEditor.Surface
new InputActionsContainer.Binding(options => options.Cut, Cut),
new InputActionsContainer.Binding(options => options.Duplicate, Duplicate),
new InputActionsContainer.Binding(options => options.NodesAutoFormat, () => { FormatGraph(SelectedNodes); }),
+ new InputActionsContainer.Binding(options => options.NodesStraightenConnections, () => { StraightenGraphConnections(SelectedNodes); }),
new InputActionsContainer.Binding(options => options.NodesAlignTop, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Top); }),
new InputActionsContainer.Binding(options => options.NodesAlignMiddle, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Middle); }),
new InputActionsContainer.Binding(options => options.NodesAlignBottom, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Bottom); }),
@@ -542,11 +543,12 @@ namespace FlaxEditor.Surface
nodes.Add(context);
context = context.Parent;
}
+ float margin = 1;
float x = NavigationBar.DefaultButtonsMargin;
- float h = toolStrip.ItemsHeight - 2 * ToolStrip.DefaultMarginV;
+ float h = toolStrip.ItemsHeight - 2 * margin;
for (int i = nodes.Count - 1; i >= 0; i--)
{
- var button = new VisjectContextNavigationButton(this, nodes[i].Context, x, ToolStrip.DefaultMarginV, h);
+ var button = new VisjectContextNavigationButton(this, nodes[i].Context, x, margin, h);
button.PerformLayout();
x += button.Width + NavigationBar.DefaultButtonsMargin;
navigationBar.AddChild(button);
diff --git a/Source/Editor/Windows/Assets/VisualScriptWindow.cs b/Source/Editor/Windows/Assets/VisualScriptWindow.cs
index 756daf66a..35b2d927d 100644
--- a/Source/Editor/Windows/Assets/VisualScriptWindow.cs
+++ b/Source/Editor/Windows/Assets/VisualScriptWindow.cs
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Reflection;
using System.Threading;
using System.Xml;
@@ -471,7 +470,7 @@ namespace FlaxEditor.Windows.Assets
private void OnOverrideMethodClicked()
{
- var cm = new ContextMenu();
+ var cm = new ItemsListContextMenu(235);
var window = (VisualScriptWindow)Values[0];
var scriptMeta = window.Asset.Meta;
var baseType = TypeUtils.GetType(scriptMeta.BaseTypename);
@@ -499,27 +498,39 @@ namespace FlaxEditor.Windows.Assets
if (isAlreadyAdded)
continue;
- var cmButton = cm.AddButton($"{name} (in {member.DeclaringType.Name})");
- cmButton.TooltipText = Editor.Instance.CodeDocs.GetTooltip(member);
- cmButton.Clicked += () =>
+ var item = new ItemsListContextMenu.Item
{
- var surface = ((VisualScriptWindow)Values[0]).Surface;
- var surfaceBounds = surface.AllNodesBounds;
- surface.ShowArea(new Rectangle(surfaceBounds.BottomLeft, new Float2(200, 150)).MakeExpanded(400.0f));
- var node = surface.Context.SpawnNode(16, 3, surfaceBounds.BottomLeft + new Float2(0, 50), new object[]
- {
- name,
- parameters.Length,
- Utils.GetEmptyArray()
- });
- surface.Select(node);
+ Name = $"{name} (in {member.DeclaringType.Name})",
+ TooltipText = Editor.Instance.CodeDocs.GetTooltip(member),
+ Tag = new object[] { name, parameters.Length, Utils.GetEmptyArray() },
+ // Do some basic sorting based on if the method is defined directly in the script base class
+ SortScore = member.DeclaringType == member.Type.ReflectedType ? 1 : 0,
};
+ cm.AddItem(item);
}
}
- if (!cm.Items.Any())
+
+ cm.ItemClicked += (item) =>
{
- cm.AddButton("Nothing to override");
+ var surface = ((VisualScriptWindow)Values[0]).Surface;
+ var surfaceBounds = surface.AllNodesBounds;
+ surface.ShowArea(new Rectangle(surfaceBounds.BottomLeft, new Float2(200, 150)).MakeExpanded(400.0f));
+ var node = surface.Context.SpawnNode(16, 3, surfaceBounds.BottomLeft + new Float2(0, 50), item.Tag as object[]);
+ surface.Select(node);
+ };
+
+ if (cm.ItemsPanel.ChildrenCount == 0)
+ {
+ var item = new ItemsListContextMenu.Item
+ {
+ Name = "Nothing to override"
+ };
+ item.Enabled = false;
+
+ cm.AddItem(item);
}
+
+ cm.SortItems();
cm.Show(_overrideButton, new Float2(0, _overrideButton.Height));
}
}
diff --git a/Source/Editor/Windows/ContentWindow.Navigation.cs b/Source/Editor/Windows/ContentWindow.Navigation.cs
index 1033db472..53dd6447b 100644
--- a/Source/Editor/Windows/ContentWindow.Navigation.cs
+++ b/Source/Editor/Windows/ContentWindow.Navigation.cs
@@ -194,17 +194,18 @@ namespace FlaxEditor.Windows
nodes.Add(node);
node = node.ParentNode;
}
+ float margin = 1;
float x = NavigationBar.DefaultButtonsMargin;
- float h = _toolStrip.ItemsHeight - 2 * ToolStrip.DefaultMarginV;
+ float h = _toolStrip.ItemsHeight - 2 * margin;
for (int i = nodes.Count - 1; i >= 0; i--)
{
- var button = new ContentNavigationButton(nodes[i], x, ToolStrip.DefaultMarginV, h);
+ var button = new ContentNavigationButton(nodes[i], x, margin, h);
button.PerformLayout();
x += button.Width + NavigationBar.DefaultButtonsMargin;
_navigationBar.AddChild(button);
if (i > 0)
{
- var separator = new ContentNavigationSeparator(button, x, ToolStrip.DefaultMarginV, h);
+ var separator = new ContentNavigationSeparator(button, x, margin, h);
separator.PerformLayout();
x += separator.Width + NavigationBar.DefaultButtonsMargin;
_navigationBar.AddChild(separator);
@@ -215,6 +216,7 @@ namespace FlaxEditor.Windows
// Update
_navigationBar.IsLayoutLocked = wasLayoutLocked;
_navigationBar.PerformLayout();
+ UpdateNavigationBarBounds();
}
///
diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs
index cf33d0b63..3d2ec4a66 100644
--- a/Source/Editor/Windows/ContentWindow.cs
+++ b/Source/Editor/Windows/ContentWindow.cs
@@ -1016,6 +1016,21 @@ namespace FlaxEditor.Windows
_navigateUpButton.Enabled = folder != null && _tree.SelectedNode != _root;
}
+ private void UpdateNavigationBarBounds()
+ {
+ if (_navigationBar != null && _toolStrip != null)
+ {
+ var bottomPrev = _toolStrip.Bottom;
+ _navigationBar.UpdateBounds(_toolStrip);
+ if (bottomPrev != _toolStrip.Bottom)
+ {
+ // Navigation bar changed toolstrip height
+ _split.Offsets = new Margin(0, 0, _toolStrip.Bottom, 0);
+ PerformLayout();
+ }
+ }
+ }
+
///
public override void OnInit()
{
@@ -1200,9 +1215,9 @@ namespace FlaxEditor.Windows
///
protected override void PerformLayoutBeforeChildren()
{
- base.PerformLayoutBeforeChildren();
+ UpdateNavigationBarBounds();
- _navigationBar?.UpdateBounds(_toolStrip);
+ base.PerformLayoutBeforeChildren();
}
///
diff --git a/Source/Editor/Windows/DebugLogWindow.cs b/Source/Editor/Windows/DebugLogWindow.cs
index ed133d4e8..5b5abf1e2 100644
--- a/Source/Editor/Windows/DebugLogWindow.cs
+++ b/Source/Editor/Windows/DebugLogWindow.cs
@@ -335,12 +335,12 @@ namespace FlaxEditor.Windows
{
Parent = this,
};
- toolstrip.AddButton("Clear", Clear).LinkTooltip("Clears all log entries");
+ toolstrip.AddButton("Clear", Clear).LinkTooltip("Clears all log entries.");
_clearOnPlayButton = (ToolStripButton)toolstrip.AddButton("Clear on Play", () =>
{
editor.Options.Options.Interface.DebugLogClearOnPlay = _clearOnPlayButton.Checked;
editor.Options.Apply(editor.Options.Options);
- }).SetAutoCheck(true).LinkTooltip("Clears all log entries on enter playmode");
+ }).SetAutoCheck(true).LinkTooltip("Clears all log entries on enter playmode.");
_collapseLogsButton = (ToolStripButton)toolstrip.AddButton("Collapse", () =>
{
editor.Options.Options.Interface.DebugLogCollapse = _collapseLogsButton.Checked;
@@ -350,14 +350,14 @@ namespace FlaxEditor.Windows
{
editor.Options.Options.Interface.DebugLogPauseOnError = _pauseOnErrorButton.Checked;
editor.Options.Apply(editor.Options.Options);
- }).SetAutoCheck(true).LinkTooltip("Performs auto pause on error");
+ }).SetAutoCheck(true).LinkTooltip("Performs auto pause on error.");
toolstrip.AddSeparator();
_groupButtons[0] = (ToolStripButton)toolstrip.AddButton(editor.Icons.Error32, () => { OnGroupButtonPressed(0); })
- .SetAutoCheck(true).LinkTooltip("Shows/hides error messages");
+ .SetAutoCheck(true).LinkTooltip("Shows/hides error messages.");
_groupButtons[1] = (ToolStripButton)toolstrip.AddButton(editor.Icons.Warning32, () => { OnGroupButtonPressed(1); })
- .SetAutoCheck(true).LinkTooltip("Shows/hides warning messages");
+ .SetAutoCheck(true).LinkTooltip("Shows/hides warning messages.");
_groupButtons[2] = (ToolStripButton)toolstrip.AddButton(editor.Icons.Info32, () => { OnGroupButtonPressed(2); })
- .SetAutoCheck(true).LinkTooltip("Shows/hides info messages");
+ .SetAutoCheck(true).LinkTooltip("Shows/hides info messages.");
UpdateCount();
// Split panel
@@ -495,6 +495,7 @@ namespace FlaxEditor.Windows
// Pause on Error (we should do it as fast as possible)
if (newEntry.Group == LogGroup.Error && _pauseOnErrorButton.Checked && Editor.StateMachine.CurrentState == Editor.StateMachine.PlayingState)
{
+ Editor.Log("Pause Play mode on error (toggle this behaviour in the Debug Log panel)");
Editor.Simulation.RequestPausePlay();
}
}
diff --git a/Source/Editor/Windows/GameCookerWindow.cs b/Source/Editor/Windows/GameCookerWindow.cs
index 232b51d8d..8aab5d759 100644
--- a/Source/Editor/Windows/GameCookerWindow.cs
+++ b/Source/Editor/Windows/GameCookerWindow.cs
@@ -970,8 +970,9 @@ namespace FlaxEditor.Windows
public void BuildAndRun()
{
Editor.Log("Building and running");
- GameCooker.GetCurrentPlatform(out var platform, out var buildPlatform, out var buildConfiguration);
+ GameCooker.GetCurrentPlatform(out var platform, out var buildPlatform, out _);
var numberOfClients = Editor.Options.Options.Interface.NumberOfGameClientsToLaunch;
+ var buildConfig = Editor.Options.Options.Interface.CookAndRunBuildConfiguration;
for (int i = 0; i < numberOfClients; i++)
{
var buildOptions = BuildOptions.AutoRun;
@@ -984,7 +985,7 @@ namespace FlaxEditor.Windows
{
Output = _buildTabProxy.PerPlatformOptions[platform].Output,
Platform = buildPlatform,
- Mode = buildConfiguration,
+ Mode = buildConfig,
},
Options = buildOptions,
});
@@ -997,8 +998,9 @@ namespace FlaxEditor.Windows
public void RunCooked()
{
Editor.Log("Running cooked build");
- GameCooker.GetCurrentPlatform(out var platform, out var buildPlatform, out var buildConfiguration);
+ GameCooker.GetCurrentPlatform(out var platform, out var buildPlatform, out _);
var numberOfClients = Editor.Options.Options.Interface.NumberOfGameClientsToLaunch;
+ var buildConfig = Editor.Options.Options.Interface.CookAndRunBuildConfiguration;
for (int i = 0; i < numberOfClients; i++)
{
_buildingQueue.Enqueue(new QueueItem
@@ -1007,7 +1009,7 @@ namespace FlaxEditor.Windows
{
Output = _buildTabProxy.PerPlatformOptions[platform].Output,
Platform = buildPlatform,
- Mode = buildConfiguration,
+ Mode = buildConfig,
},
Options = BuildOptions.AutoRun | BuildOptions.NoCook,
});
diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs
index 5b243cfed..080744c2f 100644
--- a/Source/Editor/Windows/GameWindow.cs
+++ b/Source/Editor/Windows/GameWindow.cs
@@ -63,6 +63,16 @@ namespace FlaxEditor.Windows
},
};
+ ///
+ /// Fired when the game window audio is muted.
+ ///
+ public event Action MuteAudio;
+
+ ///
+ /// Fired when the game window master audio volume is changed.
+ ///
+ public event Action MasterVolumeChanged;
+
///
/// Gets the viewport.
///
@@ -120,6 +130,7 @@ namespace FlaxEditor.Windows
{
Audio.MasterVolume = value ? 0 : AudioVolume;
_audioMuted = value;
+ MuteAudio?.Invoke();
}
}
@@ -134,6 +145,7 @@ namespace FlaxEditor.Windows
if (!AudioMuted)
Audio.MasterVolume = value;
_audioVolume = value;
+ MasterVolumeChanged?.Invoke(value);
}
}
diff --git a/Source/Editor/Windows/SplashScreen.cpp b/Source/Editor/Windows/SplashScreen.cpp
index 2700c9694..7e2889ca1 100644
--- a/Source/Editor/Windows/SplashScreen.cpp
+++ b/Source/Editor/Windows/SplashScreen.cpp
@@ -9,6 +9,7 @@
#include "Engine/Render2D/Font.h"
#include "Engine/Render2D/TextLayoutOptions.h"
#include "Engine/Render2D/Render2D.h"
+#include "Engine/Platform/FileSystem.h"
#include "Engine/Content/Content.h"
#include "FlaxEngine.Gen.h"
@@ -188,8 +189,7 @@ void SplashScreen::Show()
// Setup
_dpiScale = dpiScale;
- _width = settings.Size.X;
- _height = settings.Size.Y;
+ _size = settings.Size;
_startTime = DateTime::NowUTC();
auto str = Globals::ProjectFolder;
#if PLATFORM_WIN32
@@ -214,6 +214,12 @@ void SplashScreen::Show()
font->OnLoaded.Bind(this);
}
+ // Load custom image
+ _splashTexture.Loaded.Bind(this);
+ String splashImagePath = Globals::ProjectContentFolder / TEXT("SplashImage.flax");
+ if (FileSystem::FileExists(splashImagePath))
+ _splashTexture = Content::LoadAsync(splashImagePath);
+
_window->Show();
}
@@ -227,6 +233,10 @@ void SplashScreen::Close()
// Close window
_window->Close(ClosingReason::CloseEvent);
_window = nullptr;
+
+ _titleFont = nullptr;
+ _subtitleFont = nullptr;
+ _splashTexture = nullptr;
}
void SplashScreen::OnShown()
@@ -239,16 +249,29 @@ void SplashScreen::OnShown()
void SplashScreen::OnDraw()
{
const float s = _dpiScale;
- const float width = _width;
- const float height = _height;
+ const float width = _size.X;
+ const float height = _size.Y;
// Peek time
const float time = static_cast((DateTime::NowUTC() - _startTime).GetTotalSeconds());
// Background
- const float lightBarHeight = 112 * s;
- Render2D::FillRectangle(Rectangle(0, 0, width, 150 * s), Color::FromRGB(0x1C1C1C));
- Render2D::FillRectangle(Rectangle(0, lightBarHeight, width, height), Color::FromRGB(0x0C0C0C));
+ float lightBarHeight = 112 * s;
+ if (_splashTexture != nullptr)
+ {
+ if (_splashTexture->IsLoaded())
+ {
+ lightBarHeight = height - lightBarHeight + 20 * s;
+ Render2D::DrawTexture(_splashTexture, Rectangle(0, 0, width, height));
+ Color rectColor = Color::FromRGB(0x0C0C0C);
+ Render2D::FillRectangle(Rectangle(0, lightBarHeight, width, height - lightBarHeight),rectColor.AlphaMultiplied(0.85f), rectColor.AlphaMultiplied(0.85f), rectColor, rectColor);
+ }
+ }
+ else
+ {
+ Render2D::FillRectangle(Rectangle(0, 0, width, 150 * s), Color::FromRGB(0x1C1C1C));
+ Render2D::FillRectangle(Rectangle(0, lightBarHeight, width, height), Color::FromRGB(0x0C0C0C));
+ }
// Animated border
const float anim = Math::Sin(time * 4.0f) * 0.5f + 0.5f;
@@ -276,15 +299,27 @@ void SplashScreen::OnDraw()
for (int32 i = 0; i < 4 - static_cast(time * 2.0f) % 4; i++)
subtitle += TEXT(' ');
}
- layout.Bounds = Rectangle(width - 224 * s, lightBarHeight - 39 * s, 220 * s, 35 * s);
+ if (_splashTexture != nullptr)
+ {
+ layout.Bounds = Rectangle(width - 224 * s, lightBarHeight + 4 * s, 220 * s, 35 * s);
+ layout.VerticalAlignment = TextAlignment::Near;
+ }
+ else
+ {
+ layout.Bounds = Rectangle(width - 224 * s, lightBarHeight - 39 * s, 220 * s, 35 * s);
+ layout.VerticalAlignment = TextAlignment::Far;
+ }
layout.Scale = 1.0f;
layout.HorizontalAlignment = TextAlignment::Far;
- layout.VerticalAlignment = TextAlignment::Far;
Render2D::DrawText(_subtitleFont, subtitle, Color::FromRGB(0x8C8C8C), layout);
// Additional info
const float infoMargin = 6 * s;
- layout.Bounds = Rectangle(infoMargin, lightBarHeight + infoMargin, width - (2 * infoMargin), height - lightBarHeight - (2 * infoMargin));
+ if (_splashTexture != nullptr)
+ layout.Bounds = Rectangle(infoMargin + 4 * s, lightBarHeight + infoMargin, width - (2 * infoMargin), height - lightBarHeight - (2 * infoMargin));
+ else
+ layout.Bounds = Rectangle(infoMargin, lightBarHeight + infoMargin, width - (2 * infoMargin), height - lightBarHeight - (2 * infoMargin));
+
layout.HorizontalAlignment = TextAlignment::Near;
layout.VerticalAlignment = TextAlignment::Center;
Render2D::DrawText(_subtitleFont, _infoText, Color::FromRGB(0xFFFFFF) * 0.9f, layout);
@@ -307,3 +342,14 @@ void SplashScreen::OnFontLoaded(Asset* asset)
_titleFont = font->CreateFont(35 * s);
_subtitleFont = font->CreateFont(9 * s);
}
+
+void SplashScreen::OnSplashLoaded()
+{
+ // Resize window to be larger if texture is being used
+ auto desktopSize = Platform::GetDesktopSize();
+ auto xSize = (desktopSize.X / (600.0f * 3.0f)) * 600.0f;
+ auto ySize = (desktopSize.Y / (200.0f * 3.0f)) * 200.0f;
+ _window->SetClientSize(Float2(xSize, ySize));
+ _size = _window->GetSize();
+ _window->SetPosition((desktopSize - _size) * 0.5f);
+}
diff --git a/Source/Editor/Windows/SplashScreen.h b/Source/Editor/Windows/SplashScreen.h
index e8afcb97f..6f8f17a7f 100644
--- a/Source/Editor/Windows/SplashScreen.h
+++ b/Source/Editor/Windows/SplashScreen.h
@@ -2,6 +2,8 @@
#pragma once
+#include "Engine/Content/Assets/Texture.h"
+#include "Engine/Content/AssetReference.h"
#include "Engine/Core/Types/DateTime.h"
#include "Engine/Platform/Window.h"
@@ -18,10 +20,12 @@ private:
Window* _window = nullptr;
Font* _titleFont = nullptr;
Font* _subtitleFont = nullptr;
+ AssetReference _splashTexture;
String _title;
DateTime _startTime;
String _infoText;
- float _dpiScale, _width, _height;
+ float _dpiScale;
+ Float2 _size;
StringView _quote;
public:
@@ -78,4 +82,5 @@ private:
void OnDraw();
bool HasLoadedFonts() const;
void OnFontLoaded(Asset* asset);
+ void OnSplashLoaded();
};
diff --git a/Source/Editor/Windows/ToolboxWindow.cs b/Source/Editor/Windows/ToolboxWindow.cs
index 3c9c605a4..7ad66794d 100644
--- a/Source/Editor/Windows/ToolboxWindow.cs
+++ b/Source/Editor/Windows/ToolboxWindow.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Input;
using FlaxEditor.GUI.Tabs;
using FlaxEditor.GUI.Tree;
@@ -98,10 +99,22 @@ namespace FlaxEditor.Windows
}
}
+ [Flags]
+ private enum SearchFilter
+ {
+ UI = 1,
+ Actors = 2,
+ Primitives = 4,
+ [HideInEditor]
+ Default = UI | Actors | Primitives,
+ }
+
private TextBox _searchBox;
private ContainerControl _groupSearch;
private Tabs _actorGroups;
private ContainerControl groupPrimitives;
+ private Button _viewDropdown;
+ private int _searchFilterMask = (int)SearchFilter.Default;
///
/// The editor instance.
@@ -127,16 +140,25 @@ namespace FlaxEditor.Windows
UseScroll = true,
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
- TabsSize = new Float2(120, 32),
+ TabsSize = new Float2(90, 32),
Parent = this,
};
_groupSearch = CreateGroupWithList(_actorGroups, "Search", 26);
+
+ _viewDropdown = new Button(2, 2, 45.0f, TextBoxBase.DefaultHeight)
+ {
+ TooltipText = "Change search filter options.",
+ Text = "Filters",
+ Parent = _groupSearch.Parent.Parent,
+ };
+ _viewDropdown.Clicked += OnViewButtonClicked;
+
_searchBox = new SearchBox
{
AnchorPreset = AnchorPresets.HorizontalStretchTop,
Parent = _groupSearch.Parent.Parent,
- Bounds = new Rectangle(4, 4, _actorGroups.Width - 8, 18),
+ Bounds = new Rectangle(_viewDropdown.Right + 2, 2, _actorGroups.Width - 4, TextBoxBase.DefaultHeight),
};
_searchBox.TextChanged += OnSearchBoxTextChanged;
@@ -145,10 +167,38 @@ namespace FlaxEditor.Windows
_actorGroups.SelectedTabIndex = 1;
}
+ private void OnViewButtonClicked()
+ {
+ var menu = new ContextMenu();
+ AddSearchFilterButton(menu, SearchFilter.UI, "UI");
+ AddSearchFilterButton(menu, SearchFilter.Actors, "Actors");
+ AddSearchFilterButton(menu, SearchFilter.Primitives, "Primitives");
+ menu.Show(_viewDropdown.Parent, _viewDropdown.BottomLeft);
+ }
+
+ private void AddSearchFilterButton(ContextMenu menu, SearchFilter value, string name)
+ {
+ var button = menu.AddButton(name);
+ button.AutoCheck = true;
+ button.Checked = (_searchFilterMask & (int)value) != 0;
+ button.Clicked += () =>
+ {
+ _searchFilterMask ^= (int)value;
+ OnSearchBoxTextChanged();
+ };
+ }
+
+ ///
+ protected override void PerformLayoutBeforeChildren()
+ {
+ base.PerformLayoutBeforeChildren();
+
+ _searchBox.Width = _groupSearch.Width - _viewDropdown.Right - 4;
+ }
+
private void OnScriptsReload()
{
// Prevent any references to actor types from the game assemblies that will be reloaded
- _searchBox.Clear();
_groupSearch.DisposeChildren();
_groupSearch.PerformLayout();
@@ -172,6 +222,7 @@ namespace FlaxEditor.Windows
private void OnScriptsReloadEnd()
{
RefreshActorTabs();
+ OnSearchBoxTextChanged();
}
private void RefreshActorTabs()
@@ -192,14 +243,21 @@ namespace FlaxEditor.Windows
group.Dispose();
}
- // Setup primitives tabs
+ // Add primitives to primtives and search tab
groupPrimitives = CreateGroupWithList(_actorGroups, "Primitives");
+
groupPrimitives.AddChild(CreateEditorAssetItem("Cube", "Primitives/Cube.flax"));
+ _groupSearch.AddChild(CreateEditorAssetItem("Cube", "Primitives/Cube.flax"));
groupPrimitives.AddChild(CreateEditorAssetItem("Sphere", "Primitives/Sphere.flax"));
+ _groupSearch.AddChild(CreateEditorAssetItem("Sphere", "Primitives/Sphere.flax"));
groupPrimitives.AddChild(CreateEditorAssetItem("Plane", "Primitives/Plane.flax"));
+ _groupSearch.AddChild(CreateEditorAssetItem("Plane", "Primitives/Plane.flax"));
groupPrimitives.AddChild(CreateEditorAssetItem("Cylinder", "Primitives/Cylinder.flax"));
+ _groupSearch.AddChild(CreateEditorAssetItem("Cylinder", "Primitives/Cylinder.flax"));
groupPrimitives.AddChild(CreateEditorAssetItem("Cone", "Primitives/Cone.flax"));
+ _groupSearch.AddChild(CreateEditorAssetItem("Cone", "Primitives/Cone.flax"));
groupPrimitives.AddChild(CreateEditorAssetItem("Capsule", "Primitives/Capsule.flax"));
+ _groupSearch.AddChild(CreateEditorAssetItem("Capsule", "Primitives/Capsule.flax"));
// Created first to order specific tabs
CreateGroupWithList(_actorGroups, "Lights");
@@ -312,57 +370,115 @@ namespace FlaxEditor.Windows
_groupSearch.LockChildrenRecursive();
_groupSearch.DisposeChildren();
- foreach (var actorType in Editor.CodeEditing.Actors.Get())
+ if (((int)SearchFilter.Actors & _searchFilterMask) != 0)
{
- ActorToolboxAttribute attribute = null;
- foreach (var e in actorType.GetAttributes(false))
+ foreach (var actorType in Editor.CodeEditing.Actors.Get())
{
- if (e is ActorToolboxAttribute actorToolboxAttribute)
+ ActorToolboxAttribute attribute = null;
+ foreach (var e in actorType.GetAttributes(false))
{
- attribute = actorToolboxAttribute;
- break;
+ if (e is ActorToolboxAttribute actorToolboxAttribute)
+ {
+ attribute = actorToolboxAttribute;
+ break;
+ }
}
- }
- var text = (attribute == null) ? actorType.Name : string.IsNullOrEmpty(attribute.Name) ? actorType.Name : attribute.Name;
+ var text = (attribute == null) ? actorType.Name : string.IsNullOrEmpty(attribute.Name) ? actorType.Name : attribute.Name;
- // Display all actors on no search
- if (string.IsNullOrEmpty(filterText))
- _groupSearch.AddChild(CreateActorItem(Utilities.Utils.GetPropertyNameUI(text), actorType));
-
- if (!QueryFilterHelper.Match(filterText, text, out QueryFilterHelper.Range[] ranges))
- continue;
-
- var item = CreateActorItem(Utilities.Utils.GetPropertyNameUI(text), actorType);
- SearchFilterHighlights(item, text, ranges);
- }
-
- // Hack primitive models into the search results
- foreach (var child in groupPrimitives.Children)
- {
- if (child is Item primitiveAssetItem)
- {
- var text = primitiveAssetItem.Text;
+ // Display all actors on no search
+ if (string.IsNullOrEmpty(filterText))
+ _groupSearch.AddChild(CreateActorItem(Utilities.Utils.GetPropertyNameUI(text), actorType));
if (!QueryFilterHelper.Match(filterText, text, out QueryFilterHelper.Range[] ranges))
continue;
- // Rebuild the path based on item name (it would be better to convert the drag data back to a string somehow)
- string path = $"Primitives/{text}.flax";
-
- var item = CreateEditorAssetItem(text, path);
+ var item = CreateActorItem(Utilities.Utils.GetPropertyNameUI(text), actorType);
SearchFilterHighlights(item, text, ranges);
}
}
- if (string.IsNullOrEmpty(filterText))
- _groupSearch.SortChildren();
+ if (((int)SearchFilter.Primitives & _searchFilterMask) != 0)
+ {
+ // Hack primitive models into the search results
+ foreach (var child in groupPrimitives.Children)
+ {
+ if (child is Item primitiveAssetItem)
+ {
+ var text = primitiveAssetItem.Text;
+
+ // Rebuild the path based on item name (it would be better to convert the drag data back to a string somehow)
+ string path = $"Primitives/{text}.flax";
+
+ // Display all primitives on no search
+ if (string.IsNullOrEmpty(filterText))
+ _groupSearch.AddChild(CreateEditorAssetItem(text, path));
+
+ if (!QueryFilterHelper.Match(filterText, text, out QueryFilterHelper.Range[] ranges))
+ continue;
+
+ var item = CreateEditorAssetItem(text, path);
+ SearchFilterHighlights(item, text, ranges);
+ }
+ }
+ }
+
+ if (((int)SearchFilter.UI & _searchFilterMask) != 0)
+ {
+ foreach (var controlType in Editor.Instance.CodeEditing.Controls.Get())
+ {
+ if (controlType.IsAbstract)
+ continue;
+
+ ActorToolboxAttribute attribute = null;
+ foreach (var e in controlType.GetAttributes(false))
+ {
+ if (e is ActorToolboxAttribute actorToolboxAttribute)
+ {
+ attribute = actorToolboxAttribute;
+ break;
+ }
+ }
+
+ var text = (attribute == null) ? controlType.Name : string.IsNullOrEmpty(attribute.Name) ? controlType.Name : attribute.Name;
+
+ // Display all controls on no search
+ if (string.IsNullOrEmpty(filterText))
+ _groupSearch.AddChild(CreateControlItem(Utilities.Utils.GetPropertyNameUI(controlType.Name), controlType));
+
+ if (!QueryFilterHelper.Match(filterText, text, out QueryFilterHelper.Range[] ranges))
+ continue;
+
+ var item = CreateControlItem(Utilities.Utils.GetPropertyNameUI(controlType.Name), controlType);
+ SearchFilterHighlights(item, text, ranges);
+ }
+ }
+
+ // Sort the search results alphabetically
+ _groupSearch.SortChildren();
_groupSearch.UnlockChildrenRecursive();
PerformLayout();
PerformLayout();
}
+ ///
+ public override void Draw()
+ {
+ base.Draw();
+
+ // Show a text to hint the user that either no filter is active or the search does not return any results
+ bool noSearchResults = _groupSearch.Children.Count == 0 && !string.IsNullOrEmpty(_searchBox.Text);
+ bool showHint = _searchFilterMask == 0 || noSearchResults;
+ if (showHint)
+ {
+ string hint = noSearchResults ? "No results" : "No search filter active, please enable at least one filter";
+ var textRect = _groupSearch.Parent.Parent.Bounds;
+ var style = Style.Current;
+ Render2D.DrawText(style.FontMedium, hint, textRect, style.ForegroundGrey, TextAlignment.Center, TextAlignment.Center, TextWrapping.WrapWords);
+ }
+ }
+
private void SearchFilterHighlights(Item item, string text, QueryFilterHelper.Range[] ranges)
{
_groupSearch.AddChild(item);
diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
index c42b500fd..f75a8abd1 100644
--- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
+++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
@@ -229,18 +229,12 @@ void AnimGraphExecutor::ProcessAnimEvents(AnimGraphNode* node, bool loop, float
auto& context = *Context.Get();
float eventTimeMin = animPrevPos;
float eventTimeMax = animPos;
- if (loop && context.DeltaTime * speed < 0)
+
+ if (eventTimeMin > eventTimeMax)
{
- // Check if animation looped (for anim events shooting during backwards playback)
- //const float posNotLooped = startTimePos + oldTimePos;
- //if (posNotLooped < 0.0f || posNotLooped > length)
- //const int32 animPosCycle = Math::CeilToInt(animPos / anim->GetDuration());
- //const int32 animPrevPosCycle = Math::CeilToInt(animPrevPos / anim->GetDuration());
- //if (animPosCycle != animPrevPosCycle)
- {
- Swap(eventTimeMin, eventTimeMax);
- }
+ Swap(eventTimeMin, eventTimeMax);
}
+
const float eventTime = (float)(animPos / anim->Data.FramesPerSecond);
const float eventDeltaTime = (float)((animPos - animPrevPos) / anim->Data.FramesPerSecond);
for (const auto& track : anim->Events)
@@ -251,7 +245,13 @@ void AnimGraphExecutor::ProcessAnimEvents(AnimGraphNode* node, bool loop, float
continue;
const float duration = k.Value.Duration > 1 ? k.Value.Duration : 0.0f;
#define ADD_OUTGOING_EVENT(type) context.Data->OutgoingEvents.Add({ k.Value.Instance, (AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime, AnimGraphInstanceData::OutgoingEvent::type })
- if (k.Time <= eventTimeMax && eventTimeMin <= k.Time + duration)
+ if ((k.Time <= eventTimeMax && eventTimeMin <= k.Time + duration
+ && (Math::FloorToInt(animPos) != 0 && Math::CeilToInt(animPrevPos) != Math::CeilToInt(anim->GetDuration()) && Math::FloorToInt(animPrevPos) != 0 && Math::CeilToInt(animPos) != Math::CeilToInt(anim->GetDuration())))
+ // Handle the edge case of an event on 0 or on max animation duration during looping
+ || (loop && duration == 0.0f && Math::CeilToInt(animPos) == Math::CeilToInt(anim->GetDuration()) && k.Time == anim->GetDuration())
+ || (loop && Math::FloorToInt(animPos) == 0 && Math::CeilToInt(animPrevPos) == Math::CeilToInt(anim->GetDuration()) && k.Time == 0.0f)
+ || (loop && Math::FloorToInt(animPrevPos) == 0 && Math::CeilToInt(animPos) == Math::CeilToInt(anim->GetDuration()) && k.Time == 0.0f)
+ )
{
int32 stateIndex = -1;
if (duration > 1)
diff --git a/Source/Engine/Content/Assets/Animation.cpp b/Source/Engine/Content/Assets/Animation.cpp
index 8558e601d..93397397a 100644
--- a/Source/Engine/Content/Assets/Animation.cpp
+++ b/Source/Engine/Content/Assets/Animation.cpp
@@ -532,7 +532,7 @@ bool Animation::SaveHeader(const ModelData& modelData, WriteStream& stream, int3
// Nested animations
stream.WriteInt32(0); // Empty list
- return false;
+ return stream.HasError();
}
void Animation::GetReferences(Array& assets, Array& files) const
diff --git a/Source/Engine/Content/Assets/Model.cpp b/Source/Engine/Content/Assets/Model.cpp
index 47335ae1f..1830f40de 100644
--- a/Source/Engine/Content/Assets/Model.cpp
+++ b/Source/Engine/Content/Assets/Model.cpp
@@ -398,7 +398,7 @@ bool Model::LoadHeader(ReadStream& stream, byte& headerVersion)
}
}
- return false;
+ return stream.HasError();
}
#if USE_EDITOR
@@ -457,7 +457,7 @@ bool Model::SaveHeader(WriteStream& stream, const ModelData& modelData)
}
}
- return false;
+ return stream.HasError();
}
bool Model::Save(bool withMeshDataFromGpu, Function& getChunk) const
diff --git a/Source/Engine/Content/Assets/ModelBase.cpp b/Source/Engine/Content/Assets/ModelBase.cpp
index 2521966a2..0946e6160 100644
--- a/Source/Engine/Content/Assets/ModelBase.cpp
+++ b/Source/Engine/Content/Assets/ModelBase.cpp
@@ -332,7 +332,7 @@ bool ModelBase::LoadHeader(ReadStream& stream, byte& headerVersion)
stream.Read(slot.Name, 11);
}
- return false;
+ return stream.HasError();
}
bool ModelBase::LoadMesh(MemoryReadStream& stream, byte meshVersion, MeshBase* mesh, MeshData* dataIfReadOnly)
diff --git a/Source/Engine/Content/Assets/SkinnedModel.cpp b/Source/Engine/Content/Assets/SkinnedModel.cpp
index 9d10e0150..ed41c4aab 100644
--- a/Source/Engine/Content/Assets/SkinnedModel.cpp
+++ b/Source/Engine/Content/Assets/SkinnedModel.cpp
@@ -661,7 +661,7 @@ bool SkinnedModel::LoadHeader(ReadStream& stream, byte& headerVersion)
}
}
- return false;
+ return stream.HasError();
}
#if USE_EDITOR
@@ -691,7 +691,7 @@ bool SkinnedModel::SaveHeader(WriteStream& stream) const
const int32 blendShapes = mesh.BlendShapes.Count();
stream.Write((uint16)blendShapes);
for (const auto& blendShape : mesh.BlendShapes)
- blendShape.Save(stream);
+ blendShape.SaveHeader(stream);
}
}
diff --git a/Source/Engine/ContentImporters/ImportModel.cpp b/Source/Engine/ContentImporters/ImportModel.cpp
index d2b4256b3..be71236ab 100644
--- a/Source/Engine/ContentImporters/ImportModel.cpp
+++ b/Source/Engine/ContentImporters/ImportModel.cpp
@@ -55,6 +55,27 @@ bool ImportModel::TryGetImportOptions(const StringView& path, Options& options)
}
}
}
+ else
+ {
+ // Try model prefab
+ String pathPrefab = String(StringUtils::GetPathWithoutExtension(path)) + DEFAULT_PREFAB_EXTENSION_DOT;
+ if (FileSystem::FileExists(pathPrefab))
+ {
+ auto prefab = Content::Load(pathPrefab);
+ if (prefab)
+ {
+ for (const auto& e : prefab->ObjectsDataCache)
+ {
+ auto importOptionsMember = e.Value->FindMember("ImportOptions");
+ if (importOptionsMember != e.Value->MemberEnd() && importOptionsMember->value.IsObject())
+ {
+ options.Deserialize(*(ISerializable::DeserializeStream*)&importOptionsMember->value, nullptr);
+ return true;
+ }
+ }
+ }
+ }
+ }
return false;
}
diff --git a/Source/Engine/Core/Types/LayersMask.cs b/Source/Engine/Core/Types/LayersMask.cs
index a6bd4bcab..7d5c57066 100644
--- a/Source/Engine/Core/Types/LayersMask.cs
+++ b/Source/Engine/Core/Types/LayersMask.cs
@@ -32,13 +32,53 @@ namespace FlaxEngine
///
/// Determines whether the specified layer is set in the mask.
///
- /// Name of the layer (from layers settings).
+ /// Name of the layer (from Layers settings).
/// true if the specified layer is set; otherwise, false.
public bool HasLayer(string layerName)
{
return HasLayer(Level.GetLayerIndex(layerName));
}
+ ///
+ /// Gets a layer mask based on a specific layer names.
+ ///
+ /// The names of the layers (from Layers settings).
+ /// A layer mask with the mask set to the layers found. Returns a mask with 0 if not found.
+ public static LayersMask GetMask(params string[] layerNames)
+ {
+ LayersMask mask = new LayersMask();
+ foreach (var layerName in layerNames)
+ {
+ // Ignore blank entries
+ if (string.IsNullOrEmpty(layerName))
+ continue;
+ int index = Level.GetLayerIndex(layerName);
+ if (index != -1)
+ mask.Mask |= (uint)(1 << index);
+ }
+ return mask;
+ }
+
+ ///
+ /// Gets the layer index based on the layer name.
+ ///
+ /// The name of the layer.
+ /// The index if found, otherwise, returns -1.
+ public static int GetLayerIndex(string layerName)
+ {
+ return Level.GetLayerIndex(layerName);
+ }
+
+ ///
+ /// Gets the layer name based on the layer index.
+ ///
+ /// The index of the layer.
+ /// The name of the layer if found, otherwise, a blank string.
+ public static string GetLayerName(int layerIndex)
+ {
+ return Level.GetLayerName(layerIndex);
+ }
+
///
/// Adds two masks.
///
diff --git a/Source/Engine/Core/Types/LayersMask.h b/Source/Engine/Core/Types/LayersMask.h
index 3643af19e..25edbc05b 100644
--- a/Source/Engine/Core/Types/LayersMask.h
+++ b/Source/Engine/Core/Types/LayersMask.h
@@ -27,13 +27,30 @@ public:
{
}
+ ///
+ /// Determines whether the specified layer index is set in the mask.
+ ///
+ /// Index of the layer (zero-based).
+ /// true if the specified layer is set; otherwise, false.
FORCE_INLINE bool HasLayer(int32 layerIndex) const
{
return (Mask & (1 << layerIndex)) != 0;
}
+ ///
+ /// Determines whether the specified layer name is set in the mask.
+ ///
+ /// Name of the layer (from Layers settings).
+ /// true if the specified layer is set; otherwise, false.
bool HasLayer(const StringView& layerName) const;
+ ///
+ /// Gets a layers mask from a specific layer name.
+ ///
+ /// The names of the layers (from Layers settings).
+ /// A layers mask with the Mask set to the same Mask as the layer name passed in. Returns a LayersMask with a mask of 0 if no layer found.
+ static LayersMask GetMask(Span layerNames);
+
operator uint32() const
{
return Mask;
diff --git a/Source/Engine/Core/Types/String.cpp b/Source/Engine/Core/Types/String.cpp
index 112434426..ff0e19eba 100644
--- a/Source/Engine/Core/Types/String.cpp
+++ b/Source/Engine/Core/Types/String.cpp
@@ -4,7 +4,7 @@
#include "StringView.h"
#include "Engine/Core/Collections/Array.h"
-String String::Empty;
+const String String::Empty;
String::String(const StringAnsi& str)
{
diff --git a/Source/Engine/Core/Types/String.h b/Source/Engine/Core/Types/String.h
index 4578ac0b3..0630ff6e3 100644
--- a/Source/Engine/Core/Types/String.h
+++ b/Source/Engine/Core/Types/String.h
@@ -548,7 +548,7 @@ public:
///
/// Instance of the empty string.
///
- static String Empty;
+ static const String Empty;
public:
///
diff --git a/Source/Engine/Core/Types/StringView.cpp b/Source/Engine/Core/Types/StringView.cpp
index 505d9ec05..168a537c1 100644
--- a/Source/Engine/Core/Types/StringView.cpp
+++ b/Source/Engine/Core/Types/StringView.cpp
@@ -9,7 +9,7 @@ StringView StringBuilder::ToStringView() const
return StringView(_data.Get(), _data.Count());
}
-StringView StringView::Empty;
+const StringView StringView::Empty;
StringView::StringView(const String& str)
: StringViewBase(str.Get(), str.Length())
diff --git a/Source/Engine/Core/Types/StringView.h b/Source/Engine/Core/Types/StringView.h
index cbc221e7b..b1025fc91 100644
--- a/Source/Engine/Core/Types/StringView.h
+++ b/Source/Engine/Core/Types/StringView.h
@@ -227,7 +227,7 @@ public:
///
/// Instance of the empty string.
///
- static StringView Empty;
+ static const StringView Empty;
public:
///
diff --git a/Source/Engine/Graphics/Materials/DecalMaterialShader.cpp b/Source/Engine/Graphics/Materials/DecalMaterialShader.cpp
index e93338604..ffc8fa241 100644
--- a/Source/Engine/Graphics/Materials/DecalMaterialShader.cpp
+++ b/Source/Engine/Graphics/Materials/DecalMaterialShader.cpp
@@ -16,7 +16,7 @@
PACK_STRUCT(struct DecalMaterialShaderData {
Matrix WorldMatrix;
Matrix InvWorld;
- Matrix SVPositionToWorld;
+ Matrix SvPositionToWorld;
});
DrawPass DecalMaterialShader::GetDrawModes() const
@@ -47,7 +47,9 @@ void DecalMaterialShader::Bind(BindParameters& params)
MaterialParams::Bind(params.ParamsLink, bindMeta);
// Decals use depth buffer to draw on top of the objects
- context->BindSR(0, GET_TEXTURE_VIEW_SAFE(params.RenderContext.Buffers->DepthBuffer));
+ GPUTexture* depthBuffer = params.RenderContext.Buffers->DepthBuffer;
+ GPUTextureView* depthBufferView = EnumHasAnyFlags(depthBuffer->Flags(), GPUTextureFlags::ReadOnlyDepthView) ? depthBuffer->ViewReadOnlyDepth() : depthBuffer->View();
+ context->BindSR(0, depthBufferView);
// Setup material constants
{
@@ -65,7 +67,7 @@ void DecalMaterialShader::Bind(BindParameters& params)
0, 0, 1, 0,
-1.0f, 1.0f, 0, 1);
const Matrix svPositionToWorld = offsetMatrix * view.IVP;
- Matrix::Transpose(svPositionToWorld, materialData->SVPositionToWorld);
+ Matrix::Transpose(svPositionToWorld, materialData->SvPositionToWorld);
}
// Bind constants
@@ -90,16 +92,20 @@ void DecalMaterialShader::Unload()
bool DecalMaterialShader::Load()
{
GPUPipelineState::Description psDesc0 = GPUPipelineState::Description::DefaultNoDepth;
- psDesc0.VS = _shader->GetVS("VS_Decal");
+ psDesc0.VS = _shader->GetVS("VS_Decal"); // TODO: move VS_Decal to be shared (eg. in GBuffer.shader)
if (psDesc0.VS == nullptr)
return true;
psDesc0.PS = _shader->GetPS("PS_Decal");
psDesc0.CullMode = CullMode::Normal;
+ if (GPUDevice::Instance->Limits.HasReadOnlyDepth)
+ {
+ psDesc0.DepthEnable = true;
+ psDesc0.DepthWriteEnable = false;
+ }
switch (_info.DecalBlendingMode)
{
case MaterialDecalBlendingMode::Translucent:
- {
psDesc0.BlendMode.BlendEnable = true;
psDesc0.BlendMode.SrcBlend = BlendingMode::Blend::SrcAlpha;
psDesc0.BlendMode.DestBlend = BlendingMode::Blend::InvSrcAlpha;
@@ -107,9 +113,7 @@ bool DecalMaterialShader::Load()
psDesc0.BlendMode.DestBlendAlpha = BlendingMode::Blend::One;
psDesc0.BlendMode.RenderTargetWriteMask = BlendingMode::ColorWrite::RGB;
break;
- }
case MaterialDecalBlendingMode::Stain:
- {
psDesc0.BlendMode.BlendEnable = true;
psDesc0.BlendMode.SrcBlend = BlendingMode::Blend::DestColor;
psDesc0.BlendMode.DestBlend = BlendingMode::Blend::InvSrcAlpha;
@@ -117,9 +121,7 @@ bool DecalMaterialShader::Load()
psDesc0.BlendMode.DestBlendAlpha = BlendingMode::Blend::One;
psDesc0.BlendMode.RenderTargetWriteMask = BlendingMode::ColorWrite::RGB;
break;
- }
case MaterialDecalBlendingMode::Normal:
- {
psDesc0.BlendMode.BlendEnable = true;
psDesc0.BlendMode.SrcBlend = BlendingMode::Blend::SrcAlpha;
psDesc0.BlendMode.DestBlend = BlendingMode::Blend::InvSrcAlpha;
@@ -127,13 +129,10 @@ bool DecalMaterialShader::Load()
psDesc0.BlendMode.DestBlendAlpha = BlendingMode::Blend::One;
psDesc0.BlendMode.RenderTargetWriteMask = BlendingMode::ColorWrite::RGB;
break;
- }
case MaterialDecalBlendingMode::Emissive:
- {
psDesc0.BlendMode = BlendingMode::Additive;
break;
}
- }
_cache.Outside = GPUDevice::Instance->CreatePipelineState();
if (_cache.Outside->Init(psDesc0))
@@ -143,6 +142,7 @@ bool DecalMaterialShader::Load()
}
psDesc0.CullMode = CullMode::Inverted;
+ psDesc0.DepthEnable = false;
_cache.Inside = GPUDevice::Instance->CreatePipelineState();
if (_cache.Inside->Init(psDesc0))
{
diff --git a/Source/Engine/Graphics/Materials/GUIMaterialShader.cpp b/Source/Engine/Graphics/Materials/GUIMaterialShader.cpp
index 0811570ea..874694702 100644
--- a/Source/Engine/Graphics/Materials/GUIMaterialShader.cpp
+++ b/Source/Engine/Graphics/Materials/GUIMaterialShader.cpp
@@ -23,6 +23,8 @@ PACK_STRUCT(struct GUIMaterialShaderData {
Float4 ViewInfo;
Float4 ScreenSize;
Float4 ViewSize;
+ Float3 ViewPadding0;
+ float UnscaledTimeParam;
});
void GUIMaterialShader::Bind(BindParameters& params)
@@ -55,7 +57,8 @@ void GUIMaterialShader::Bind(BindParameters& params)
materialData->ViewPos = Float3::Zero;
materialData->ViewFar = 0.0f;
materialData->ViewDir = Float3::Forward;
- materialData->TimeParam = params.TimeParam;
+ materialData->TimeParam = params.Time;
+ materialData->UnscaledTimeParam = params.UnscaledTime;
materialData->ViewInfo = Float4::Zero;
auto& viewport = Render2D::GetViewport();
materialData->ScreenSize = Float4(viewport.Width, viewport.Height, 1.0f / viewport.Width, 1.0f / viewport.Height);
diff --git a/Source/Engine/Graphics/Materials/IMaterial.h b/Source/Engine/Graphics/Materials/IMaterial.h
index 186be0830..23d06589b 100644
--- a/Source/Engine/Graphics/Materials/IMaterial.h
+++ b/Source/Engine/Graphics/Materials/IMaterial.h
@@ -148,7 +148,7 @@ public:
const ::DrawCall* DrawCall = nullptr;
MaterialParamsLink* ParamsLink = nullptr;
void* CustomData = nullptr;
- float TimeParam;
+ float Time, UnscaledTime;
bool Instanced = false;
///
diff --git a/Source/Engine/Graphics/Materials/MaterialShader.cpp b/Source/Engine/Graphics/Materials/MaterialShader.cpp
index c24631d56..f1e9fd542 100644
--- a/Source/Engine/Graphics/Materials/MaterialShader.cpp
+++ b/Source/Engine/Graphics/Materials/MaterialShader.cpp
@@ -37,12 +37,15 @@ GPU_CB_STRUCT(MaterialShaderDataPerView {
Float4 TemporalAAJitter;
Float3 LargeWorldsChunkIndex;
float LargeWorldsChunkSize;
+ Float3 ViewPadding0;
+ float UnscaledTimeParam;
});
IMaterial::BindParameters::BindParameters(::GPUContext* context, const ::RenderContext& renderContext)
: GPUContext(context)
, RenderContext(renderContext)
- , TimeParam(Time::Draw.UnscaledTime.GetTotalSeconds())
+ , Time(Time::Draw.Time.GetTotalSeconds())
+ , UnscaledTime(Time::Draw.UnscaledTime.GetTotalSeconds())
{
}
@@ -50,7 +53,8 @@ IMaterial::BindParameters::BindParameters(::GPUContext* context, const ::RenderC
: GPUContext(context)
, RenderContext(renderContext)
, DrawCall(&drawCall)
- , TimeParam(Time::Draw.UnscaledTime.GetTotalSeconds())
+ , Time(Time::Draw.Time.GetTotalSeconds())
+ , UnscaledTime(Time::Draw.UnscaledTime.GetTotalSeconds())
, Instanced(instanced)
{
}
@@ -78,7 +82,8 @@ void IMaterial::BindParameters::BindViewData()
cb.ViewPos = view.Position;
cb.ViewFar = view.Far;
cb.ViewDir = view.Direction;
- cb.TimeParam = TimeParam;
+ cb.TimeParam = Time;
+ cb.UnscaledTimeParam = UnscaledTime;
cb.ViewInfo = view.ViewInfo;
cb.ScreenSize = view.ScreenSize;
cb.TemporalAAJitter = view.TemporalAAJitter;
diff --git a/Source/Engine/Graphics/Materials/MaterialShader.h b/Source/Engine/Graphics/Materials/MaterialShader.h
index ccc11c8cf..aedf2e870 100644
--- a/Source/Engine/Graphics/Materials/MaterialShader.h
+++ b/Source/Engine/Graphics/Materials/MaterialShader.h
@@ -10,7 +10,7 @@
///
/// Current materials shader version.
///
-#define MATERIAL_GRAPH_VERSION 173
+#define MATERIAL_GRAPH_VERSION 174
class Material;
class GPUShader;
diff --git a/Source/Engine/Graphics/Materials/PostFxMaterialShader.cpp b/Source/Engine/Graphics/Materials/PostFxMaterialShader.cpp
index 52ba7bc58..c82dd7447 100644
--- a/Source/Engine/Graphics/Materials/PostFxMaterialShader.cpp
+++ b/Source/Engine/Graphics/Materials/PostFxMaterialShader.cpp
@@ -21,6 +21,8 @@ PACK_STRUCT(struct PostFxMaterialShaderData {
Float4 ScreenSize;
Float4 TemporalAAJitter;
Matrix InverseViewProjectionMatrix;
+ Float3 ViewPadding0;
+ float UnscaledTimeParam;
});
void PostFxMaterialShader::Bind(BindParameters& params)
@@ -51,7 +53,8 @@ void PostFxMaterialShader::Bind(BindParameters& params)
materialData->ViewPos = view.Position;
materialData->ViewFar = view.Far;
materialData->ViewDir = view.Direction;
- materialData->TimeParam = params.TimeParam;
+ materialData->TimeParam = params.Time;
+ materialData->UnscaledTimeParam = params.UnscaledTime;
materialData->ViewInfo = view.ViewInfo;
materialData->ScreenSize = view.ScreenSize;
materialData->TemporalAAJitter = view.TemporalAAJitter;
diff --git a/Source/Engine/Graphics/RenderBuffers.h b/Source/Engine/Graphics/RenderBuffers.h
index 159ab13a9..647d28c4c 100644
--- a/Source/Engine/Graphics/RenderBuffers.h
+++ b/Source/Engine/Graphics/RenderBuffers.h
@@ -175,6 +175,12 @@ public:
return (const T*)FindCustomBuffer(name, withLinked);
}
+ template
+ const T* FindLinkedBuffer(const StringView& name) const
+ {
+ return LinkedCustomBuffers ? (const T*)LinkedCustomBuffers->FindCustomBuffer(name, true) : nullptr;
+ }
+
template
T* GetCustomBuffer(const StringView& name, bool withLinked = true)
{
diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp
index ec50cfd56..18342dbbb 100644
--- a/Source/Engine/Level/Actors/AnimatedModel.cpp
+++ b/Source/Engine/Level/Actors/AnimatedModel.cpp
@@ -184,6 +184,14 @@ void AnimatedModel::GetNodeTransformation(const StringView& nodeName, Matrix& no
GetNodeTransformation(SkinnedModel ? SkinnedModel->FindNode(nodeName) : -1, nodeTransformation, worldSpace);
}
+void AnimatedModel::GetNodeTransformation(Array& nodeTransformations, bool worldSpace) const
+{
+ for (NodeTransformation& item : nodeTransformations)
+ {
+ GetNodeTransformation(item.NodeIndex, item.NodeMatrix, worldSpace);
+ }
+}
+
void AnimatedModel::SetNodeTransformation(int32 nodeIndex, const Matrix& nodeTransformation, bool worldSpace)
{
if (GraphInstance.NodesPose.IsEmpty())
@@ -201,6 +209,33 @@ void AnimatedModel::SetNodeTransformation(int32 nodeIndex, const Matrix& nodeTra
OnAnimationUpdated();
}
+void AnimatedModel::SetNodeTransformation(const Array& nodeTransformations, bool worldSpace)
+{
+ if (GraphInstance.NodesPose.IsEmpty())
+ const_cast(this)->PreInitSkinningData(); // Ensure to have valid nodes pose to return
+
+ // Calculate it once, outside loop
+ Matrix invWorld;
+ if (worldSpace)
+ {
+ Matrix world;
+ GetLocalToWorldMatrix(world);
+ Matrix::Invert(world, invWorld);
+ }
+
+ for (int i = 0; i < nodeTransformations.Count(); i++)
+ {
+ int nodeIndex = nodeTransformations[i].NodeIndex;
+ CHECK(nodeIndex >= 0 && nodeIndex < GraphInstance.NodesPose.Count());
+ GraphInstance.NodesPose[nodeIndex] = nodeTransformations[i].NodeMatrix;
+ if (worldSpace)
+ {
+ GraphInstance.NodesPose[nodeIndex] = GraphInstance.NodesPose[nodeIndex] * invWorld;
+ }
+ }
+ OnAnimationUpdated();
+}
+
void AnimatedModel::SetNodeTransformation(const StringView& nodeName, const Matrix& nodeTransformation, bool worldSpace)
{
SetNodeTransformation(SkinnedModel ? SkinnedModel->FindNode(nodeName) : -1, nodeTransformation, worldSpace);
@@ -830,7 +865,10 @@ void AnimatedModel::OnAnimationUpdated_Async()
_skinningData.OnDataChanged(!PerBoneMotionBlur);
}
- UpdateBounds();
+ if (UpdateWhenOffscreen)
+ {
+ UpdateBounds();
+ }
}
void AnimatedModel::OnAnimationUpdated_Sync()
diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h
index deadf8c7c..a011be0d0 100644
--- a/Source/Engine/Level/Actors/AnimatedModel.h
+++ b/Source/Engine/Level/Actors/AnimatedModel.h
@@ -18,6 +18,24 @@ class FLAXENGINE_API AnimatedModel : public ModelInstanceActor, IAssetReference
DECLARE_SCENE_OBJECT(AnimatedModel);
friend class AnimationsSystem;
+ ///
+ /// Keeps the data of a Node and its relevant Transform Matrix together when passing it between functions.
+ ///
+ API_STRUCT() struct NodeTransformation
+ {
+ DECLARE_SCRIPTING_TYPE_MINIMAL(NodeTransformation);
+
+ ///
+ /// The index of the node in the node hierarchy.
+ ///
+ API_FIELD() uint32 NodeIndex;
+
+ ///
+ /// The transformation matrix of the node
+ ///
+ API_FIELD() Matrix NodeMatrix;
+ };
+
///
/// Describes the animation graph updates frequency for the animated model.
///
@@ -242,6 +260,14 @@ public:
/// True if convert matrices into world-space, otherwise returned values will be in local-space of the actor.
API_FUNCTION() void GetNodeTransformation(const StringView& nodeName, API_PARAM(Out) Matrix& nodeTransformation, bool worldSpace = false) const;
+ ///
+ /// Gets the node final transformation for a series of nodes.
+ ///
+ /// The series of nodes that will be returned
+ /// True if convert matrices into world-space, otherwise returned values will be in local-space of the actor.
+ ///
+ API_FUNCTION() void GetNodeTransformation(API_PARAM(Ref) Array& nodeTransformations, bool worldSpace = false) const;
+
///
/// Sets the node final transformation. If multiple nodes are to be set within a frame, do not use set worldSpace to true, and do the conversion yourself to avoid recalculation of inv matrices.
///
@@ -258,6 +284,14 @@ public:
/// True if convert matrices from world-space, otherwise values will be in local-space of the actor.
API_FUNCTION() void SetNodeTransformation(const StringView& nodeName, const Matrix& nodeTransformation, bool worldSpace = false);
+ ///
+ /// Sets a group of nodes final transformation.
+ ///
+ /// Array of the final node transformation matrix.
+ /// True if convert matrices from world-space, otherwise values will be in local-space of the actor.
+ ///
+ API_FUNCTION() void SetNodeTransformation(const Array& nodeTransformations, bool worldSpace = false);
+
///
/// Finds the closest node to a given location.
///
diff --git a/Source/Engine/Level/Level.cpp b/Source/Engine/Level/Level.cpp
index ddb871846..9cf6794be 100644
--- a/Source/Engine/Level/Level.cpp
+++ b/Source/Engine/Level/Level.cpp
@@ -65,6 +65,21 @@ bool LayersMask::HasLayer(const StringView& layerName) const
return HasLayer(Level::GetLayerIndex(layerName));
}
+LayersMask LayersMask::GetMask(Span layerNames)
+{
+ LayersMask mask(0);
+ for (StringView& layerName : layerNames)
+ {
+ // Ignore blank entries
+ if (layerName.Length() == 0)
+ continue;
+ int32 index = Level::GetLayerIndex(layerName);
+ if (index != -1)
+ mask.Mask |= (uint32)(1 << index);
+ }
+ return mask;
+}
+
enum class SceneEventType
{
OnSceneSaving = 0,
@@ -824,6 +839,18 @@ int32 Level::GetLayerIndex(const StringView& layer)
return result;
}
+StringView Level::GetLayerName(const int32 layerIndex)
+{
+ for (int32 i = 0; i < 32; i++)
+ {
+ if (i == layerIndex)
+ {
+ return Layers[i];
+ }
+ }
+ return TEXT("");
+}
+
void Level::callActorEvent(ActorEventType eventType, Actor* a, Actor* b)
{
PROFILE_CPU();
diff --git a/Source/Engine/Level/Level.h b/Source/Engine/Level/Level.h
index 484ba35b8..110b9ac61 100644
--- a/Source/Engine/Level/Level.h
+++ b/Source/Engine/Level/Level.h
@@ -539,6 +539,13 @@ public:
///
API_FUNCTION() static int32 GetLayerIndex(const StringView& layer);
+ ///
+ /// Gets the name of the layer based on the index.
+ ///
+ /// The index to find the layer string. 0 - 32.
+ /// The layer string. Returns a blank string if index not found.
+ API_FUNCTION() static StringView GetLayerName(const int32 layerIndex);
+
private:
// Actor API
enum class ActorEventType
diff --git a/Source/Engine/Particles/ParticleEffect.cpp b/Source/Engine/Particles/ParticleEffect.cpp
index 183438ba2..f30f6a495 100644
--- a/Source/Engine/Particles/ParticleEffect.cpp
+++ b/Source/Engine/Particles/ParticleEffect.cpp
@@ -549,6 +549,19 @@ void ParticleEffect::OnAssetChanged(Asset* asset, void* caller)
}
void ParticleEffect::OnAssetLoaded(Asset* asset, void* caller)
+{
+ ApplyModifiedParameters();
+#if USE_EDITOR
+ // When one of the emitters gets edited, cached parameters need to be applied
+ auto& emitters = ParticleSystem.Get()->Emitters;
+ for (auto& emitter : emitters)
+ {
+ emitter.Loaded.BindUnique(this);
+ }
+#endif
+}
+
+void ParticleEffect::OnParticleEmitterLoaded()
{
ApplyModifiedParameters();
}
@@ -844,6 +857,10 @@ void ParticleEffect::OnActiveInTreeChanged()
CacheModifiedParameters();
Instance.ClearState();
}
+ else
+ {
+ ApplyModifiedParameters();
+ }
}
void ParticleEffect::OnTransformChanged()
diff --git a/Source/Engine/Particles/ParticleEffect.h b/Source/Engine/Particles/ParticleEffect.h
index 4ec06e82b..c54fa5639 100644
--- a/Source/Engine/Particles/ParticleEffect.h
+++ b/Source/Engine/Particles/ParticleEffect.h
@@ -394,6 +394,7 @@ private:
void ApplyModifiedParameters();
void OnParticleSystemModified();
void OnParticleSystemLoaded();
+ void OnParticleEmitterLoaded();
// [IAssetReference]
void OnAssetChanged(Asset* asset, void* caller) override;
diff --git a/Source/Engine/Platform/Apple/AppleFileSystem.cpp b/Source/Engine/Platform/Apple/AppleFileSystem.cpp
index 6e596bf1b..450507009 100644
--- a/Source/Engine/Platform/Apple/AppleFileSystem.cpp
+++ b/Source/Engine/Platform/Apple/AppleFileSystem.cpp
@@ -7,10 +7,6 @@
#include "Engine/Platform/File.h"
#include "Engine/Core/Types/String.h"
#include "Engine/Core/Types/StringView.h"
-#include "Engine/Core/Types/TimeSpan.h"
-#include "Engine/Core/Collections/Array.h"
-#include "Engine/Core/Math/Math.h"
-#include "Engine/Core/Log.h"
#include "Engine/Utilities/StringConverter.h"
#include
#include
@@ -18,281 +14,9 @@
#include
#include
#include
-#include
#include
#include
-const DateTime UnixEpoch(1970, 1, 1);
-
-bool AppleFileSystem::CreateDirectory(const StringView& path)
-{
- const StringAsANSI<> pathAnsi(*path, path.Length());
-
- // Skip if already exists
- struct stat fileInfo;
- if (stat(pathAnsi.Get(), &fileInfo) != -1 && S_ISDIR(fileInfo.st_mode))
- {
- return false;
- }
-
- // Recursively do it all again for the parent directory, if any
- const int32 slashIndex = path.FindLast('/');
- if (slashIndex > 1)
- {
- if (CreateDirectory(path.Substring(0, slashIndex)))
- {
- return true;
- }
- }
-
- // Create the last directory on the path (the recursive calls will have taken care of the parent directories by now)
- return mkdir(pathAnsi.Get(), 0755) != 0 && errno != EEXIST;
-}
-
-bool DeletePathTree(const char* path)
-{
- size_t pathLength;
- DIR* dir;
- struct stat statPath, statEntry;
- struct dirent* entry;
-
- // Stat for the path
- stat(path, &statPath);
-
- // If path does not exists or is not dir - exit with status -1
- if (S_ISDIR(statPath.st_mode) == 0)
- {
- // Is not directory
- return true;
- }
-
- // If not possible to read the directory for this user
- if ((dir = opendir(path)) == NULL)
- {
- // Cannot open directory
- return true;
- }
-
- // The length of the path
- pathLength = strlen(path);
-
- // Iteration through entries in the directory
- while ((entry = readdir(dir)) != NULL)
- {
- // Skip entries "." and ".."
- if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
- continue;
-
- // Determinate a full path of an entry
- char full_path[256];
- ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(full_path));
- strcpy(full_path, path);
- strcat(full_path, "/");
- strcat(full_path, entry->d_name);
-
- // Stat for the entry
- stat(full_path, &statEntry);
-
- // Recursively remove a nested directory
- if (S_ISDIR(statEntry.st_mode) != 0)
- {
- if (DeletePathTree(full_path))
- return true;
- continue;
- }
-
- // Remove a file object
- if (unlink(full_path) != 0)
- return true;
- }
-
- // Remove the devastated directory and close the object of it
- if (rmdir(path) != 0)
- return true;
-
- closedir(dir);
-
- return false;
-}
-
-bool AppleFileSystem::DeleteDirectory(const String& path, bool deleteContents)
-{
- const StringAsANSI<> pathANSI(*path, path.Length());
- if (deleteContents)
- return DeletePathTree(pathANSI.Get());
- return rmdir(pathANSI.Get()) != 0;
-}
-
-bool AppleFileSystem::DirectoryExists(const StringView& path)
-{
- struct stat fileInfo;
- const StringAsANSI<> pathANSI(*path, path.Length());
- if (stat(pathANSI.Get(), &fileInfo) != -1)
- {
- return S_ISDIR(fileInfo.st_mode);
- }
- return false;
-}
-
-bool AppleFileSystem::DirectoryGetFiles(Array& results, const String& path, const Char* searchPattern, DirectorySearchOption option)
-{
- const StringAsANSI<> pathANSI(*path, path.Length());
- const StringAsANSI<> searchPatternANSI(searchPattern);
-
- // Check if use only top directory
- if (option == DirectorySearchOption::TopDirectoryOnly)
- return getFilesFromDirectoryTop(results, pathANSI.Get(), searchPatternANSI.Get());
- return getFilesFromDirectoryAll(results, pathANSI.Get(), searchPatternANSI.Get());
-}
-
-bool AppleFileSystem::GetChildDirectories(Array& results, const String& path)
-{
- size_t pathLength;
- DIR* dir;
- struct stat statPath, statEntry;
- struct dirent* entry;
- const StringAsANSI<> pathANSI(*path, path.Length());
- const char* pathStr = pathANSI.Get();
-
- // Stat for the path
- stat(pathStr, &statPath);
-
- // If path does not exist or is not dir - exit with status -1
- if (S_ISDIR(statPath.st_mode) == 0)
- {
- // Is not directory
- return true;
- }
-
- // If not possible to read the directory for this user
- if ((dir = opendir(pathStr)) == NULL)
- {
- // Cannot open directory
- return true;
- }
-
- // The length of the path
- pathLength = strlen(pathStr);
-
- // Iteration through entries in the directory
- while ((entry = readdir(dir)) != NULL)
- {
- // Skip entries "." and ".."
- if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
- continue;
-
- // Determinate a full path of an entry
- char fullPath[256];
- ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(fullPath));
- strcpy(fullPath, pathStr);
- strcat(fullPath, "/");
- strcat(fullPath, entry->d_name);
-
- // Stat for the entry
- stat(fullPath, &statEntry);
-
- // Check for directory
- if (S_ISDIR(statEntry.st_mode) != 0)
- {
- // Add directory
- results.Add(String(fullPath));
- }
- }
-
- closedir(dir);
-
- return false;
-}
-
-bool AppleFileSystem::FileExists(const StringView& path)
-{
- struct stat fileInfo;
- const StringAsANSI<> pathANSI(*path, path.Length());
- if (stat(pathANSI.Get(), &fileInfo) != -1)
- {
- return S_ISREG(fileInfo.st_mode);
- }
- return false;
-}
-
-bool AppleFileSystem::DeleteFile(const StringView& path)
-{
- const StringAsANSI<> pathANSI(*path, path.Length());
- return unlink(pathANSI.Get()) != 0;
-}
-
-uint64 AppleFileSystem::GetFileSize(const StringView& path)
-{
- struct stat fileInfo;
- fileInfo.st_size = 0;
- const StringAsANSI<> pathANSI(*path, path.Length());
- if (stat(pathANSI.Get(), &fileInfo) != -1)
- {
- // Check for directories
- if (S_ISDIR(fileInfo.st_mode))
- {
- fileInfo.st_size = 0;
- }
- }
- return fileInfo.st_size;
-}
-
-bool AppleFileSystem::IsReadOnly(const StringView& path)
-{
- const StringAsANSI<> pathANSI(*path, path.Length());
- if (access(pathANSI.Get(), W_OK) == -1)
- {
- return errno == EACCES;
- }
- return false;
-}
-
-bool AppleFileSystem::SetReadOnly(const StringView& path, bool isReadOnly)
-{
- const StringAsANSI<> pathANSI(*path, path.Length());
- struct stat fileInfo;
- if (stat(pathANSI.Get(), &fileInfo) != -1)
- {
- if (isReadOnly)
- {
- fileInfo.st_mode &= ~S_IWUSR;
- }
- else
- {
- fileInfo.st_mode |= S_IWUSR;
- }
- return chmod(pathANSI.Get(), fileInfo.st_mode) == 0;
- }
- return false;
-}
-
-bool AppleFileSystem::MoveFile(const StringView& dst, const StringView& src, bool overwrite)
-{
- if (!overwrite && FileExists(dst))
- {
- // Already exists
- return true;
- }
-
- if (overwrite)
- {
- unlink(StringAsANSI<>(*dst, dst.Length()).Get());
- }
- if (rename(StringAsANSI<>(*src, src.Length()).Get(), StringAsANSI<>(*dst, dst.Length()).Get()) != 0)
- {
- if (errno == EXDEV)
- {
- if (!CopyFile(dst, src))
- {
- unlink(StringAsANSI<>(*src, src.Length()).Get());
- return false;
- }
- }
- return true;
- }
- return false;
-}
-
bool AppleFileSystem::CopyFile(const StringView& dst, const StringView& src)
{
const StringAsANSI<> srcANSI(*src, src.Length());
@@ -352,156 +76,6 @@ out_error:
return true;
}
-bool AppleFileSystem::getFilesFromDirectoryTop(Array& results, const char* path, const char* searchPattern)
-{
- size_t pathLength;
- struct stat statPath, statEntry;
- struct dirent* entry;
-
- // Stat for the path
- stat(path, &statPath);
-
- // If path does not exists or is not dir - exit with status -1
- if (S_ISDIR(statPath.st_mode) == 0)
- {
- // Is not directory
- return true;
- }
-
- // If not possible to read the directory for this user
- DIR* dir = opendir(path);
- if (dir == NULL)
- {
- // Cannot open directory
- return true;
- }
-
- // The length of the path
- pathLength = strlen(path);
-
- // Iteration through entries in the directory
- while ((entry = readdir(dir)) != NULL)
- {
- // Skip entries "." and ".."
- if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
- continue;
-
- // Determinate a full path of an entry
- char fullPath[256];
- ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(fullPath));
- strcpy(fullPath, path);
- strcat(fullPath, "/");
- strcat(fullPath, entry->d_name);
-
- // Stat for the entry
- stat(fullPath, &statEntry);
-
- // Check for file
- if (S_ISREG(statEntry.st_mode) != 0)
- {
- // Validate with filter
- const int32 fullPathLength = StringUtils::Length(fullPath);
- const int32 searchPatternLength = StringUtils::Length(searchPattern);
- if (searchPatternLength == 0 || StringUtils::Compare(searchPattern, "*") == 0)
- {
- // All files
- }
- else if (searchPattern[0] == '*' && searchPatternLength < fullPathLength && StringUtils::Compare(fullPath + fullPathLength - searchPatternLength + 1, searchPattern + 1, searchPatternLength - 1) == 0)
- {
- // Path ending
- }
- else
- {
- // TODO: implement all cases in a generic way
- continue;
- }
-
- // Add file
- results.Add(String(fullPath));
- }
- }
-
- closedir(dir);
-
- return false;
-}
-
-bool AppleFileSystem::getFilesFromDirectoryAll(Array& results, const char* path, const char* searchPattern)
-{
- // Find all files in this directory
- getFilesFromDirectoryTop(results, path, searchPattern);
-
- size_t pathLength;
- DIR* dir;
- struct stat statPath, statEntry;
- struct dirent* entry;
-
- // Stat for the path
- stat(path, &statPath);
-
- // If path does not exists or is not dir - exit with status -1
- if (S_ISDIR(statPath.st_mode) == 0)
- {
- // Is not directory
- return true;
- }
-
- // If not possible to read the directory for this user
- if ((dir = opendir(path)) == NULL)
- {
- // Cannot open directory
- return true;
- }
-
- // The length of the path
- pathLength = strlen(path);
-
- // Iteration through entries in the directory
- while ((entry = readdir(dir)) != NULL)
- {
- // Skip entries "." and ".."
- if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
- continue;
-
- // Determinate a full path of an entry
- char full_path[256];
- ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(full_path));
- strcpy(full_path, path);
- strcat(full_path, "/");
- strcat(full_path, entry->d_name);
-
- // Stat for the entry
- stat(full_path, &statEntry);
-
- // Check for directory
- if (S_ISDIR(statEntry.st_mode) != 0)
- {
- if (getFilesFromDirectoryAll(results, full_path, searchPattern))
- {
- closedir(dir);
- return true;
- }
- }
- }
-
- closedir(dir);
-
- return false;
-}
-
-DateTime AppleFileSystem::GetFileLastEditTime(const StringView& path)
-{
- struct stat fileInfo;
- const StringAsANSI<> pathANSI(*path, path.Length());
- if (stat(pathANSI.Get(), &fileInfo) == -1)
- {
- return DateTime::MinValue();
- }
-
- const TimeSpan timeSinceEpoch(0, 0, 0, fileInfo.st_mtime);
- return UnixEpoch + timeSinceEpoch;
-}
-
void AppleFileSystem::GetSpecialFolderPath(const SpecialFolder type, String& result)
{
String home;
diff --git a/Source/Engine/Platform/Apple/AppleFileSystem.h b/Source/Engine/Platform/Apple/AppleFileSystem.h
index 5fa6ad1b7..45d9e4df6 100644
--- a/Source/Engine/Platform/Apple/AppleFileSystem.h
+++ b/Source/Engine/Platform/Apple/AppleFileSystem.h
@@ -4,33 +4,17 @@
#if PLATFORM_MAC || PLATFORM_IOS
-#include "Engine/Platform/Base/FileSystemBase.h"
+#include "Engine/Platform/Unix/UnixFileSystem.h"
///
/// Apple platform implementation of filesystem service.
///
-class FLAXENGINE_API AppleFileSystem : public FileSystemBase
+class FLAXENGINE_API AppleFileSystem : public UnixFileSystem
{
public:
// [FileSystemBase]
- static bool CreateDirectory(const StringView& path);
- static bool DeleteDirectory(const String& path, bool deleteContents = true);
- static bool DirectoryExists(const StringView& path);
- static bool DirectoryGetFiles(Array& results, const String& path, const Char* searchPattern = TEXT("*"), DirectorySearchOption option = DirectorySearchOption::AllDirectories);
- static bool GetChildDirectories(Array& results, const String& path);
- static bool FileExists(const StringView& path);
- static bool DeleteFile(const StringView& path);
- static uint64 GetFileSize(const StringView& path);
- static bool IsReadOnly(const StringView& path);
- static bool SetReadOnly(const StringView& path, bool isReadOnly);
- static bool MoveFile(const StringView& dst, const StringView& src, bool overwrite = false);
static bool CopyFile(const StringView& dst, const StringView& src);
- static DateTime GetFileLastEditTime(const StringView& path);
static void GetSpecialFolderPath(const SpecialFolder type, String& result);
-
-private:
- static bool getFilesFromDirectoryTop(Array& results, const char* path, const char* searchPattern);
- static bool getFilesFromDirectoryAll(Array& results, const char* path, const char* searchPattern);
};
#endif
diff --git a/Source/Engine/Platform/Linux/LinuxFileSystem.cpp b/Source/Engine/Platform/Linux/LinuxFileSystem.cpp
index 3e2055572..9b31dc28f 100644
--- a/Source/Engine/Platform/Linux/LinuxFileSystem.cpp
+++ b/Source/Engine/Platform/Linux/LinuxFileSystem.cpp
@@ -4,22 +4,16 @@
#include "LinuxFileSystem.h"
#include "Engine/Platform/File.h"
-#include "Engine/Platform/StringUtils.h"
-#include "Engine/Core/Types/String.h"
#include "Engine/Core/Types/StringBuilder.h"
-#include "Engine/Core/Types/StringView.h"
-#include "Engine/Core/Types/TimeSpan.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Core/Math/Math.h"
#include "Engine/Core/Log.h"
#include "Engine/Utilities/StringConverter.h"
-#include
#include
#include
#include
#include
#include
-#include
#include
#include
#include
@@ -165,280 +159,6 @@ bool LinuxFileSystem::ShowFileExplorer(const StringView& path)
return false;
}
-bool LinuxFileSystem::CreateDirectory(const StringView& path)
-{
- const StringAsUTF8<> pathAnsi(*path, path.Length());
-
- // Skip if already exists
- struct stat fileInfo;
- if (stat(pathAnsi.Get(), &fileInfo) != -1 && S_ISDIR(fileInfo.st_mode))
- {
- return false;
- }
-
- // Recursively do it all again for the parent directory, if any
- const int32 slashIndex = path.FindLast('/');
- if (slashIndex > 1)
- {
- if (CreateDirectory(path.Substring(0, slashIndex)))
- {
- return true;
- }
- }
-
- // Create the last directory on the path (the recursive calls will have taken care of the parent directories by now)
- return mkdir(pathAnsi.Get(), 0755) != 0 && errno != EEXIST;
-}
-
-bool DeleteUnixPathTree(const char* path)
-{
- size_t pathLength;
- DIR* dir;
- struct stat statPath, statEntry;
- struct dirent* entry;
-
- // Stat for the path
- stat(path, &statPath);
-
- // If path does not exists or is not dir - exit with status -1
- if (S_ISDIR(statPath.st_mode) == 0)
- {
- // Is not directory
- return true;
- }
-
- // If not possible to read the directory for this user
- if ((dir = opendir(path)) == NULL)
- {
- // Cannot open directory
- return true;
- }
-
- // The length of the path
- pathLength = strlen(path);
-
- // Iteration through entries in the directory
- while ((entry = readdir(dir)) != NULL)
- {
- // Skip entries "." and ".."
- if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
- continue;
-
- // Determinate a full path of an entry
- char full_path[256];
- ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(full_path));
- strcpy(full_path, path);
- strcat(full_path, "/");
- strcat(full_path, entry->d_name);
-
- // Stat for the entry
- stat(full_path, &statEntry);
-
- // Recursively remove a nested directory
- if (S_ISDIR(statEntry.st_mode) != 0)
- {
- if (DeleteUnixPathTree(full_path))
- return true;
- continue;
- }
-
- // Remove a file object
- if (unlink(full_path) != 0)
- return true;
- }
-
- // Remove the devastated directory and close the object of it
- if (rmdir(path) != 0)
- return true;
-
- closedir(dir);
-
- return false;
-}
-
-bool LinuxFileSystem::DeleteDirectory(const String& path, bool deleteContents)
-{
- const StringAsUTF8<> pathANSI(*path, path.Length());
- if (deleteContents)
- {
- return DeleteUnixPathTree(pathANSI.Get());
- }
- else
- {
- return rmdir(pathANSI.Get()) != 0;
- }
-}
-
-bool LinuxFileSystem::DirectoryExists(const StringView& path)
-{
- struct stat fileInfo;
- const StringAsUTF8<> pathANSI(*path, path.Length());
- if (stat(pathANSI.Get(), &fileInfo) != -1)
- {
- return S_ISDIR(fileInfo.st_mode);
- }
- return false;
-}
-
-bool LinuxFileSystem::DirectoryGetFiles(Array& results, const String& path, const Char* searchPattern, DirectorySearchOption option)
-{
- const StringAsUTF8<> pathANSI(*path, path.Length());
- const StringAsUTF8<> searchPatternANSI(searchPattern);
-
- // Check if use only top directory
- if (option == DirectorySearchOption::TopDirectoryOnly)
- return getFilesFromDirectoryTop(results, pathANSI.Get(), searchPatternANSI.Get());
- return getFilesFromDirectoryAll(results, pathANSI.Get(), searchPatternANSI.Get());
-}
-
-bool LinuxFileSystem::GetChildDirectories(Array& results, const String& path)
-{
- size_t pathLength;
- DIR* dir;
- struct stat statPath, statEntry;
- struct dirent* entry;
- const StringAsUTF8<> pathANSI(*path, path.Length());
- const char* pathStr = pathANSI.Get();
-
- // Stat for the path
- stat(pathStr, &statPath);
-
- // If path does not exists or is not dir - exit with status -1
- if (S_ISDIR(statPath.st_mode) == 0)
- {
- // Is not directory
- return true;
- }
-
- // If not possible to read the directory for this user
- if ((dir = opendir(pathStr)) == NULL)
- {
- // Cannot open directory
- return true;
- }
-
- // The length of the path
- pathLength = strlen(pathStr);
-
- // Iteration through entries in the directory
- while ((entry = readdir(dir)) != NULL)
- {
- // Skip entries "." and ".."
- if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
- continue;
-
- // Determinate a full path of an entry
- char fullPath[256];
- ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(fullPath));
- strcpy(fullPath, pathStr);
- strcat(fullPath, "/");
- strcat(fullPath, entry->d_name);
-
- // Stat for the entry
- stat(fullPath, &statEntry);
-
- // Check for directory
- if (S_ISDIR(statEntry.st_mode) != 0)
- {
- // Add directory
- results.Add(String(fullPath));
- }
- }
-
- closedir(dir);
-
- return false;
-}
-
-bool LinuxFileSystem::FileExists(const StringView& path)
-{
- struct stat fileInfo;
- const StringAsUTF8<> pathANSI(*path, path.Length());
- if (stat(pathANSI.Get(), &fileInfo) != -1)
- {
- return S_ISREG(fileInfo.st_mode);
- }
- return false;
-}
-
-bool LinuxFileSystem::DeleteFile(const StringView& path)
-{
- const StringAsUTF8<> pathANSI(*path, path.Length());
- return unlink(pathANSI.Get()) != 0;
-}
-
-uint64 LinuxFileSystem::GetFileSize(const StringView& path)
-{
- struct stat fileInfo;
- fileInfo.st_size = 0;
- const StringAsUTF8<> pathANSI(*path, path.Length());
- if (stat(pathANSI.Get(), &fileInfo) != -1)
- {
- // Check for directories
- if (S_ISDIR(fileInfo.st_mode))
- {
- fileInfo.st_size = 0;
- }
- }
- return fileInfo.st_size;
-}
-
-bool LinuxFileSystem::IsReadOnly(const StringView& path)
-{
- const StringAsUTF8<> pathANSI(*path, path.Length());
- if (access(pathANSI.Get(), W_OK) == -1)
- {
- return errno == EACCES;
- }
- return false;
-}
-
-bool LinuxFileSystem::SetReadOnly(const StringView& path, bool isReadOnly)
-{
- const StringAsUTF8<> pathANSI(*path, path.Length());
- struct stat fileInfo;
- if (stat(pathANSI.Get(), &fileInfo) != -1)
- {
- if (isReadOnly)
- {
- fileInfo.st_mode &= ~S_IWUSR;
- }
- else
- {
- fileInfo.st_mode |= S_IWUSR;
- }
- return chmod(pathANSI.Get(), fileInfo.st_mode) == 0;
- }
- return false;
-}
-
-bool LinuxFileSystem::MoveFile(const StringView& dst, const StringView& src, bool overwrite)
-{
- if (!overwrite && FileExists(dst))
- {
- // Already exists
- return true;
- }
-
- if (overwrite)
- {
- unlink(StringAsUTF8<>(*dst, dst.Length()).Get());
- }
- if (rename(StringAsUTF8<>(*src, src.Length()).Get(), StringAsUTF8<>(*dst, dst.Length()).Get()) != 0)
- {
- if (errno == EXDEV)
- {
- if (!CopyFile(dst, src))
- {
- unlink(StringAsUTF8<>(*src, src.Length()).Get());
- return false;
- }
- }
- return true;
- }
- return false;
-}
-
bool LinuxFileSystem::CopyFile(const StringView& dst, const StringView& src)
{
const StringAsUTF8<> srcANSI(*src, src.Length());
@@ -612,156 +332,6 @@ bool LinuxFileSystem::UrnEncodePath(const char *path, char *result, const int ma
return true;
}
-bool LinuxFileSystem::getFilesFromDirectoryTop(Array& results, const char* path, const char* searchPattern)
-{
- size_t pathLength;
- struct stat statPath, statEntry;
- struct dirent* entry;
-
- // Stat for the path
- stat(path, &statPath);
-
- // If path does not exists or is not dir - exit with status -1
- if (S_ISDIR(statPath.st_mode) == 0)
- {
- // Is not directory
- return true;
- }
-
- // If not possible to read the directory for this user
- DIR* dir = opendir(path);
- if (dir == NULL)
- {
- // Cannot open directory
- return true;
- }
-
- // The length of the path
- pathLength = strlen(path);
-
- // Iteration through entries in the directory
- while ((entry = readdir(dir)) != NULL)
- {
- // Skip entries "." and ".."
- if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
- continue;
-
- // Determinate a full path of an entry
- char fullPath[256];
- ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(fullPath));
- strcpy(fullPath, path);
- strcat(fullPath, "/");
- strcat(fullPath, entry->d_name);
-
- // Stat for the entry
- stat(fullPath, &statEntry);
-
- // Check for file
- if (S_ISREG(statEntry.st_mode) != 0)
- {
- // Validate with filter
- const int32 fullPathLength = StringUtils::Length(fullPath);
- const int32 searchPatternLength = StringUtils::Length(searchPattern);
- if (searchPatternLength == 0 || StringUtils::Compare(searchPattern, "*") == 0)
- {
- // All files
- }
- else if (searchPattern[0] == '*' && searchPatternLength < fullPathLength && StringUtils::Compare(fullPath + fullPathLength - searchPatternLength + 1, searchPattern + 1, searchPatternLength - 1) == 0)
- {
- // Path ending
- }
- else
- {
- // TODO: implement all cases in a generic way
- continue;
- }
-
- // Add file
- results.Add(String(fullPath));
- }
- }
-
- closedir(dir);
-
- return false;
-}
-
-bool LinuxFileSystem::getFilesFromDirectoryAll(Array& results, const char* path, const char* searchPattern)
-{
- // Find all files in this directory
- getFilesFromDirectoryTop(results, path, searchPattern);
-
- size_t pathLength;
- DIR* dir;
- struct stat statPath, statEntry;
- struct dirent* entry;
-
- // Stat for the path
- stat(path, &statPath);
-
- // If path does not exists or is not dir - exit with status -1
- if (S_ISDIR(statPath.st_mode) == 0)
- {
- // Is not directory
- return true;
- }
-
- // If not possible to read the directory for this user
- if ((dir = opendir(path)) == NULL)
- {
- // Cannot open directory
- return true;
- }
-
- // The length of the path
- pathLength = strlen(path);
-
- // Iteration through entries in the directory
- while ((entry = readdir(dir)) != NULL)
- {
- // Skip entries "." and ".."
- if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
- continue;
-
- // Determinate a full path of an entry
- char full_path[256];
- ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(full_path));
- strcpy(full_path, path);
- strcat(full_path, "/");
- strcat(full_path, entry->d_name);
-
- // Stat for the entry
- stat(full_path, &statEntry);
-
- // Check for directory
- if (S_ISDIR(statEntry.st_mode) != 0)
- {
- if (getFilesFromDirectoryAll(results, full_path, searchPattern))
- {
- closedir(dir);
- return true;
- }
- }
- }
-
- closedir(dir);
-
- return false;
-}
-
-DateTime LinuxFileSystem::GetFileLastEditTime(const StringView& path)
-{
- struct stat fileInfo;
- const StringAsUTF8<> pathANSI(*path, path.Length());
- if (stat(pathANSI.Get(), &fileInfo) == -1)
- {
- return DateTime::MinValue();
- }
-
- const TimeSpan timeSinceEpoch(0, 0, 0, fileInfo.st_mtime);
- return UnixEpoch + timeSinceEpoch;
-}
-
void LinuxFileSystem::GetSpecialFolderPath(const SpecialFolder type, String& result)
{
const String& home = LinuxPlatform::GetHomeDirectory();
diff --git a/Source/Engine/Platform/Linux/LinuxFileSystem.h b/Source/Engine/Platform/Linux/LinuxFileSystem.h
index f77c3dd4f..377bbbd99 100644
--- a/Source/Engine/Platform/Linux/LinuxFileSystem.h
+++ b/Source/Engine/Platform/Linux/LinuxFileSystem.h
@@ -4,38 +4,24 @@
#if PLATFORM_LINUX
-#include "Engine/Platform/Base/FileSystemBase.h"
+#include "Engine/Platform/Unix/UnixFileSystem.h"
///
/// Linux platform implementation of filesystem service.
///
-class FLAXENGINE_API LinuxFileSystem : public FileSystemBase
+class FLAXENGINE_API LinuxFileSystem : public UnixFileSystem
{
public:
// [FileSystemBase]
static bool ShowOpenFileDialog(Window* parentWindow, const StringView& initialDirectory, const StringView& filter, bool multiSelect, const StringView& title, Array& filenames);
static bool ShowBrowseFolderDialog(Window* parentWindow, const StringView& initialDirectory, const StringView& title, String& path);
static bool ShowFileExplorer(const StringView& path);
- static bool CreateDirectory(const StringView& path);
- static bool DeleteDirectory(const String& path, bool deleteContents = true);
- static bool DirectoryExists(const StringView& path);
- static bool DirectoryGetFiles(Array& results, const String& path, const Char* searchPattern = TEXT("*"), DirectorySearchOption option = DirectorySearchOption::AllDirectories);
- static bool GetChildDirectories(Array& results, const String& path);
- static bool FileExists(const StringView& path);
- static bool DeleteFile(const StringView& path);
- static bool MoveFileToRecycleBin(const StringView& path);
- static uint64 GetFileSize(const StringView& path);
- static bool IsReadOnly(const StringView& path);
- static bool SetReadOnly(const StringView& path, bool isReadOnly);
- static bool MoveFile(const StringView& dst, const StringView& src, bool overwrite = false);
static bool CopyFile(const StringView& dst, const StringView& src);
- static DateTime GetFileLastEditTime(const StringView& path);
+ static bool MoveFileToRecycleBin(const StringView& path);
static void GetSpecialFolderPath(const SpecialFolder type, String& result);
private:
static bool UrnEncodePath(const char *path, char *result, int maxLength);
- static bool getFilesFromDirectoryTop(Array& results, const char* path, const char* searchPattern);
- static bool getFilesFromDirectoryAll(Array& results, const char* path, const char* searchPattern);
static String getBaseName(const StringView& path);
static String getNameWithoutExtension(const StringView& path);
};
diff --git a/Source/Engine/Platform/Unix/UnixFileSystem.cpp b/Source/Engine/Platform/Unix/UnixFileSystem.cpp
new file mode 100644
index 000000000..1fb47f0f5
--- /dev/null
+++ b/Source/Engine/Platform/Unix/UnixFileSystem.cpp
@@ -0,0 +1,470 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#if PLATFORM_UNIX
+
+#include "UnixFileSystem.h"
+#include "Engine/Platform/File.h"
+#include "Engine/Core/Types/TimeSpan.h"
+#include "Engine/Core/Collections/Array.h"
+#include "Engine/Core/Math/Math.h"
+#include "Engine/Core/Log.h"
+#include "Engine/Utilities/StringConverter.h"
+#include
+#include
+#include
+#include
+#include
+
+#if PLATFORM_MAC || PLATFORM_IOS
+typedef StringAsANSI<> UnixString;
+#else
+typedef StringAsUTF8<> UnixString;
+#endif
+
+const DateTime UnixEpoch(1970, 1, 1);
+
+bool UnixFileSystem::CreateDirectory(const StringView& path)
+{
+ const UnixString pathAnsi(*path, path.Length());
+
+ // Skip if already exists
+ struct stat fileInfo;
+ if (stat(pathAnsi.Get(), &fileInfo) != -1 && S_ISDIR(fileInfo.st_mode))
+ {
+ return false;
+ }
+
+ // Recursively do it all again for the parent directory, if any
+ const int32 slashIndex = path.FindLast('/');
+ if (slashIndex > 1)
+ {
+ if (CreateDirectory(path.Substring(0, slashIndex)))
+ {
+ return true;
+ }
+ }
+
+ // Create the last directory on the path (the recursive calls will have taken care of the parent directories by now)
+ return mkdir(pathAnsi.Get(), 0755) != 0 && errno != EEXIST;
+}
+
+bool DeleteUnixPathTree(const char* path)
+{
+ size_t pathLength;
+ DIR* dir;
+ struct stat statPath, statEntry;
+ struct dirent* entry;
+
+ // Stat for the path
+ stat(path, &statPath);
+
+ // If path does not exists or is not dir - exit with status -1
+ if (S_ISDIR(statPath.st_mode) == 0)
+ {
+ // Is not directory
+ return true;
+ }
+
+ // If not possible to read the directory for this user
+ if ((dir = opendir(path)) == NULL)
+ {
+ // Cannot open directory
+ return true;
+ }
+
+ // The length of the path
+ pathLength = strlen(path);
+
+ // Iteration through entries in the directory
+ while ((entry = readdir(dir)) != NULL)
+ {
+ // Skip entries "." and ".."
+ if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
+ continue;
+
+ // Determinate a full path of an entry
+ char full_path[256];
+ ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(full_path));
+ strcpy(full_path, path);
+ strcat(full_path, "/");
+ strcat(full_path, entry->d_name);
+
+ // Stat for the entry
+ stat(full_path, &statEntry);
+
+ // Recursively remove a nested directory
+ if (S_ISDIR(statEntry.st_mode) != 0)
+ {
+ if (DeleteUnixPathTree(full_path))
+ return true;
+ continue;
+ }
+
+ // Remove a file object
+ if (unlink(full_path) != 0)
+ return true;
+ }
+
+ // Remove the devastated directory and close the object of it
+ if (rmdir(path) != 0)
+ return true;
+
+ closedir(dir);
+
+ return false;
+}
+
+bool UnixFileSystem::DeleteDirectory(const String& path, bool deleteContents)
+{
+ const UnixString pathANSI(*path, path.Length());
+ if (deleteContents)
+ {
+ return DeleteUnixPathTree(pathANSI.Get());
+ }
+ else
+ {
+ return rmdir(pathANSI.Get()) != 0;
+ }
+}
+
+bool UnixFileSystem::DirectoryExists(const StringView& path)
+{
+ struct stat fileInfo;
+ const UnixString pathANSI(*path, path.Length());
+ if (stat(pathANSI.Get(), &fileInfo) != -1)
+ {
+ return S_ISDIR(fileInfo.st_mode);
+ }
+ return false;
+}
+
+bool UnixFileSystem::DirectoryGetFiles(Array& results, const String& path, const Char* searchPattern, DirectorySearchOption option)
+{
+ const UnixString pathANSI(*path, path.Length());
+ const UnixString searchPatternANSI(searchPattern);
+
+ // Check if use only top directory
+ if (option == DirectorySearchOption::TopDirectoryOnly)
+ return getFilesFromDirectoryTop(results, pathANSI.Get(), searchPatternANSI.Get());
+ return getFilesFromDirectoryAll(results, pathANSI.Get(), searchPatternANSI.Get());
+}
+
+bool UnixFileSystem::GetChildDirectories(Array& results, const String& path)
+{
+ size_t pathLength;
+ DIR* dir;
+ struct stat statPath, statEntry;
+ struct dirent* entry;
+ const UnixString pathANSI(*path, path.Length());
+ const char* pathStr = pathANSI.Get();
+
+ // Stat for the path
+ stat(pathStr, &statPath);
+
+ // If path does not exists or is not dir - exit with status -1
+ if (S_ISDIR(statPath.st_mode) == 0)
+ {
+ // Is not directory
+ return true;
+ }
+
+ // If not possible to read the directory for this user
+ if ((dir = opendir(pathStr)) == NULL)
+ {
+ // Cannot open directory
+ return true;
+ }
+
+ // The length of the path
+ pathLength = strlen(pathStr);
+
+ // Iteration through entries in the directory
+ while ((entry = readdir(dir)) != NULL)
+ {
+ // Skip entries "." and ".."
+ if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
+ continue;
+
+ // Determinate a full path of an entry
+ char fullPath[256];
+ ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(fullPath));
+ strcpy(fullPath, pathStr);
+ strcat(fullPath, "/");
+ strcat(fullPath, entry->d_name);
+
+ // Stat for the entry
+ stat(fullPath, &statEntry);
+
+ // Check for directory
+ if (S_ISDIR(statEntry.st_mode) != 0)
+ {
+ // Add directory
+ results.Add(String(fullPath));
+ }
+ }
+
+ closedir(dir);
+
+ return false;
+}
+
+bool UnixFileSystem::FileExists(const StringView& path)
+{
+ struct stat fileInfo;
+ const UnixString pathANSI(*path, path.Length());
+ if (stat(pathANSI.Get(), &fileInfo) != -1)
+ {
+ return S_ISREG(fileInfo.st_mode);
+ }
+ return false;
+}
+
+bool UnixFileSystem::DeleteFile(const StringView& path)
+{
+ const UnixString pathANSI(*path, path.Length());
+ return unlink(pathANSI.Get()) != 0;
+}
+
+uint64 UnixFileSystem::GetFileSize(const StringView& path)
+{
+ struct stat fileInfo;
+ fileInfo.st_size = 0;
+ const UnixString pathANSI(*path, path.Length());
+ if (stat(pathANSI.Get(), &fileInfo) != -1)
+ {
+ // Check for directories
+ if (S_ISDIR(fileInfo.st_mode))
+ {
+ fileInfo.st_size = 0;
+ }
+ }
+ return fileInfo.st_size;
+}
+
+bool UnixFileSystem::IsReadOnly(const StringView& path)
+{
+ const UnixString pathANSI(*path, path.Length());
+ if (access(pathANSI.Get(), W_OK) == -1)
+ {
+ return errno == EACCES;
+ }
+ return false;
+}
+
+bool UnixFileSystem::SetReadOnly(const StringView& path, bool isReadOnly)
+{
+ const UnixString pathANSI(*path, path.Length());
+ struct stat fileInfo;
+ if (stat(pathANSI.Get(), &fileInfo) != -1)
+ {
+ if (isReadOnly)
+ {
+ fileInfo.st_mode &= ~S_IWUSR;
+ }
+ else
+ {
+ fileInfo.st_mode |= S_IWUSR;
+ }
+ return chmod(pathANSI.Get(), fileInfo.st_mode) == 0;
+ }
+ return false;
+}
+
+bool UnixFileSystem::MoveFile(const StringView& dst, const StringView& src, bool overwrite)
+{
+ if (!overwrite && FileExists(dst))
+ {
+ // Already exists
+ return true;
+ }
+
+ if (overwrite)
+ {
+ unlink(UnixString(*dst, dst.Length()).Get());
+ }
+ if (rename(UnixString(*src, src.Length()).Get(), UnixString(*dst, dst.Length()).Get()) != 0)
+ {
+ if (errno == EXDEV)
+ {
+ if (!CopyFile(dst, src))
+ {
+ unlink(UnixString(*src, src.Length()).Get());
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+}
+
+bool UnixFileSystem::getFilesFromDirectoryTop(Array& results, const char* path, const char* searchPattern)
+{
+ size_t pathLength;
+ struct stat statPath, statEntry;
+ struct dirent* entry;
+
+ // Stat for the path
+ stat(path, &statPath);
+
+ // If path does not exists or is not dir - exit with status -1
+ if (S_ISDIR(statPath.st_mode) == 0)
+ {
+ // Is not directory
+ return true;
+ }
+
+ // If not possible to read the directory for this user
+ DIR* dir = opendir(path);
+ if (dir == NULL)
+ {
+ // Cannot open directory
+ return true;
+ }
+
+ // The length of the path
+ pathLength = strlen(path);
+
+ // Iteration through entries in the directory
+ while ((entry = readdir(dir)) != NULL)
+ {
+ // Skip entries "." and ".."
+ if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
+ continue;
+
+ // Determinate a full path of an entry
+ char fullPath[256];
+ const int32 pathLength = strlen(entry->d_name);
+ ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(fullPath));
+ strcpy(fullPath, path);
+ strcat(fullPath, "/");
+ strcat(fullPath, entry->d_name);
+
+ // Stat for the entry
+ stat(fullPath, &statEntry);
+
+ // Check for file
+ if (S_ISREG(statEntry.st_mode) != 0)
+ {
+ // Validate with filter
+ const int32 fullPathLength = StringUtils::Length(fullPath);
+ const int32 searchPatternLength = StringUtils::Length(searchPattern);
+ if (searchPatternLength == 0 ||
+ StringUtils::Compare(searchPattern, "*") == 0 ||
+ StringUtils::Compare(searchPattern, "*.*") == 0)
+ {
+ // All files
+ }
+ else if (searchPattern[0] == '*' && searchPatternLength < fullPathLength && StringUtils::Compare(fullPath + fullPathLength - searchPatternLength + 1, searchPattern + 1, searchPatternLength - 1) == 0)
+ {
+ // Path ending
+ }
+ else if (searchPattern[0] == '*' && searchPatternLength > 2 && searchPattern[searchPatternLength-1] == '*')
+ {
+ // Contains pattern
+ bool match = false;
+ for (int32 i = 0; i < pathLength - searchPatternLength - 1; i++)
+ {
+ int32 len = Math::Min(searchPatternLength - 2, pathLength - i);
+ if (StringUtils::Compare(&entry->d_name[i], &searchPattern[1], len) == 0)
+ {
+ match = true;
+ break;
+ }
+ }
+ if (!match)
+ continue;
+ }
+ else
+ {
+ // TODO: implement all cases in a generic way
+ LOG(Warning, "DirectoryGetFiles: Wildcard filter is not implemented");
+ continue;
+ }
+
+ // Add file
+ results.Add(String(fullPath));
+ }
+ }
+
+ closedir(dir);
+
+ return false;
+}
+
+bool UnixFileSystem::getFilesFromDirectoryAll(Array& results, const char* path, const char* searchPattern)
+{
+ // Find all files in this directory
+ getFilesFromDirectoryTop(results, path, searchPattern);
+
+ size_t pathLength;
+ DIR* dir;
+ struct stat statPath, statEntry;
+ struct dirent* entry;
+
+ // Stat for the path
+ stat(path, &statPath);
+
+ // If path does not exists or is not dir - exit with status -1
+ if (S_ISDIR(statPath.st_mode) == 0)
+ {
+ // Is not directory
+ return true;
+ }
+
+ // If not possible to read the directory for this user
+ if ((dir = opendir(path)) == NULL)
+ {
+ // Cannot open directory
+ return true;
+ }
+
+ // The length of the path
+ pathLength = strlen(path);
+
+ // Iteration through entries in the directory
+ while ((entry = readdir(dir)) != NULL)
+ {
+ // Skip entries "." and ".."
+ if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
+ continue;
+
+ // Determinate a full path of an entry
+ char full_path[256];
+ ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(full_path));
+ strcpy(full_path, path);
+ strcat(full_path, "/");
+ strcat(full_path, entry->d_name);
+
+ // Stat for the entry
+ stat(full_path, &statEntry);
+
+ // Check for directory
+ if (S_ISDIR(statEntry.st_mode) != 0)
+ {
+ if (getFilesFromDirectoryAll(results, full_path, searchPattern))
+ {
+ closedir(dir);
+ return true;
+ }
+ }
+ }
+
+ closedir(dir);
+
+ return false;
+}
+
+DateTime UnixFileSystem::GetFileLastEditTime(const StringView& path)
+{
+ struct stat fileInfo;
+ const UnixString pathANSI(*path, path.Length());
+ if (stat(pathANSI.Get(), &fileInfo) == -1)
+ {
+ return DateTime::MinValue();
+ }
+
+ const TimeSpan timeSinceEpoch(0, 0, 0, fileInfo.st_mtime);
+ return UnixEpoch + timeSinceEpoch;
+}
+
+#endif
diff --git a/Source/Engine/Platform/Unix/UnixFileSystem.h b/Source/Engine/Platform/Unix/UnixFileSystem.h
new file mode 100644
index 000000000..4c7256aca
--- /dev/null
+++ b/Source/Engine/Platform/Unix/UnixFileSystem.h
@@ -0,0 +1,35 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#pragma once
+
+#if PLATFORM_UNIX
+
+#include "Engine/Platform/Base/FileSystemBase.h"
+
+///
+/// Unix platform implementation of filesystem service.
+///
+class FLAXENGINE_API UnixFileSystem : public FileSystemBase
+{
+public:
+ // [FileSystemBase]
+ static bool CreateDirectory(const StringView& path);
+ static bool DeleteDirectory(const String& path, bool deleteContents = true);
+ static bool DirectoryExists(const StringView& path);
+ static bool DirectoryGetFiles(Array& results, const String& path, const Char* searchPattern = TEXT("*"), DirectorySearchOption option = DirectorySearchOption::AllDirectories);
+ static bool GetChildDirectories(Array& results, const String& path);
+ static bool FileExists(const StringView& path);
+ static bool DeleteFile(const StringView& path);
+ static bool MoveFileToRecycleBin(const StringView& path);
+ static uint64 GetFileSize(const StringView& path);
+ static bool IsReadOnly(const StringView& path);
+ static bool SetReadOnly(const StringView& path, bool isReadOnly);
+ static bool MoveFile(const StringView& dst, const StringView& src, bool overwrite = false);
+ static DateTime GetFileLastEditTime(const StringView& path);
+
+private:
+ static bool getFilesFromDirectoryTop(Array& results, const char* path, const char* searchPattern);
+ static bool getFilesFromDirectoryAll(Array& results, const char* path, const char* searchPattern);
+};
+
+#endif
diff --git a/Source/Engine/Renderer/GBufferPass.cpp b/Source/Engine/Renderer/GBufferPass.cpp
index ee021d0ea..9d3684010 100644
--- a/Source/Engine/Renderer/GBufferPass.cpp
+++ b/Source/Engine/Renderer/GBufferPass.cpp
@@ -434,6 +434,7 @@ void GBufferPass::DrawDecals(RenderContext& renderContext, GPUTextureView* light
PROFILE_GPU_CPU("Decals");
auto context = GPUDevice::Instance->GetMainContext();
auto buffers = renderContext.Buffers;
+ GPUTextureView* depthBuffer = EnumHasAnyFlags(buffers->DepthBuffer->Flags(), GPUTextureFlags::ReadOnlyDepthView) ? buffers->DepthBuffer->ViewReadOnlyDepth() : nullptr;
// Sort decals from the lowest order to the highest order
Sorting::QuickSort(decals.Get(), decals.Count(), &SortDecal);
@@ -484,22 +485,22 @@ void GBufferPass::DrawDecals(RenderContext& renderContext, GPUTextureView* light
count++;
targetBuffers[2] = buffers->GBuffer1->View();
}
- context->SetRenderTarget(nullptr, ToSpan(targetBuffers, count));
+ context->SetRenderTarget(depthBuffer, ToSpan(targetBuffers, count));
break;
}
case MaterialDecalBlendingMode::Stain:
{
- context->SetRenderTarget(buffers->GBuffer0->View());
+ context->SetRenderTarget(depthBuffer, buffers->GBuffer0->View());
break;
}
case MaterialDecalBlendingMode::Normal:
{
- context->SetRenderTarget(buffers->GBuffer1->View());
+ context->SetRenderTarget(depthBuffer, buffers->GBuffer1->View());
break;
}
case MaterialDecalBlendingMode::Emissive:
{
- context->SetRenderTarget(lightBuffer);
+ context->SetRenderTarget(depthBuffer, lightBuffer);
break;
}
}
diff --git a/Source/Engine/Renderer/ShadowsPass.cpp b/Source/Engine/Renderer/ShadowsPass.cpp
index 968b271dd..fc93202e8 100644
--- a/Source/Engine/Renderer/ShadowsPass.cpp
+++ b/Source/Engine/Renderer/ShadowsPass.cpp
@@ -69,6 +69,7 @@ struct ShadowAtlasLightTile
{
ShadowsAtlasRectTile* RectTile;
ShadowsAtlasRectTile* StaticRectTile;
+ const ShadowsAtlasRectTile* LinkedRectTile;
Matrix WorldToShadow;
float FramesToUpdate; // Amount of frames (with fraction) until the next shadow update can happen
bool SkipUpdate;
@@ -94,6 +95,7 @@ struct ShadowAtlasLightTile
void ClearStatic()
{
StaticRectTile = nullptr;
+ LinkedRectTile = nullptr;
FramesToUpdate = 0;
SkipUpdate = false;
}
@@ -301,6 +303,7 @@ public:
GPUTexture* StaticShadowMapAtlas = nullptr;
DynamicTypedBuffer ShadowsBuffer;
GPUBufferView* ShadowsBufferView = nullptr;
+ const ShadowsCustomBuffer* LinkedShadows = nullptr;
RectPackAtlas Atlas;
RectPackAtlas StaticAtlas;
Dictionary Lights;
@@ -1044,6 +1047,32 @@ void ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render
}
}
+void ShadowsPass::ClearShadowMapTile(GPUContext* context, GPUConstantBuffer* quadShaderCB, QuadShaderData& quadShaderData) const
+{
+ // Color.r is used by PS_DepthClear in Quad shader to clear depth
+ quadShaderData.Color = Float4::One;
+ context->UpdateCB(quadShaderCB, &quadShaderData);
+ context->BindCB(0, quadShaderCB);
+
+ // Clear tile depth
+ context->SetState(_psDepthClear);
+ context->DrawFullscreenTriangle();
+}
+
+void ShadowsPass::CopyShadowMapTile(GPUContext* context, GPUConstantBuffer* quadShaderCB, QuadShaderData& quadShaderData, const GPUTexture* srcShadowMap, const ShadowsAtlasRectTile* srcTile) const
+{
+ // Color.xyzw is used by PS_DepthCopy in Quad shader to scale input texture UVs
+ const float staticAtlasResolutionInv = 1.0f / (float)srcShadowMap->Width();
+ quadShaderData.Color = Float4(srcTile->Width, srcTile->Height, srcTile->X, srcTile->Y) * staticAtlasResolutionInv;
+ context->UpdateCB(quadShaderCB, &quadShaderData);
+ context->BindCB(0, quadShaderCB);
+
+ // Copy tile depth
+ context->BindSR(0, srcShadowMap->View());
+ context->SetState(_psDepthCopy);
+ context->DrawFullscreenTriangle();
+}
+
void ShadowsPass::Dispose()
{
// Base
@@ -1068,26 +1097,26 @@ void ShadowsPass::SetupShadows(RenderContext& renderContext, RenderContextBatch&
// Early out and skip shadows setup if no lights is actively casting shadows
// RenderBuffers will automatically free any old ShadowsCustomBuffer after a few frames if we don't update LastFrameUsed
Array shadowedLights;
- for (auto& light : renderContext.List->DirectionalLights)
+ if (_shadowMapFormat != PixelFormat::Unknown && EnumHasAllFlags(renderContext.View.Flags, ViewFlags::Shadows) && !checkIfSkipPass())
{
- if (light.CanRenderShadow(renderContext.View))
- shadowedLights.Add(&light);
- }
- for (auto& light : renderContext.List->SpotLights)
- {
- if (light.CanRenderShadow(renderContext.View))
- shadowedLights.Add(&light);
- }
- for (auto& light : renderContext.List->PointLights)
- {
- if (light.CanRenderShadow(renderContext.View))
- shadowedLights.Add(&light);
+ for (auto& light : renderContext.List->DirectionalLights)
+ {
+ if (light.CanRenderShadow(renderContext.View))
+ shadowedLights.Add(&light);
+ }
+ for (auto& light : renderContext.List->SpotLights)
+ {
+ if (light.CanRenderShadow(renderContext.View))
+ shadowedLights.Add(&light);
+ }
+ for (auto& light : renderContext.List->PointLights)
+ {
+ if (light.CanRenderShadow(renderContext.View))
+ shadowedLights.Add(&light);
+ }
}
const auto currentFrame = Engine::FrameCount;
- if (_shadowMapFormat == PixelFormat::Unknown ||
- EnumHasNoneFlags(renderContext.View.Flags, ViewFlags::Shadows) ||
- checkIfSkipPass() ||
- shadowedLights.IsEmpty())
+ if (shadowedLights.IsEmpty())
{
// Invalidate any existing custom buffer that could have been used by the same task (eg. when rendering 6 sides of env probe)
if (auto* old = (ShadowsCustomBuffer*)renderContext.Buffers->FindCustomBuffer(TEXT("Shadows"), false))
@@ -1100,11 +1129,14 @@ void ShadowsPass::SetupShadows(RenderContext& renderContext, RenderContextBatch&
// Initialize shadow atlas
auto& shadows = *renderContext.Buffers->GetCustomBuffer(TEXT("Shadows"), false);
+ shadows.LinkedShadows = renderContext.Buffers->FindLinkedBuffer(TEXT("Shadows"));
+ if (shadows.LinkedShadows && (shadows.LinkedShadows->LastFrameUsed != currentFrame || shadows.LinkedShadows->ViewOrigin != renderContext.View.Origin))
+ shadows.LinkedShadows = nullptr; // Don't use incompatible linked shadows buffer
if (shadows.LastFrameUsed == currentFrame)
shadows.Reset();
shadows.LastFrameUsed = currentFrame;
shadows.MaxShadowsQuality = Math::Clamp(Math::Min((int32)Graphics::ShadowsQuality, (int32)renderContext.View.MaxShadowsQuality), 0, (int32)Quality::MAX - 1);
- shadows.EnableStaticShadows = !renderContext.View.IsOfflinePass && !renderContext.View.IsSingleFrame;
+ shadows.EnableStaticShadows = !renderContext.View.IsOfflinePass && !renderContext.View.IsSingleFrame && !shadows.LinkedShadows;
int32 atlasResolution;
switch (Graphics::ShadowMapsQuality)
{
@@ -1323,6 +1355,29 @@ RETRY_ATLAS_SETUP:
SetupLight(shadows, renderContext, renderContextBatch, *(RenderSpotLightData*)light, atlasLight);
else //if (light->IsDirectionalLight)
SetupLight(shadows, renderContext, renderContextBatch, *(RenderDirectionalLightData*)light, atlasLight);
+
+ // Check if that light exists in linked shadows buffer to reuse shadow maps
+ const ShadowAtlasLight* linkedAtlasLight;
+ if (shadows.LinkedShadows && ((linkedAtlasLight = shadows.LinkedShadows->Lights.TryGet(light->ID))) && linkedAtlasLight->TilesCount == atlasLight.TilesCount)
+ {
+ for (int32 tileIndex = 0; tileIndex < atlasLight.TilesCount; tileIndex++)
+ {
+ auto& tile = atlasLight.Tiles[tileIndex];
+ tile.LinkedRectTile = nullptr;
+ auto& linkedTile = linkedAtlasLight->Tiles[tileIndex];
+
+ // Check if both lights use the same projections
+ if (tile.WorldToShadow == linkedTile.WorldToShadow && linkedTile.RectTile)
+ {
+ tile.LinkedRectTile = linkedTile.RectTile;
+ }
+ }
+ }
+ else
+ {
+ for (auto& tile : atlasLight.Tiles)
+ tile.LinkedRectTile = nullptr;
+ }
}
}
if (shadows.StaticAtlas.IsInitialized())
@@ -1493,29 +1548,21 @@ void ShadowsPass::RenderShadowMaps(RenderContextBatch& renderContextBatch)
// Set viewport for tile
context->SetViewportAndScissors(tile.CachedViewport);
- if (tile.StaticRectTile && atlasLight.StaticState == ShadowAtlasLight::CopyStaticShadow)
+ if (tile.LinkedRectTile)
{
- // Color.xyzw is used by PS_DepthCopy in Quad shader to scale input texture UVs
- const float staticAtlasResolutionInv = 1.0f / shadows.StaticShadowMapAtlas->Width();
- quadShaderData.Color = Float4(tile.StaticRectTile->Width, tile.StaticRectTile->Height, tile.StaticRectTile->X, tile.StaticRectTile->Y) * staticAtlasResolutionInv;
- context->UpdateCB(quadShaderCB, &quadShaderData);
- context->BindCB(0, quadShaderCB);
-
- // Copy tile depth
- context->BindSR(0, shadows.StaticShadowMapAtlas->View());
- context->SetState(_psDepthCopy);
- context->DrawFullscreenTriangle();
+ // Copy linked shadow
+ ASSERT(shadows.LinkedShadows);
+ CopyShadowMapTile(context, quadShaderCB, quadShaderData, shadows.LinkedShadows->ShadowMapAtlas, tile.LinkedRectTile);
+ }
+ else if (tile.StaticRectTile && atlasLight.StaticState == ShadowAtlasLight::CopyStaticShadow)
+ {
+ // Copy static shadow
+ CopyShadowMapTile(context, quadShaderCB, quadShaderData, shadows.StaticShadowMapAtlas, tile.StaticRectTile);
}
else if (!shadows.ClearShadowMapAtlas)
{
- // Color.r is used by PS_DepthClear in Quad shader to clear depth
- quadShaderData.Color = Float4::One;
- context->UpdateCB(quadShaderCB, &quadShaderData);
- context->BindCB(0, quadShaderCB);
-
- // Clear tile depth
- context->SetState(_psDepthClear);
- context->DrawFullscreenTriangle();
+ // Clear shadow
+ ClearShadowMapTile(context, quadShaderCB, quadShaderData);
}
// Draw objects depth
diff --git a/Source/Engine/Renderer/ShadowsPass.h b/Source/Engine/Renderer/ShadowsPass.h
index 748a7c084..8e64a205d 100644
--- a/Source/Engine/Renderer/ShadowsPass.h
+++ b/Source/Engine/Renderer/ShadowsPass.h
@@ -60,6 +60,8 @@ private:
static void SetupLight(ShadowsCustomBuffer& shadows, RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderDirectionalLightData& light, ShadowAtlasLight& atlasLight);
static void SetupLight(ShadowsCustomBuffer& shadows, RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderPointLightData& light, ShadowAtlasLight& atlasLight);
static void SetupLight(ShadowsCustomBuffer& shadows, RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderSpotLightData& light, ShadowAtlasLight& atlasLight);
+ void ClearShadowMapTile(GPUContext* context, GPUConstantBuffer* quadShaderCB, struct QuadShaderData& quadShaderData) const;
+ void CopyShadowMapTile(GPUContext* context, GPUConstantBuffer* quadShaderCB, struct QuadShaderData& quadShaderData, const GPUTexture* srcShadowMap, const struct ShadowsAtlasRectTile* srcTile) const;
#if COMPILE_WITH_DEV_ENV
void OnShaderReloading(Asset* obj)
diff --git a/Source/Engine/Scripting/Scripting.cpp b/Source/Engine/Scripting/Scripting.cpp
index d1203a1f3..cc77f2337 100644
--- a/Source/Engine/Scripting/Scripting.cpp
+++ b/Source/Engine/Scripting/Scripting.cpp
@@ -108,6 +108,7 @@ namespace
MMethod* _method_LateFixedUpdate = nullptr;
MMethod* _method_Draw = nullptr;
MMethod* _method_Exit = nullptr;
+ Array> UpdateActions;
Dictionary> _nonNativeModules;
#if USE_EDITOR
bool LastBinariesLoadTriggeredCompilation = false;
@@ -245,6 +246,27 @@ void ScriptingService::Update()
PROFILE_CPU_NAMED("Scripting::Update");
INVOKE_EVENT(Update);
+ // Flush update actions
+ _objectsLocker.Lock();
+ int32 count = UpdateActions.Count();
+ for (int32 i = 0; i < count; i++)
+ {
+ UpdateActions[i]();
+ }
+ int32 newlyAdded = UpdateActions.Count() - count;
+ if (newlyAdded == 0)
+ UpdateActions.Clear();
+ else
+ {
+ // Someone added another action within current callback
+ Array> tmp;
+ for (int32 i = newlyAdded; i < UpdateActions.Count(); i++)
+ tmp.Add(UpdateActions[i]);
+ UpdateActions.Clear();
+ UpdateActions.Add(tmp);
+ }
+ _objectsLocker.Unlock();
+
#if defined(USE_NETCORE) && !USE_EDITOR
// Force GC to run in background periodically to avoid large blocking collections causing hitches
if (Time::Update.TicksCount % 60 == 0)
@@ -311,6 +333,13 @@ void Scripting::ProcessBuildInfoPath(String& path, const String& projectFolderPa
path = projectFolderPath / path;
}
+void Scripting::InvokeOnUpdate(const Function& action)
+{
+ _objectsLocker.Lock();
+ UpdateActions.Add(action);
+ _objectsLocker.Unlock();
+}
+
bool Scripting::LoadBinaryModules(const String& path, const String& projectFolderPath)
{
PROFILE_CPU_NAMED("LoadBinaryModules");
diff --git a/Source/Engine/Scripting/Scripting.h b/Source/Engine/Scripting/Scripting.h
index be5e9ca34..1e7daf6f0 100644
--- a/Source/Engine/Scripting/Scripting.h
+++ b/Source/Engine/Scripting/Scripting.h
@@ -230,6 +230,11 @@ public:
static void ProcessBuildInfoPath(String& path, const String& projectFolderPath);
+ ///
+ /// Calls the given action on the next scripting update.
+ ///
+ /// The action to invoke.
+ static void InvokeOnUpdate(const Function& action);
private:
static bool LoadBinaryModules(const String& path, const String& projectFolderPath);
diff --git a/Source/Engine/Serialization/MemoryReadStream.cpp b/Source/Engine/Serialization/MemoryReadStream.cpp
index b84cb3af6..e6a1acea1 100644
--- a/Source/Engine/Serialization/MemoryReadStream.cpp
+++ b/Source/Engine/Serialization/MemoryReadStream.cpp
@@ -61,7 +61,11 @@ void MemoryReadStream::ReadBytes(void* data, uint32 bytes)
{
if (bytes > 0)
{
- ASSERT(data && GetLength() - GetPosition() >= bytes);
+ if (!data || GetLength() - GetPosition() < bytes)
+ {
+ _hasError = true;
+ return;
+ }
Platform::MemoryCopy(data, _position, bytes);
_position += bytes;
}
diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp
index 608f57ec3..6bd88f2ae 100644
--- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp
+++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp
@@ -34,7 +34,6 @@ MaterialValue* MaterialGenerator::sampleTextureRaw(Node* caller, Value& value, B
const bool isArray = texture->Type == MaterialParameterType::GPUTextureArray;
const bool isVolume = texture->Type == MaterialParameterType::GPUTextureVolume;
const bool isNormalMap = texture->Type == MaterialParameterType::NormalMap;
- const bool canUseSample = CanUseSample(_treeType);
MaterialGraphBox* valueBox = parent->GetBox(1);
// Check if has variable assigned
@@ -63,6 +62,16 @@ MaterialValue* MaterialGenerator::sampleTextureRaw(Node* caller, Value& value, B
// Check if hasn't been sampled during that tree eating
if (valueBox->Cache.IsInvalid())
{
+ bool canUseSample = CanUseSample(_treeType);
+ String mipLevel = TEXT("0");
+ const auto layer = GetRootLayer();
+ if (layer && layer->Domain == MaterialDomain::Decal && _treeType == MaterialTreeType::PixelShader)
+ {
+ // Decals use computed mip level due to ddx/ddy being unreliable
+ canUseSample = false;
+ mipLevel = String::Format(TEXT("CalculateTextureMipmap(input, {})"), texture->ShaderName);
+ }
+
// Check if use custom UVs
String uv;
MaterialGraphBox* uvBox = parent->GetBox(0);
@@ -94,10 +103,10 @@ MaterialValue* MaterialGenerator::sampleTextureRaw(Node* caller, Value& value, B
// Sample texture
if (isNormalMap)
{
- const Char* format = canUseSample ? TEXT("{0}.Sample({1}, {2}).xyz") : TEXT("{0}.SampleLevel({1}, {2}, 0).xyz");
+ const Char* format = canUseSample ? TEXT("{0}.Sample({1}, {2}).xyz") : TEXT("{0}.SampleLevel({1}, {2}, {3}).xyz");
// Sample encoded normal map
- const String sampledValue = String::Format(format, texture->ShaderName, sampler, uv);
+ const String sampledValue = String::Format(format, texture->ShaderName, sampler, uv, mipLevel);
const auto normalVector = writeLocal(VariantType::Float3, sampledValue, parent);
// Decode normal vector
@@ -123,12 +132,12 @@ MaterialValue* MaterialGenerator::sampleTextureRaw(Node* caller, Value& value, B
}
else*/
{
- format = canUseSample ? TEXT("{0}.Sample({1}, {2})") : TEXT("{0}.SampleLevel({1}, {2}, 0)");
+ format = canUseSample ? TEXT("{0}.Sample({1}, {2})") : TEXT("{0}.SampleLevel({1}, {2}, {3})");
}
}
// Sample texture
- String sampledValue = String::Format(format, texture->ShaderName, sampler, uv, _ddx.Value, _ddy.Value);
+ String sampledValue = String::Format(format, texture->ShaderName, sampler, uv, mipLevel);
valueBox->Cache = writeLocal(VariantType::Float4, sampledValue, parent);
}
}
diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Tools.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Tools.cpp
index 975bbabc6..056ddd7ce 100644
--- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Tools.cpp
+++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Tools.cpp
@@ -48,10 +48,8 @@ void MaterialGenerator::ProcessGroupTools(Box* box, Node* node, Value& value)
}
// Time
case 3:
- {
- value = getTime;
+ value = box->ID == 1 ? getUnscaledTime : getTime;
break;
- }
// Panner
case 6:
{
diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp
index 2aea40c94..9c42d1865 100644
--- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp
+++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp
@@ -107,6 +107,7 @@ bool FeatureData::Init()
MaterialValue MaterialGenerator::getUVs(VariantType::Float2, TEXT("input.TexCoord"));
MaterialValue MaterialGenerator::getTime(VariantType::Float, TEXT("TimeParam"));
+MaterialValue MaterialGenerator::getUnscaledTime(VariantType::Float, TEXT("UnscaledTimeParam"));
MaterialValue MaterialGenerator::getNormal(VariantType::Float3, TEXT("input.TBN[2]"));
MaterialValue MaterialGenerator::getNormalZero(VariantType::Float3, TEXT("float3(0, 0, 1)"));
MaterialValue MaterialGenerator::getVertexColor(VariantType::Float4, TEXT("GetVertexColor(input)"));
diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.h b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.h
index 59c34c55f..706929d75 100644
--- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.h
+++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.h
@@ -120,7 +120,6 @@ private:
MaterialValue _ddx, _ddy, _cameraVector;
public:
-
MaterialGenerator();
~MaterialGenerator();
@@ -211,6 +210,7 @@ public:
static MaterialValue getUVs;
static MaterialValue getTime;
+ static MaterialValue getUnscaledTime;
static MaterialValue getNormal;
static MaterialValue getNormalZero;
static MaterialValue getVertexColor;
diff --git a/Source/Engine/UI/GUI/Brushes/MaterialBrush.cs b/Source/Engine/UI/GUI/Brushes/MaterialBrush.cs
index 6210ca2d0..13e9dab24 100644
--- a/Source/Engine/UI/GUI/Brushes/MaterialBrush.cs
+++ b/Source/Engine/UI/GUI/Brushes/MaterialBrush.cs
@@ -25,7 +25,7 @@ namespace FlaxEngine.GUI
/// Initializes a new instance of the struct.
///
/// The material.
- public MaterialBrush(Material material)
+ public MaterialBrush(MaterialBase material)
{
Material = material;
}
diff --git a/Source/Engine/Utilities/HtmlParser.cs b/Source/Engine/Utilities/HtmlParser.cs
index 175b0247a..0b189ff83 100644
--- a/Source/Engine/Utilities/HtmlParser.cs
+++ b/Source/Engine/Utilities/HtmlParser.cs
@@ -134,6 +134,10 @@ namespace FlaxEngine.Utilities
if (isLeadingSlash)
Move();
+ // Dont process if wrong slash is used.
+ if (c =='\\')
+ return false;
+
// Parse tag
bool result = ParseTag(ref tag, name);
@@ -206,6 +210,10 @@ namespace FlaxEngine.Utilities
SkipWhitespace();
while (Peek() != '>')
{
+ // Return false if start of new html tag is detected.
+ if (Peek() == '<')
+ return false;
+
if (Peek() == '/')
{
// Handle trailing forward slash
diff --git a/Source/Shaders/Lighting.hlsl b/Source/Shaders/Lighting.hlsl
index 1429e45d1..9b14db5ed 100644
--- a/Source/Shaders/Lighting.hlsl
+++ b/Source/Shaders/Lighting.hlsl
@@ -122,6 +122,10 @@ float4 GetLighting(float3 viewPos, LightData lightData, GBufferSample gBuffer, f
// Calculate shadow
ShadowSample shadow = GetShadow(lightData, gBuffer, shadowMask);
+#if !LIGHTING_NO_DIRECTIONAL
+ // Directional shadowing
+ shadow.SurfaceShadow *= NoL;
+#endif
// Calculate attenuation
if (isRadial)
@@ -135,11 +139,6 @@ float4 GetLighting(float3 viewPos, LightData lightData, GBufferSample gBuffer, f
shadow.TransmissionShadow *= attenuation;
}
-#if !LIGHTING_NO_DIRECTIONAL
- // Reduce shadow mapping artifacts
- shadow.SurfaceShadow *= saturate(NoL * 6.0f - 0.2f) * NoL;
-#endif
-
BRANCH
if (shadow.SurfaceShadow + shadow.TransmissionShadow > 0)
{
diff --git a/Source/Shaders/MaterialCommon.hlsl b/Source/Shaders/MaterialCommon.hlsl
index f835b2327..897b01d7e 100644
--- a/Source/Shaders/MaterialCommon.hlsl
+++ b/Source/Shaders/MaterialCommon.hlsl
@@ -176,6 +176,8 @@ cbuffer ViewData : register(b1)
float4 TemporalAAJitter;
float3 LargeWorldsChunkIndex;
float LargeWorldsChunkSize;
+ float3 ViewPadding0;
+ float UnscaledTimeParam;
};
#endif
diff --git a/Source/Shaders/SSAO.shader b/Source/Shaders/SSAO.shader
index 6fc3c523c..5116b3cb9 100644
--- a/Source/Shaders/SSAO.shader
+++ b/Source/Shaders/SSAO.shader
@@ -56,7 +56,7 @@ static const uint g_numTaps[4] = { 3, 5, 8, 12 };
#define SSAO_NORMAL_BASED_EDGES_ENABLE_AT_QUALITY_PRESET (2) // to disable simply set to 99 or similar
#define SSAO_NORMAL_BASED_EDGES_DOT_THRESHOLD (0.5) // use 0-0.1 for super-sharp normal-based edges
//
-#define SSAO_DETAIL_AO_ENABLE_AT_QUALITY_PRESET (1) // whether to use DetailAOStrength; to disable simply set to 99 or similar
+#define SSAO_DETAIL_AO_ENABLE_AT_QUALITY_PRESET (99) // whether to use DetailAOStrength; to disable simply set to 99 or similar
//
#define SSAO_DEPTH_MIPS_ENABLE_AT_QUALITY_PRESET (99) // !!warning!! the MIP generation on the C++ side will be enabled on quality preset 2 regardless of this value, so if changing here, change the C++ side too
#define SSAO_DEPTH_MIPS_GLOBAL_OFFSET (-4.3) // best noise/quality/performance tradeoff, found empirically
diff --git a/Source/Tools/Flax.Build/Build/Builder.Projects.cs b/Source/Tools/Flax.Build/Build/Builder.Projects.cs
index 008e9d982..59538364c 100644
--- a/Source/Tools/Flax.Build/Build/Builder.Projects.cs
+++ b/Source/Tools/Flax.Build/Build/Builder.Projects.cs
@@ -513,6 +513,7 @@ namespace Flax.Build
// Combine build options from this module
project.CSharp.SystemReferences.AddRange(moduleBuildOptions.ScriptingAPI.SystemReferences);
project.CSharp.FileReferences.AddRange(moduleBuildOptions.ScriptingAPI.FileReferences);
+ project.CSharp.NugetPackageReferences.AddRange(moduleBuildOptions.NugetPackageReferences);
// Find references based on the modules dependencies (external or from projects)
foreach (var dependencyName in moduleBuildOptions.PublicDependencies.Concat(moduleBuildOptions.PrivateDependencies))
diff --git a/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs b/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs
index 2619f1a17..23646a3d7 100644
--- a/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs
+++ b/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs
@@ -109,6 +109,7 @@ namespace Flax.Build
// Merge module into target environment
buildData.TargetOptions.LinkEnv.InputFiles.AddRange(moduleOptions.OutputFiles);
buildData.TargetOptions.DependencyFiles.AddRange(moduleOptions.DependencyFiles);
+ buildData.TargetOptions.NugetPackageReferences.AddRange(moduleOptions.NugetPackageReferences);
buildData.TargetOptions.OptionalDependencyFiles.AddRange(moduleOptions.OptionalDependencyFiles);
buildData.TargetOptions.Libraries.AddRange(moduleOptions.Libraries);
buildData.TargetOptions.DelayLoadLibraries.AddRange(moduleOptions.DelayLoadLibraries);
@@ -141,6 +142,19 @@ namespace Flax.Build
var dstFile = Path.Combine(outputPath, Path.GetFileName(srcFile));
graph.AddCopyFile(dstFile, srcFile);
}
+
+ if (targetBuildOptions.NugetPackageReferences.Any())
+ {
+ var nugetPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages");
+ foreach (var reference in targetBuildOptions.NugetPackageReferences)
+ {
+ var path = Path.Combine(nugetPath, reference.Name, reference.Version, "lib", reference.Framework, $"{reference.Name}.dll");
+ if (!File.Exists(path))
+ Utilities.RestoreNugetPackages(graph, target);
+ var dstFile = Path.Combine(outputPath, Path.GetFileName(path));
+ graph.AddCopyFile(dstFile, path);
+ }
+ }
}
}
@@ -283,6 +297,18 @@ namespace Flax.Build
args.Add(string.Format("/reference:\"{0}{1}.dll\"", referenceAssemblies, reference));
foreach (var reference in fileReferences)
args.Add(string.Format("/reference:\"{0}\"", reference));
+
+ // Reference Nuget package
+ if (buildData.TargetOptions.NugetPackageReferences.Any())
+ {
+ var nugetPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages");
+ foreach (var reference in buildOptions.NugetPackageReferences)
+ {
+ var path = Path.Combine(nugetPath, reference.Name, reference.Version, "lib", reference.Framework, $"{reference.Name}.dll");
+ args.Add(string.Format("/reference:\"{0}\"", path));
+ }
+ }
+
#if USE_NETCORE
foreach (var systemAnalyzer in buildOptions.ScriptingAPI.SystemAnalyzers)
args.Add(string.Format("/analyzer:\"{0}{1}.dll\"", referenceAnalyzers, systemAnalyzer));
diff --git a/Source/Tools/Flax.Build/Build/EngineTarget.cs b/Source/Tools/Flax.Build/Build/EngineTarget.cs
index 6a44991a2..827205e91 100644
--- a/Source/Tools/Flax.Build/Build/EngineTarget.cs
+++ b/Source/Tools/Flax.Build/Build/EngineTarget.cs
@@ -220,6 +220,7 @@ namespace Flax.Build
exeBuildOptions.LinkEnv.InputLibraries.Add(Path.Combine(buildOptions.OutputFolder, buildOptions.Platform.GetLinkOutputFileName(LibraryName, engineLibraryType)));
exeBuildOptions.LinkEnv.InputFiles.AddRange(mainModuleOptions.OutputFiles);
exeBuildOptions.DependencyFiles.AddRange(mainModuleOptions.DependencyFiles);
+ exeBuildOptions.NugetPackageReferences.AddRange(mainModuleOptions.NugetPackageReferences);
exeBuildOptions.OptionalDependencyFiles.AddRange(mainModuleOptions.OptionalDependencyFiles);
exeBuildOptions.Libraries.AddRange(mainModuleOptions.Libraries);
exeBuildOptions.DelayLoadLibraries.AddRange(mainModuleOptions.DelayLoadLibraries);
diff --git a/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs b/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs
index 95695e203..13ecd1982 100644
--- a/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs
+++ b/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs
@@ -70,6 +70,40 @@ namespace Flax.Build.NativeCpp
Annotations,
}
+ ///
+ /// Defines a Nuget Package
+ ///
+ public struct NugetPackage
+ {
+ ///
+ /// The name of the nuget package.
+ ///
+ public string Name;
+
+ ///
+ /// The version of the nuget package.
+ ///
+ public string Version;
+
+ ///
+ /// The target framework. ex. net8.0, netstandard2.1
+ ///
+ public string Framework;
+
+ ///
+ /// Initialize the nuget package.
+ ///
+ /// The name of the package.
+ /// The version of the package.
+ /// The target framework. ex. net8.0, netstandard2.1, etc.
+ public NugetPackage(string name, string version, string framework)
+ {
+ Name = name;
+ Version = version;
+ Framework = framework;
+ }
+ }
+
///
/// The native C++ module build settings container.
///
@@ -129,6 +163,11 @@ namespace Flax.Build.NativeCpp
/// The collection of the modules that are required by this module (for linking).
///
public List PrivateDependencies = new List();
+
+ ///
+ /// The nuget package references.
+ ///
+ public List NugetPackageReferences = new List();
///
/// The collection of defines with preprocessing symbol for a source files of this module. Inherited by the modules that include it.
diff --git a/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs b/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs
index 1d0d3c695..18459ddb5 100644
--- a/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs
+++ b/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs
@@ -425,6 +425,7 @@ namespace Flax.Build
moduleOptions.LinkEnv.InputFiles.AddRange(dependencyOptions.OutputFiles);
moduleOptions.DependencyFiles.AddRange(dependencyOptions.DependencyFiles);
moduleOptions.OptionalDependencyFiles.AddRange(dependencyOptions.OptionalDependencyFiles);
+ moduleOptions.NugetPackageReferences.AddRange(dependencyOptions.NugetPackageReferences);
moduleOptions.PrivateIncludePaths.AddRange(dependencyOptions.PublicIncludePaths);
moduleOptions.Libraries.AddRange(dependencyOptions.Libraries);
moduleOptions.DelayLoadLibraries.AddRange(dependencyOptions.DelayLoadLibraries);
@@ -440,6 +441,7 @@ namespace Flax.Build
moduleOptions.LinkEnv.InputFiles.AddRange(dependencyOptions.OutputFiles);
moduleOptions.DependencyFiles.AddRange(dependencyOptions.DependencyFiles);
moduleOptions.OptionalDependencyFiles.AddRange(dependencyOptions.OptionalDependencyFiles);
+ moduleOptions.NugetPackageReferences.AddRange(dependencyOptions.NugetPackageReferences);
moduleOptions.PublicIncludePaths.AddRange(dependencyOptions.PublicIncludePaths);
moduleOptions.Libraries.AddRange(dependencyOptions.Libraries);
moduleOptions.DelayLoadLibraries.AddRange(dependencyOptions.DelayLoadLibraries);
@@ -934,6 +936,7 @@ namespace Flax.Build
buildData.TargetOptions.LinkEnv.InputFiles.AddRange(moduleOptions.OutputFiles);
buildData.TargetOptions.DependencyFiles.AddRange(moduleOptions.DependencyFiles);
buildData.TargetOptions.OptionalDependencyFiles.AddRange(moduleOptions.OptionalDependencyFiles);
+ buildData.TargetOptions.NugetPackageReferences.AddRange(moduleOptions.NugetPackageReferences);
buildData.TargetOptions.Libraries.AddRange(moduleOptions.Libraries);
buildData.TargetOptions.DelayLoadLibraries.AddRange(moduleOptions.DelayLoadLibraries);
buildData.TargetOptions.ScriptingAPI.Add(moduleOptions.ScriptingAPI);
@@ -1054,6 +1057,19 @@ namespace Flax.Build
var dstFile = Path.Combine(outputPath, Path.GetFileName(srcFile));
graph.AddCopyFile(dstFile, srcFile);
}
+
+ if (targetBuildOptions.NugetPackageReferences.Any())
+ {
+ var nugetPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages");
+ foreach (var reference in targetBuildOptions.NugetPackageReferences)
+ {
+ var path = Path.Combine(nugetPath, reference.Name, reference.Version, "lib", reference.Framework, $"{reference.Name}.dll");
+ if (!File.Exists(path))
+ Utilities.RestoreNugetPackages(graph, target);
+ var dstFile = Path.Combine(outputPath, Path.GetFileName(path));
+ graph.AddCopyFile(dstFile, path);
+ }
+ }
}
}
@@ -1192,6 +1208,7 @@ namespace Flax.Build
buildData.TargetOptions.ExternalModules.AddRange(moduleOptions.ExternalModules);
buildData.TargetOptions.DependencyFiles.AddRange(moduleOptions.DependencyFiles);
buildData.TargetOptions.OptionalDependencyFiles.AddRange(moduleOptions.OptionalDependencyFiles);
+ buildData.TargetOptions.NugetPackageReferences.AddRange(moduleOptions.NugetPackageReferences);
}
}
}
@@ -1253,6 +1270,19 @@ namespace Flax.Build
var dstFile = Path.Combine(outputPath, Path.GetFileName(srcFile));
graph.AddCopyFile(dstFile, srcFile);
}
+
+ if (targetBuildOptions.NugetPackageReferences.Any())
+ {
+ var nugetPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages");
+ foreach (var reference in targetBuildOptions.NugetPackageReferences)
+ {
+ var path = Path.Combine(nugetPath, reference.Name, reference.Version, "lib", reference.Framework, $"{reference.Name}.dll");
+ if (!File.Exists(path))
+ Utilities.RestoreNugetPackages(graph, target);
+ var dstFile = Path.Combine(outputPath, Path.GetFileName(path));
+ graph.AddCopyFile(dstFile, path);
+ }
+ }
}
}
diff --git a/Source/Tools/Flax.Build/Projects/Project.cs b/Source/Tools/Flax.Build/Projects/Project.cs
index 5aa77a453..46ae6102b 100644
--- a/Source/Tools/Flax.Build/Projects/Project.cs
+++ b/Source/Tools/Flax.Build/Projects/Project.cs
@@ -229,6 +229,11 @@ namespace Flax.Build.Projects
/// The .Net libraries references (dll or exe files paths).
///
public HashSet FileReferences;
+
+ ///
+ /// The nuget references.
+ ///
+ public HashSet NugetPackageReferences;
///
/// The output folder path (optional).
@@ -248,6 +253,7 @@ namespace Flax.Build.Projects
{
SystemReferences = new HashSet(),
FileReferences = new HashSet(),
+ NugetPackageReferences = new HashSet(),
};
///
diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/CSProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/CSProjectGenerator.cs
index da6907f78..35bc7b0ac 100644
--- a/Source/Tools/Flax.Build/Projects/VisualStudio/CSProjectGenerator.cs
+++ b/Source/Tools/Flax.Build/Projects/VisualStudio/CSProjectGenerator.cs
@@ -175,6 +175,19 @@ namespace Flax.Build.Projects.VisualStudio
csProjectFileContent.AppendLine(" ");
}
+ // Nuget
+ if (project.CSharp.NugetPackageReferences.Any())
+ {
+ csProjectFileContent.AppendLine(" ");
+
+ foreach (var reference in project.CSharp.NugetPackageReferences)
+ {
+ csProjectFileContent.AppendLine(string.Format(" ", reference.Name, reference.Version));
+ }
+
+ csProjectFileContent.AppendLine(" ");
+ }
+
// References
csProjectFileContent.AppendLine(" ");
diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs
index df1424e95..f126bd3ef 100644
--- a/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs
+++ b/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs
@@ -134,6 +134,20 @@ namespace Flax.Build.Projects.VisualStudio
}
// References
+
+ // Nuget
+ if (project.CSharp.NugetPackageReferences.Any())
+ {
+ csProjectFileContent.AppendLine(" ");
+
+ foreach (var reference in project.CSharp.NugetPackageReferences)
+ {
+ csProjectFileContent.AppendLine(string.Format(" ", reference.Name, reference.Version));
+ }
+
+ csProjectFileContent.AppendLine(" ");
+ csProjectFileContent.AppendLine("");
+ }
csProjectFileContent.AppendLine(" ");
diff --git a/Source/Tools/Flax.Build/Utilities/Utilities.cs b/Source/Tools/Flax.Build/Utilities/Utilities.cs
index 85f791d09..0ce5c8b7b 100644
--- a/Source/Tools/Flax.Build/Utilities/Utilities.cs
+++ b/Source/Tools/Flax.Build/Utilities/Utilities.cs
@@ -16,6 +16,47 @@ namespace Flax.Build
///
public static class Utilities
{
+ ///
+ /// Gets the .Net SDK path.
+ ///
+ /// The path.
+ public static string GetDotNetPath()
+ {
+ var buildPlatform = Platform.BuildTargetPlatform;
+ var dotnetSdk = DotNetSdk.Instance;
+ if (!dotnetSdk.IsValid)
+ throw new DotNetSdk.MissingException();
+ var dotnetPath = "dotnet";
+ switch (buildPlatform)
+ {
+ case TargetPlatform.Windows:
+ dotnetPath = Path.Combine(dotnetSdk.RootPath, "dotnet.exe");
+ break;
+ case TargetPlatform.Linux: break;
+ case TargetPlatform.Mac:
+ dotnetPath = Path.Combine(dotnetSdk.RootPath, "dotnet");
+ break;
+ default: throw new InvalidPlatformException(buildPlatform);
+ }
+ return dotnetPath;
+ }
+
+ ///
+ /// Restores a targets nuget packages.
+ ///
+ /// The task graph.
+ /// The target.
+ /// The dotnet path.
+ public static void RestoreNugetPackages(Graph.TaskGraph graph, Target target)
+ {
+ var dotNetPath = GetDotNetPath();
+ var task = graph.Add();
+ task.WorkingDirectory = target.FolderPath;
+ task.InfoMessage = $"Restoring Nuget Packages for {target.Name}";
+ task.CommandPath = dotNetPath;
+ task.CommandArguments = $"restore";
+ }
+
///
/// Gets the hash code for the string (the same for all platforms). Matches Engine algorithm for string hashing.
///