diff --git a/Assets/01_Scenes/MyProject/GameScene.unity b/Assets/01_Scenes/MyProject/GameScene.unity index 5ccd2df2..1032445c 100644 --- a/Assets/01_Scenes/MyProject/GameScene.unity +++ b/Assets/01_Scenes/MyProject/GameScene.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cc238f3a331986a64f912c5ae7662f7ef12ebb179c8aaae99a8a0fddfddb035 -size 13874110 +oid sha256:bc2e73daec0ff742799e08a87f86a64c22301acfc2a95547e78d6c991ad01db6 +size 13928671 diff --git a/Assets/02_Scripts/Item/ItemData.cs b/Assets/02_Scripts/Item/ItemData.cs index 9dc1238d..99e0746a 100644 --- a/Assets/02_Scripts/Item/ItemData.cs +++ b/Assets/02_Scripts/Item/ItemData.cs @@ -51,5 +51,6 @@ public enum ProductGroup None, ChocoBar, PotatoChip, + GreenBeans, } } diff --git a/Assets/02_Scripts/UI/ItemInfoPanel.cs b/Assets/02_Scripts/UI/ItemInfoPanel.cs index e5658f83..834db23d 100644 --- a/Assets/02_Scripts/UI/ItemInfoPanel.cs +++ b/Assets/02_Scripts/UI/ItemInfoPanel.cs @@ -57,7 +57,7 @@ public void Show(ItemData data) if (hasDiscount) { if (_discountPriceText != null) _discountPriceText.text = FormatPrice(data.FinalPrice); - if (_discountRateText != null) _discountRateText.text = $"-{Mathf.RoundToInt(data.DiscountRate * 100f)}%"; + if (_discountRateText != null) _discountRateText.text = $"{Mathf.RoundToInt(data.DiscountRate * 100f)}"; } _root.SetActive(true); diff --git a/Assets/03_Models/Stylized_Hands/Poses/Pose_GrabSnack2.asset b/Assets/03_Models/Stylized_Hands/Poses/Pose_GrabSnack2.asset new file mode 100644 index 00000000..3432550d --- /dev/null +++ b/Assets/03_Models/Stylized_Hands/Poses/Pose_GrabSnack2.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6784bacdc71c1390522e6afb492e7bb1cc7c990fc1b007059214c38f8512313e +size 4957 diff --git a/Assets/03_Models/Stylized_Hands/Poses/Pose_GrabSnack2.asset.meta b/Assets/03_Models/Stylized_Hands/Poses/Pose_GrabSnack2.asset.meta new file mode 100644 index 00000000..07db1a92 --- /dev/null +++ b/Assets/03_Models/Stylized_Hands/Poses/Pose_GrabSnack2.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3768d0f9519980b4b8c0f62966a56ab0 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/08_Data/Items/GreenBeans.asset b/Assets/08_Data/Items/GreenBeans.asset new file mode 100644 index 00000000..9f473dc8 --- /dev/null +++ b/Assets/08_Data/Items/GreenBeans.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3e8fdcee8105574ab2328760bd532a0e6d7973af4d4e166343450a93282df15 +size 709 diff --git a/Assets/08_Data/Items/GreenBeans.asset.meta b/Assets/08_Data/Items/GreenBeans.asset.meta new file mode 100644 index 00000000..725c20c9 --- /dev/null +++ b/Assets/08_Data/Items/GreenBeans.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 736a87880695f4d47b1c732083f04582 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/EasyColliderEditor.meta b/Assets/EasyColliderEditor.meta new file mode 100644 index 00000000..4cf5df2e --- /dev/null +++ b/Assets/EasyColliderEditor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4152f82672157b240a0eb0b802584254 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/EasyColliderEditor/DOTS.meta b/Assets/EasyColliderEditor/DOTS.meta new file mode 100644 index 00000000..53cf0c1f --- /dev/null +++ b/Assets/EasyColliderEditor/DOTS.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: e9e109fb49381c540b4a5108479c269e +folderAsset: yes +timeCreated: 1626271522 +licenseType: Store +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/EasyColliderEditor/DOTS/EasyDOTSDocumentation.pdf b/Assets/EasyColliderEditor/DOTS/EasyDOTSDocumentation.pdf new file mode 100644 index 00000000..ecae91ae --- /dev/null +++ b/Assets/EasyColliderEditor/DOTS/EasyDOTSDocumentation.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89d055cff278e61036ad1fcb43d66531569a2237be013de31be87daca16297fd +size 138255 diff --git a/Assets/EasyColliderEditor/DOTS/EasyDOTSDocumentation.pdf.meta b/Assets/EasyColliderEditor/DOTS/EasyDOTSDocumentation.pdf.meta new file mode 100644 index 00000000..8aa830d6 --- /dev/null +++ b/Assets/EasyColliderEditor/DOTS/EasyDOTSDocumentation.pdf.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 078b33d3bbdbd3f478bca270510b58aa +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/DOTS/EasyDOTSDocumentation.pdf + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/DOTS/Scripts.meta b/Assets/EasyColliderEditor/DOTS/Scripts.meta new file mode 100644 index 00000000..e338d18d --- /dev/null +++ b/Assets/EasyColliderEditor/DOTS/Scripts.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 28e4c31123bcd614b910d5cfe5c82e2e +folderAsset: yes +timeCreated: 1626290231 +licenseType: Store +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/EasyColliderEditor/DOTS/Scripts/EasyColliderDOTS.cs b/Assets/EasyColliderEditor/DOTS/Scripts/EasyColliderDOTS.cs new file mode 100644 index 00000000..4535e8a3 --- /dev/null +++ b/Assets/EasyColliderEditor/DOTS/Scripts/EasyColliderDOTS.cs @@ -0,0 +1,18 @@ +#if (UNITY_EDITOR) + +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +namespace ECE +{ + // This is just the empty class that get's overwritten to add DOTS support. + // This make it much simpler for future updates to also work with DOTS as + // the only thing it does is convert the colliders through the UI Button. + // by having the same class with required method, we don't need to adjust + // other scripts each update. + public class EasyColliderDOTS + { + public void OnInspectorGUI(EasyColliderEditor Editor) { } + } +} +#endif \ No newline at end of file diff --git a/Assets/EasyColliderEditor/DOTS/Scripts/EasyColliderDOTS.cs.meta b/Assets/EasyColliderEditor/DOTS/Scripts/EasyColliderDOTS.cs.meta new file mode 100644 index 00000000..9b543402 --- /dev/null +++ b/Assets/EasyColliderEditor/DOTS/Scripts/EasyColliderDOTS.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: a8ac46b819edd88459956fa0b4832ff7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/DOTS/Scripts/EasyColliderDOTS.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/EasyColliderEditorDocumentation.pdf b/Assets/EasyColliderEditor/EasyColliderEditorDocumentation.pdf new file mode 100644 index 00000000..e832fdc8 --- /dev/null +++ b/Assets/EasyColliderEditor/EasyColliderEditorDocumentation.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1489fe0b47e4b18aa2df1d69426d7469a88f7efe662fb0f37ecf365a6b90f7c9 +size 5063330 diff --git a/Assets/EasyColliderEditor/EasyColliderEditorDocumentation.pdf.meta b/Assets/EasyColliderEditor/EasyColliderEditorDocumentation.pdf.meta new file mode 100644 index 00000000..7c73a805 --- /dev/null +++ b/Assets/EasyColliderEditor/EasyColliderEditorDocumentation.pdf.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: b04cc4b7e03f58f46a6bd2a7b3fe4498 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/EasyColliderEditorDocumentation.pdf + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Icons.meta b/Assets/EasyColliderEditor/Icons.meta new file mode 100644 index 00000000..60812467 --- /dev/null +++ b/Assets/EasyColliderEditor/Icons.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 723d55ae3f9555c42bcd3edb401a4613 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/EasyColliderEditor/Icons/ECEUIBox32.png b/Assets/EasyColliderEditor/Icons/ECEUIBox32.png new file mode 100644 index 00000000..94e34f25 --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUIBox32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f84a2424e992091f841f148dbf9f7698e17a4aec587d2265b88ef209f4517dc5 +size 19184 diff --git a/Assets/EasyColliderEditor/Icons/ECEUIBox32.png.meta b/Assets/EasyColliderEditor/Icons/ECEUIBox32.png.meta new file mode 100644 index 00000000..152bb8c3 --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUIBox32.png.meta @@ -0,0 +1,106 @@ +fileFormatVersion: 2 +guid: e998347e00b1629449691174d6ce75de +TextureImporter: + fileIDToRecycleName: {} + externalObjects: {} + serializedVersion: 9 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: -1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: -1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + platformSettings: + - serializedVersion: 2 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + - serializedVersion: 2 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 82ac149ea221fb545a0fb56f2203b797 + vertices: [] + indices: + edges: [] + weights: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Icons/ECEUIBox32.png + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Icons/ECEUIBox32Merge.png b/Assets/EasyColliderEditor/Icons/ECEUIBox32Merge.png new file mode 100644 index 00000000..fe73a9d3 --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUIBox32Merge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8539e0fa41b5f5edc6152068e5e48bee6180100821c1f18f558e58076bd9788f +size 21792 diff --git a/Assets/EasyColliderEditor/Icons/ECEUIBox32Merge.png.meta b/Assets/EasyColliderEditor/Icons/ECEUIBox32Merge.png.meta new file mode 100644 index 00000000..f0656faa --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUIBox32Merge.png.meta @@ -0,0 +1,106 @@ +fileFormatVersion: 2 +guid: 480816d7d5b863f42984e0a47484f216 +TextureImporter: + fileIDToRecycleName: {} + externalObjects: {} + serializedVersion: 9 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: -1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: -1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + platformSettings: + - serializedVersion: 2 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + - serializedVersion: 2 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: e7eeee5780c59014ea4b310dbd384852 + vertices: [] + indices: + edges: [] + weights: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Icons/ECEUIBox32Merge.png + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Icons/ECEUICapsule32.png b/Assets/EasyColliderEditor/Icons/ECEUICapsule32.png new file mode 100644 index 00000000..d7f44eb1 --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUICapsule32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58a6ed72e8a8e4135aa282d07ad765a7cbb4cd76943a76829cbbf4d7851d1dce +size 19527 diff --git a/Assets/EasyColliderEditor/Icons/ECEUICapsule32.png.meta b/Assets/EasyColliderEditor/Icons/ECEUICapsule32.png.meta new file mode 100644 index 00000000..4668479f --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUICapsule32.png.meta @@ -0,0 +1,106 @@ +fileFormatVersion: 2 +guid: 1b9402ac744555345b9f6111ff8c4b8a +TextureImporter: + fileIDToRecycleName: {} + externalObjects: {} + serializedVersion: 9 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: -1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: -1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + platformSettings: + - serializedVersion: 2 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + - serializedVersion: 2 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: ec9b94295078e8e4989339a3c3f2c388 + vertices: [] + indices: + edges: [] + weights: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Icons/ECEUICapsule32.png + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Icons/ECEUICapsule32Merge.png b/Assets/EasyColliderEditor/Icons/ECEUICapsule32Merge.png new file mode 100644 index 00000000..9bf6945c --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUICapsule32Merge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1e162b48c7b9c635bc380831306f1fd42ae78e23f4ce97e16ecb5464a4ee104 +size 20242 diff --git a/Assets/EasyColliderEditor/Icons/ECEUICapsule32Merge.png.meta b/Assets/EasyColliderEditor/Icons/ECEUICapsule32Merge.png.meta new file mode 100644 index 00000000..c41ee13e --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUICapsule32Merge.png.meta @@ -0,0 +1,106 @@ +fileFormatVersion: 2 +guid: 0ad3709299b6f5e44a2db34afc52ccd4 +TextureImporter: + fileIDToRecycleName: {} + externalObjects: {} + serializedVersion: 9 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: -1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: -1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + platformSettings: + - serializedVersion: 2 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + - serializedVersion: 2 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: f27b277bd5d034d4097d222005f63399 + vertices: [] + indices: + edges: [] + weights: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Icons/ECEUICapsule32Merge.png + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Icons/ECEUIConvexMesh32.png b/Assets/EasyColliderEditor/Icons/ECEUIConvexMesh32.png new file mode 100644 index 00000000..5942d2df --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUIConvexMesh32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a6643dffa6fa560129473eda461a5929a2bacdfe03ef64bca8c802bcd2117a8 +size 19797 diff --git a/Assets/EasyColliderEditor/Icons/ECEUIConvexMesh32.png.meta b/Assets/EasyColliderEditor/Icons/ECEUIConvexMesh32.png.meta new file mode 100644 index 00000000..952c430f --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUIConvexMesh32.png.meta @@ -0,0 +1,106 @@ +fileFormatVersion: 2 +guid: 53b9b7a941c40804b8179b13ecf40b09 +TextureImporter: + fileIDToRecycleName: {} + externalObjects: {} + serializedVersion: 9 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: -1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: -1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + platformSettings: + - serializedVersion: 2 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + - serializedVersion: 2 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: eaf3439ed7ce6624ca3aba5a5563f408 + vertices: [] + indices: + edges: [] + weights: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Icons/ECEUIConvexMesh32.png + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Icons/ECEUIConvexMesh32Merge.png b/Assets/EasyColliderEditor/Icons/ECEUIConvexMesh32Merge.png new file mode 100644 index 00000000..bdd30d55 --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUIConvexMesh32Merge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:133ea62a996ccaf73b32d279d2df1f7da7453689c33a34ce26111d7b92cc8aca +size 20493 diff --git a/Assets/EasyColliderEditor/Icons/ECEUIConvexMesh32Merge.png.meta b/Assets/EasyColliderEditor/Icons/ECEUIConvexMesh32Merge.png.meta new file mode 100644 index 00000000..ee7e530e --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUIConvexMesh32Merge.png.meta @@ -0,0 +1,106 @@ +fileFormatVersion: 2 +guid: b0c31ecbc9bd28e4e8ac1e5e43ba307e +TextureImporter: + fileIDToRecycleName: {} + externalObjects: {} + serializedVersion: 9 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: -1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: -1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + platformSettings: + - serializedVersion: 2 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + - serializedVersion: 2 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 15c81a624ef7f0a46ac5c566730ebca8 + vertices: [] + indices: + edges: [] + weights: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Icons/ECEUIConvexMesh32Merge.png + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Icons/ECEUICylinder32.png b/Assets/EasyColliderEditor/Icons/ECEUICylinder32.png new file mode 100644 index 00000000..d990c1b8 --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUICylinder32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:786212f9fe7987fae57797492635321c61a962ffb6506806edcc8bbe1fe6bb03 +size 21906 diff --git a/Assets/EasyColliderEditor/Icons/ECEUICylinder32.png.meta b/Assets/EasyColliderEditor/Icons/ECEUICylinder32.png.meta new file mode 100644 index 00000000..a6d316fe --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUICylinder32.png.meta @@ -0,0 +1,106 @@ +fileFormatVersion: 2 +guid: ccbaf976cae4b064f9a918823e8f6552 +TextureImporter: + fileIDToRecycleName: {} + externalObjects: {} + serializedVersion: 9 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: -1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: -1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + platformSettings: + - serializedVersion: 2 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + - serializedVersion: 2 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 4674b33d58a9a5d45b39418124f53ac9 + vertices: [] + indices: + edges: [] + weights: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Icons/ECEUICylinder32.png + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Icons/ECEUICylinder32Merge.png b/Assets/EasyColliderEditor/Icons/ECEUICylinder32Merge.png new file mode 100644 index 00000000..43fd76d4 --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUICylinder32Merge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8020279a429e85ecc71b90c3d7e2aeea1987937bab3060469b3460668a10f18d +size 21879 diff --git a/Assets/EasyColliderEditor/Icons/ECEUICylinder32Merge.png.meta b/Assets/EasyColliderEditor/Icons/ECEUICylinder32Merge.png.meta new file mode 100644 index 00000000..a39fcb67 --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUICylinder32Merge.png.meta @@ -0,0 +1,106 @@ +fileFormatVersion: 2 +guid: 2d782a0c73983ee4b95417abf7be42ef +TextureImporter: + fileIDToRecycleName: {} + externalObjects: {} + serializedVersion: 9 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: -1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: -1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + platformSettings: + - serializedVersion: 2 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + - serializedVersion: 2 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 6d9f9948f75eff44b8243fe5f1bc1a83 + vertices: [] + indices: + edges: [] + weights: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Icons/ECEUICylinder32Merge.png + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Icons/ECEUIRotatedBox32.png b/Assets/EasyColliderEditor/Icons/ECEUIRotatedBox32.png new file mode 100644 index 00000000..70bc9026 --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUIRotatedBox32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f632e058d1d654ca617cb5ecb7fb59c5a9a041f674169df61d417884bcb259a +size 20028 diff --git a/Assets/EasyColliderEditor/Icons/ECEUIRotatedBox32.png.meta b/Assets/EasyColliderEditor/Icons/ECEUIRotatedBox32.png.meta new file mode 100644 index 00000000..c8c0ad25 --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUIRotatedBox32.png.meta @@ -0,0 +1,106 @@ +fileFormatVersion: 2 +guid: 06f5537b1554be7429d39076a339a6e9 +TextureImporter: + fileIDToRecycleName: {} + externalObjects: {} + serializedVersion: 9 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: -1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: -1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + platformSettings: + - serializedVersion: 2 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + - serializedVersion: 2 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 43dba5d881a539c46b514427a4041b89 + vertices: [] + indices: + edges: [] + weights: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Icons/ECEUIRotatedBox32.png + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Icons/ECEUIRotatedBox32Merge.png b/Assets/EasyColliderEditor/Icons/ECEUIRotatedBox32Merge.png new file mode 100644 index 00000000..7b7a7eb5 --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUIRotatedBox32Merge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:caa8932a3a5f62bd8519c88964d100e45f6077cc7d7d2dd487f317ae3de9fc47 +size 22812 diff --git a/Assets/EasyColliderEditor/Icons/ECEUIRotatedBox32Merge.png.meta b/Assets/EasyColliderEditor/Icons/ECEUIRotatedBox32Merge.png.meta new file mode 100644 index 00000000..b76c8f2f --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUIRotatedBox32Merge.png.meta @@ -0,0 +1,106 @@ +fileFormatVersion: 2 +guid: 9759e644c16116c46ad7d928f57b00be +TextureImporter: + fileIDToRecycleName: {} + externalObjects: {} + serializedVersion: 9 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: -1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: -1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + platformSettings: + - serializedVersion: 2 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + - serializedVersion: 2 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 1fbf28e7ef18a624b882903ec37cf195 + vertices: [] + indices: + edges: [] + weights: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Icons/ECEUIRotatedBox32Merge.png + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Icons/ECEUIRotatedCapsule32.png b/Assets/EasyColliderEditor/Icons/ECEUIRotatedCapsule32.png new file mode 100644 index 00000000..c9ba12df --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUIRotatedCapsule32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:204dd2f0c8ddd7de4c6ff94cb5428d6d2b94d901e60e40b140e5cbf677664a74 +size 19846 diff --git a/Assets/EasyColliderEditor/Icons/ECEUIRotatedCapsule32.png.meta b/Assets/EasyColliderEditor/Icons/ECEUIRotatedCapsule32.png.meta new file mode 100644 index 00000000..641b972d --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUIRotatedCapsule32.png.meta @@ -0,0 +1,106 @@ +fileFormatVersion: 2 +guid: b909a7c899938ca4d8c9705a045fe410 +TextureImporter: + fileIDToRecycleName: {} + externalObjects: {} + serializedVersion: 9 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: -1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: -1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + platformSettings: + - serializedVersion: 2 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + - serializedVersion: 2 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: b586c269269556a418cca90db1e955c1 + vertices: [] + indices: + edges: [] + weights: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Icons/ECEUIRotatedCapsule32.png + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Icons/ECEUIRotatedCapsule32Merge.png b/Assets/EasyColliderEditor/Icons/ECEUIRotatedCapsule32Merge.png new file mode 100644 index 00000000..7ae72fd9 --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUIRotatedCapsule32Merge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be44f82fc5000d44c7ded1a85b683e425d36096bf65a6bb8e58b40eebd0343e6 +size 22709 diff --git a/Assets/EasyColliderEditor/Icons/ECEUIRotatedCapsule32Merge.png.meta b/Assets/EasyColliderEditor/Icons/ECEUIRotatedCapsule32Merge.png.meta new file mode 100644 index 00000000..683331ff --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUIRotatedCapsule32Merge.png.meta @@ -0,0 +1,106 @@ +fileFormatVersion: 2 +guid: 764a526c991bd194cbfc2f4319b1d8dc +TextureImporter: + fileIDToRecycleName: {} + externalObjects: {} + serializedVersion: 9 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: -1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: -1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + platformSettings: + - serializedVersion: 2 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + - serializedVersion: 2 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: fa24b5ba62ace4d47990cefe71a46da7 + vertices: [] + indices: + edges: [] + weights: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Icons/ECEUIRotatedCapsule32Merge.png + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Icons/ECEUISphere32.png b/Assets/EasyColliderEditor/Icons/ECEUISphere32.png new file mode 100644 index 00000000..29f20f35 --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUISphere32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:324c2f03ab47ac8a532112d1fea317f7c194ba46459d0d2803980d138af14533 +size 19835 diff --git a/Assets/EasyColliderEditor/Icons/ECEUISphere32.png.meta b/Assets/EasyColliderEditor/Icons/ECEUISphere32.png.meta new file mode 100644 index 00000000..af3ea83f --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUISphere32.png.meta @@ -0,0 +1,106 @@ +fileFormatVersion: 2 +guid: e01e5ee2b0ff2214196190f59f373bac +TextureImporter: + fileIDToRecycleName: {} + externalObjects: {} + serializedVersion: 9 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: -1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: -1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + platformSettings: + - serializedVersion: 2 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + - serializedVersion: 2 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 50086ebfa2f74a447b9f73bcb4ebf480 + vertices: [] + indices: + edges: [] + weights: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Icons/ECEUISphere32.png + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Icons/ECEUISphereMerge32.png b/Assets/EasyColliderEditor/Icons/ECEUISphereMerge32.png new file mode 100644 index 00000000..a0a5480d --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUISphereMerge32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88412a0728ab18ac35e99662c22c74915988850697786a092bf5eec4181f9088 +size 20552 diff --git a/Assets/EasyColliderEditor/Icons/ECEUISphereMerge32.png.meta b/Assets/EasyColliderEditor/Icons/ECEUISphereMerge32.png.meta new file mode 100644 index 00000000..8754b445 --- /dev/null +++ b/Assets/EasyColliderEditor/Icons/ECEUISphereMerge32.png.meta @@ -0,0 +1,106 @@ +fileFormatVersion: 2 +guid: 950da02c7c566da49bd0785d2f903f0f +TextureImporter: + fileIDToRecycleName: {} + externalObjects: {} + serializedVersion: 9 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: -1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: -1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + platformSettings: + - serializedVersion: 2 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + - serializedVersion: 2 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 76f0a0b7288f5c940b9cd55cb702ba24 + vertices: [] + indices: + edges: [] + weights: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Icons/ECEUISphereMerge32.png + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts.meta b/Assets/EasyColliderEditor/Scripts.meta new file mode 100644 index 00000000..31f2cd12 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e652979cf9e1eac4e90f77a15a69ff0c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderAutoSkinned.cs b/Assets/EasyColliderEditor/Scripts/EasyColliderAutoSkinned.cs new file mode 100644 index 00000000..9c109dce --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderAutoSkinned.cs @@ -0,0 +1,1832 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using System.Linq; +using System.Collections; +#if UNITY_2019_1_OR_NEWER +using Unity.Collections; +#endif +using System; + +namespace ECE +{ + + // TODO: + + // Convex mesh colliders can fail silently on primarily badly scaled skinned meshes. (Once in local space, the distances are too small for quickhull.) + // have tried slowly reducing calculated epsilon, but that does not work either. + // best option is to add a faq to documentation, and explain that scaling from some models (particularily blender it seems) + // exports with 100,100,100 scaling on the root + + // NOTE: limit by child bone distance exprimented with, does not function well compared to depenetration methods. + // would get vertex distance and closest axis and check distance and toss vertices over the distance to prevent overlap. This does not work well. + + [System.Serializable] + public class EasyColliderAutoSkinned : ScriptableObject, ISerializationCallbackReceiver + { + // All these proprties added to work without preferences file + // values can be set from your own script. + // default values should be the same as in preferences. + + SKINNED_MESH_DEPENETRATE_ORDER _autoSkinnedDepenetrateOrder; + public SKINNED_MESH_DEPENETRATE_ORDER AutoSkinnedDepenetrateOrder + { + get + { +#if (UNITY_EDITOR) + return Application.isPlaying ? _autoSkinnedDepenetrateOrder : Preferences.AutoSkinnedDepenetrateOrder; +#else + return _autoSkinnedDepenetrateOrder; +#endif + } + set + { + _autoSkinnedDepenetrateOrder = value; + } + } + + bool _autoSkinnedForce256Triangles = true; + public bool AutoSkinnedForce256Triangles + { + get + { +#if (UNITY_EDITOR) + return Application.isPlaying ? _autoSkinnedForce256Triangles : Preferences.AutoSkinnedForce256Triangles; +#else + return _autoSkinnedForce256Triangles; +#endif + } + set { _autoSkinnedForce256Triangles = value; } + } + + float _autoSkinnedMinBoneWeight = 0.5f; + public float AutoSkinnedMinBoneWeight + { + get + { +#if (UNITY_EDITOR) + return Application.isPlaying ? _autoSkinnedMinBoneWeight : Preferences.AutoSkinnedMinBoneWeight; +#else + return _autoSkinnedMinBoneWeight; +#endif + } + set { _autoSkinnedMinBoneWeight = value; } + } + + float _autoSkinnedMinRealignAngle; + public float AutoSkinnedMinRealignAngle + { + get + { +#if (UNITY_EDITOR) + return Application.isPlaying ? _autoSkinnedMinRealignAngle : Preferences.AutoSkinnedMinRealignAngle; +#else + return _autoSkinnedMinRealignAngle; +#endif + } + set { _autoSkinnedMinRealignAngle = value; } + } + + bool _autoSkinnedAllowRealign; + public bool AutoSkinnedAllowRealign + { + get + { +#if (UNITY_EDITOR) + return Application.isPlaying ? _autoSkinnedAllowRealign : Preferences.AutoSkinnedAllowRealign; +#else + return _autoSkinnedAllowRealign; +#endif + } + set { _autoSkinnedAllowRealign = value; } + } + + + + SKINNED_MESH_COLLIDER_TYPE _autoSkinnedColliderType = SKINNED_MESH_COLLIDER_TYPE.Box; + public SKINNED_MESH_COLLIDER_TYPE AutoSkinnedColliderType + { + get + { +#if (UNITY_EDITOR) + return Application.isPlaying ? _autoSkinnedColliderType : Preferences.AutoSkinnedColliderType; +#else + return _autoSkinnedColliderType; +#endif + } + set { _autoSkinnedColliderType = value; } + } + + + bool _autoSkinnedDepenetrate; + public bool AutoSkinnedDepenetrate + { + get + { +#if (UNITY_EDITOR) + return Application.isPlaying ? _autoSkinnedDepenetrate : Preferences.AutoSkinnedDepenetrate; +#else + return _autoSkinnedDepenetrate; +#endif + } + set { _autoSkinnedDepenetrate = value; } + } + + bool _autoSkinnedIndents = true; + public bool AutoSkinnedIndents + { + get + { +#if (UNITY_EDITOR) + return Application.isPlaying ? _autoSkinnedIndents : Preferences.AutoSkinnedIndents; +#else + return _autoSkinnedIndents; +#endif + } + set { _autoSkinnedIndents = value; } + } + + int _autoSkinnedIterativeDepenetrationCount = 15; + public int AutoSkinnedIterativeDepenetrationCount + { + get + { +#if (UNITY_EDITOR) + return Application.isPlaying ? _autoSkinnedIterativeDepenetrationCount : Preferences.AutoSkinnedIterativeDepenetrationCount; +#else + return _autoSkinnedIterativeDepenetrationCount; +#endif + } + set { _autoSkinnedIterativeDepenetrationCount = value; } + + } + + bool _autoSkinnedPairing = true; + public bool AutoSkinnedPairing + { + get + { +#if (UNITY_EDITOR) + return Application.isPlaying ? _autoSkinnedPairing : Preferences.AutoSkinnedPairing; +#else + return _autoSkinnedPairing; +#endif + } + set { _autoSkinnedPairing = value; } + } + + bool _autoSkinnedPerBoneSettings; + public bool AutoSkinnedPerBoneSettings + { + get + { +#if (UNITY_EDITOR) + return Application.isPlaying ? _autoSkinnedPerBoneSettings : Preferences.AutoSkinnedPerBoneSettings; +#else + return _autoSkinnedPerBoneSettings; +#endif + } + set + { + _autoSkinnedPerBoneSettings = value; + } + } + + + + float _autoSkinnedShrinkAmount = 0.5f; + public float AutoSkinnedShrinkAmount + { + get + { +#if (UNITY_EDITOR) + return Application.isPlaying ? _autoSkinnedShrinkAmount : Preferences.AutoSkinnedShrinkAmount; +#else + return _autoSkinnedShrinkAmount; +#endif + } + set + { + _autoSkinnedShrinkAmount = value; + } + } + + bool _autoSkinnedUseDistanceDeltaPairing = true; + public bool AutoSkinnedUseDistanceDeltaPairing + { + get + { +#if (UNITY_EDITOR) + return Application.isPlaying ? _autoSkinnedUseDistanceDeltaPairing : Preferences.AutoSkinnedUseDistanceDeltaPairing; +#else + return _autoSkinnedUseDistanceDeltaPairing; +#endif + } + set + { + _autoSkinnedUseDistanceDeltaPairing = value; + } + } + + + float _autoSkinnedPairedDistanceDelta = 0.01f; + public float AutoSkinnedPairedDistanceDelta + { + get + { +#if (UNITY_EDITOR) + return Application.isPlaying ? _autoSkinnedPairedDistanceDelta : Preferences.AutoSkinnedPairedDistanceDelta; +#else + return _autoSkinnedPairedDistanceDelta; +#endif + } + set + { + _autoSkinnedPairedDistanceDelta = value; + } + } + + + +#if (UNITY_EDITOR) + private EasyColliderPreferences _Preferences; + private EasyColliderPreferences Preferences + { + get + { + if (_Preferences == null) + { + _Preferences = EasyColliderPreferences.Preferences; + } + return _Preferences; + } + } +#endif + + /// + /// Cleans up the data. + /// + public void Clean() + { + BoneList = new List(); + SortedBoneList = new List(); + renderer = null; + transformHashCode = -1; + _selectedBone = null; + _initialScannedObject = null; + } + + EasyColliderAutoSkinnedBone _selectedBone; + public void SetSelectedBone(EasyColliderAutoSkinnedBone bone) + { + _selectedBone = bone; + } + public EasyColliderAutoSkinnedBone GetSelectedBone() + { + return _selectedBone; + } + + GameObject _initialScannedObject; + public GameObject GetInitialScannedObject() + { + return _initialScannedObject; + } + + [SerializeField] + /// + /// List of bones on the current skinned mesh. In the same order as the skinnedmesh's skinnedMesh.bones transform array. + /// + /// + /// + public List BoneList = new List(); + + [SerializeField] + /// + /// hierarchical sorted bones of the current skinned mesh. + /// + /// + /// + public List SortedBoneList = new List(); + + /// + /// Hashcode of the position, rotation, and scale of the transform to more easily detect movement and update preview + /// + public int transformHashCode; + + /// + /// current skinned mesh renderer + /// + public SkinnedMeshRenderer renderer; + + /// + /// Checks if the preview needs updating because the root of the skinned mesh has moved/rotated/scaled. + /// + /// + public bool HasSkinnedMeshRendererTransformed() + { + if (renderer == null) return false; + int newHash = renderer.transform.position.GetHashCode() + renderer.transform.rotation.GetHashCode() + renderer.transform.lossyScale.GetHashCode(); + if (transformHashCode != newHash) + { + transformHashCode = newHash; + return true; + } + return false; + } + + + /// + /// Goes through all the bones in the bone list and makes sure at least one bone weight on the skinned mesh is above it's threshold. If so, marks that bone as valid. + /// + /// + /// + private void SetBoneValidity(SkinnedMeshRenderer smr, List boneList) + { + BoneWeight[] boneWeights = smr.sharedMesh.boneWeights; + foreach (var b in boneWeights) + { + if (b.boneIndex0 >= 0 && b.weight0 > 0) boneList[b.boneIndex0].IsValid = true; + if (b.boneIndex1 >= 0 && b.weight1 > 0) boneList[b.boneIndex1].IsValid = true; + if (b.boneIndex2 >= 0 && b.weight2 > 0) boneList[b.boneIndex2].IsValid = true; + if (b.boneIndex3 >= 0 && b.weight3 > 0) boneList[b.boneIndex3].IsValid = true; + } + } + + /// + /// Does an initial scan of bones to create the bone list, sorted bone list, pairs them, and sets initial weights + /// + /// + /// + public void InitialScanBones(GameObject selectedObject, float weight = 0.5f) + { + // Debug.Log("InitialScan." + Preferences.SkinnedMeshColliderType); + SkinnedMeshRenderer smr = selectedObject.GetComponentInChildren(); + _initialScannedObject = selectedObject; + if (smr == null) return; + EasyColliderAutoSkinnedBone[] boneArray = GetSkinnedMeshBones(smr); + if (boneArray == null) return; + BoneList = boneArray.ToList(); + if (BoneList.Count == 0) return; + SetBoneValidity(smr, BoneList); + // set initial bone weights. + foreach (var smbs in BoneList) + { + smbs.BoneWeight = weight; + } + List boneTransforms = smr.bones.ToList(); + // pair bones up + PairBones(BoneList, boneTransforms); + + // smr has a root property, so we're good. + if (smr.rootBone != null) + { + Transform root = smr.rootBone; + // calculate indent levels + if (root.parent != null) + { + SetIndentRecursive(root.parent, boneTransforms, 0); + } + else + { + SetIndentRecursive(root, boneTransforms, 0); + } + // calculate the sorted bone list. (use root's parent if possible, as there can be multiple "root" bones, so without it some wouldn't get detected.) + if (root.parent != null) + { + SortedBoneList = SortBonesRecursive(root.parent, boneTransforms, BoneList, new List()); + } + else + { + SortedBoneList = SortBonesRecursive(root, boneTransforms, BoneList, new List()); + } + } + else + { + // rootBone property of the skinned mesh is null, which would normally cause issues + // so we need to identify the top-level bones, then indent them all, and sort them. + List rootBones = IdentifyRootBones(BoneList); + foreach (var b in rootBones) + { + SetIndentRecursive(b.Transform, boneTransforms, 0); + } + SortedBoneList = NoRootBoneSort(rootBones, boneTransforms); + } + } + + /// + /// Identifies the "root bones" (the top-level bones) in a bone list. Used in cases where the skinned mesh renderers rootBone property is null. + /// + /// + /// + List IdentifyRootBones(List boneList) + { + HashSet examinedBones = new HashSet(); + // without a root bone, find the "toplevel" bones. + List rootBones = new List(); + foreach (var b in boneList) + { + if (b.Transform == null) continue; + if (examinedBones.Contains(b)) continue; + examinedBones.Add(b); + EasyColliderAutoSkinnedBone bone = b; + bool hasParent = true; + while (hasParent) + { + hasParent = false; + Transform parent = bone.Transform.parent; + if (parent != null) + { + foreach (var a in boneList) + { + if (a.Transform == parent) + { + if (rootBones.Contains(a) || examinedBones.Contains(a)) + { + // already added it's parent as a root, so stop looking + bone = null; + hasParent = false; + break; + } + else + { + // is a new parent, keep goin. + bone = a; + hasParent = true; + break; + } + } + } + } + } + examinedBones.Add(bone); + if (bone != null) + { + rootBones.Add(bone); + } + } + return rootBones; + } + + /// + /// Sorts the bones based on a list of identified rootbones in cases where the skinned mesh renderer's root bone has been remvoed. + /// + /// identified top level bones + /// all bone transforms + /// list of bones sorted similar to the unity hierarchy. + List NoRootBoneSort(List rootBones, List boneTransforms) + { + var sortedBones = new List(); + foreach (var b in rootBones) + { + // SetIndentRecursive(b.Transform, boneTransforms, 0); + List sortedBonesForBone = new List(); + SortBonesRecursive(b.Transform, boneTransforms, BoneList, sortedBonesForBone); + sortedBones.AddRange(sortedBonesForBone); + } + return sortedBones; + } + + /// + /// Goes through the boneList and pairs bones togehter by looking at the number of children to determine if theres multiple offshoots (arms/legs) + /// then pairing offshoots based on bone-transform chain length. + /// + /// + /// + private void PairBones(List boneList, List boneTransforms) + { + foreach (var smb in boneList) + { + // skip already paired bones, NOT invalid ones because invalid ones can still have children that are valid and need to be paired. + if (smb.IsPaired) continue; + if (smb.Transform == null) continue; + int childCount = smb.Transform.childCount; + // count the number of valid (is a bone) direct children that this bone has. + List validChildren = new List(); + for (int i = 0; i < childCount; i++) + { + if (boneTransforms.Contains(smb.Transform.GetChild(i))) + { + validChildren.Add(i); + } + } + // more than one child on a bone, means it has multiple offshoots (like arms/legs off the back / hips) + if (validChildren.Count > 1) + { + // calculate the number of valid bones each offshoot has. + // we are identifying which bone offshoots match with other offshoots by the total valid bone length. + List perChainTransformCount = new List(new int[validChildren.Count]); + List perChainDistances = new List(new float[validChildren.Count]); + for (int i = 0; i < validChildren.Count; i++) + { + Transform child = smb.Transform.GetChild(validChildren[i]); + // skip already paired bones. + // get all children and count which ones are actually bones. + List childTransforms = child.GetComponentsInChildren().ToList(); + int childsValidBoneCount = 0; + foreach (var t in childTransforms) + { + if (boneTransforms.Contains(t)) + { + childsValidBoneCount++; + } + perChainDistances[i] = perChainDistances[i] + Vector3.Distance(child.position, t.position); + } + perChainTransformCount[i] = childsValidBoneCount; + } + // go through each child and see if the offshoots can be paired, and pair them. + for (int i = 0; i < validChildren.Count; i++) + { + // get the index of the current bone we are looking at. + int index = boneTransforms.IndexOf(smb.Transform.GetChild(i)); + // if the bone is already paired, skip it. + if (index < 0 || boneList[index].IsPaired) continue; + // identify bones that have similar count in their bone chain using the perChainTransformCount. + List pairedBones = new List(); + for (int j = 0; j < validChildren.Count; j++) + { + if (j == i) continue; + // same number of transforms? likely to be a pair. + bool isPaired = perChainTransformCount[i] == perChainTransformCount[j]; + + if (AutoSkinnedUseDistanceDeltaPairing) + { + // works, and simplifying to only using the actual transform works less well than using full chain distance. + if (isPaired && (perChainDistances[i] == perChainDistances[j] || Mathf.Abs(perChainDistances[i] - perChainDistances[j]) < AutoSkinnedPairedDistanceDelta)) + { + isPaired = true; + } + else if (isPaired) + { + isPaired = false; + } + } + + if (isPaired) + { + // bone pair found! + Transform c1 = smb.Transform.GetChild(j); + int i1 = boneTransforms.IndexOf(c1); + // make sure index > 0 + if (i1 >= 0) + { + pairedBones.Add(i1); + } + } + } + + + + // if we have a paired bone, pair it up (and their children!) + if (pairedBones.Count > 0) + { + // mark the current bone as paired. + boneList[index].PairedBones = pairedBones; + boneList[index].IsPaired = true; + // mark each bone as paired. + foreach (var pIndex in pairedBones) + { + boneList[pIndex].IsPaired = true; + } + // we need to pair the children as well, create a list of transform's including itself and it's children. + List bChildren = boneList[index].Transform.GetComponentsInChildren().ToList(); + // list to contain each paired-bone's list of transforms. + List> pairedChildren = new List>(); + // create the paired children list, doing the same thing as for the original. + foreach (var pIndex in pairedBones) + { + pairedChildren.Add(boneTransforms[pIndex].GetComponentsInChildren().ToList()); + } + for (int j = 0; j < bChildren.Count; j++) + { + // get the bone transform index of the child we're looking at. + int bIndex = boneTransforms.IndexOf(bChildren[j]); + // skip non-bone transforms. + if (bIndex < 0) continue; + // pair the bones. + List cPairedBones = new List(); + foreach (var tList in pairedChildren) + { + // pair only the matching indexs in each transform list. + int pIndex = boneTransforms.IndexOf(tList[j]); + if (pIndex < 0) continue; + // add them as a paired bonelist index, and mark the current one as paired. + cPairedBones.Add(pIndex); + boneList[pIndex].IsPaired = true; + } + // if we paired this child transform with another transform. + if (cPairedBones.Count > 0) + { + // mark it as paired, set it's paired list, and set it as the display bone. + boneList[bIndex].IsPaired = true; + boneList[bIndex].PairedBones = cPairedBones; + boneList[bIndex].IsPairDisplayBone = true; + } + } + } + } + } + } + } + + /// + /// Recursively sorts bones in the boneList into the sortedList to match with how they would be displayed in the scene hierarchy. + /// + /// initially the skinned mesh's root bone. + /// the skinned mesh renderers bones + /// + /// + /// Sorted list of bones. + List SortBonesRecursive(Transform current, List boneTransforms, List boneList, List sortedList) + { + int index = boneTransforms.IndexOf(current); + if (index >= 0) + { + EasyColliderAutoSkinnedBone smb = BoneList[index]; + smb.BoneName = current.name; + sortedList.Add(smb); + for (int i = 0; i < current.childCount; i++) + { + Transform child = current.GetChild(i); + SortBonesRecursive(child, boneTransforms, boneList, sortedList); + } + } + else + { + for (int i = 0; i < current.childCount; i++) + { + Transform child = current.GetChild(i); + SortBonesRecursive(child, boneTransforms, boneList, sortedList); + } + } + return sortedList; + } + + + /// + /// Recursively calculates the index level of each bone. + /// + /// initially the skinned mesh's root bone. + /// + /// + void SetIndentRecursive(Transform current, List boneTransforms, int currentIndent) + { + int index = boneTransforms.IndexOf(current); + if (index >= 0) + { + // only set the indent and increase the indent if the bone is valid (has at least 1 skinned vertex); + if (BoneList[index].IsValid) + { + BoneList[index].IndentLevel = currentIndent; + currentIndent += 1; + } + for (int i = 0; i < current.childCount; i++) + { + SetIndentRecursive(current.GetChild(i), boneTransforms, currentIndent); + } + } + else + { + for (int i = 0; i < current.childCount; i++) + { + SetIndentRecursive(current.GetChild(i), boneTransforms, currentIndent); + } + } + } + + + + + /// + /// (Undoable) Creates a gameobject as a child of parent, with it's forward axis pointed towards the child, and its up axis at the cross of the new forward and the parent's up direction. + /// + /// parent transform + /// child transform + /// Gameobject at parent's location with it's forward axis pointing towards child. + public GameObject CreateRealignedObject(Transform parent, Transform child, bool forPreview = false) + { + // realign with a rotated gameboject. + GameObject obj = new GameObject(parent.transform.name + "_RotatedCollider"); + Vector3 childDir = child.position - parent.position; + // use either the bone's right or the the bone's forward, whichever isn't the same as the child's direction in the calculation. + Vector3 right = parent.transform.right; + if (childDir == right) { right = parent.transform.forward; } + Vector3 up = Vector3.Cross(childDir, right); + obj.transform.rotation = Quaternion.LookRotation(childDir, up); + obj.transform.SetParent(parent.transform); + obj.layer = obj.transform.parent.gameObject.layer; + obj.transform.localPosition = Vector3.zero; + if (!forPreview) + { +#if (UNITY_EDITOR) + Undo.RegisterCreatedObjectUndo(obj, "Create realign bone object"); +#endif + } + return obj; + } + + /// + /// gets the TRS that represents the transform from the parent to the child as the forward direction. + /// + /// + /// + /// + public Matrix4x4 GetMatrixForObject(Transform parent, Transform child) + { + Vector3 childDir = child.position - parent.position; + Vector3 right = parent.transform.right; + if (childDir == right) { right = parent.transform.forward; } + Vector3 up = Vector3.Cross(childDir, right); + Quaternion q = Quaternion.LookRotation(childDir, up); + return Matrix4x4.TRS(parent.position, q, Vector3.one); + } + + + private List ToLocalVerts(Transform transform, List worldVertices) + { + List localVerts = new List(worldVertices.Count); + foreach (Vector3 v in worldVertices) + { + localVerts.Add(transform.InverseTransformPoint(v)); + } + return localVerts; + } + + /// + /// Creates and attaches a collider of given type + /// + /// type of collider + /// properties of collider + /// data to create collider with + /// save path for mesh colliders + private Collider GenerateCollider(SKINNED_MESH_COLLIDER_TYPE colliderType, EasyColliderProperties properties, EasyColliderAutoSkinnedBone s, string savePath, bool forPreview = false) + { + EasyColliderCreator ecc = new EasyColliderCreator(); +#if(UNITY_EDITOR) + if (forPreview) + { + ecc.UndoEnabled = false; + } +#endif + switch (colliderType) + { + case SKINNED_MESH_COLLIDER_TYPE.Box: + return ecc.CreateBoxCollider(s.WorldSpaceVertices, properties); + case SKINNED_MESH_COLLIDER_TYPE.Capsule: + return ecc.CreateCapsuleCollider_MinMax(s.WorldSpaceVertices, properties, CAPSULE_COLLIDER_METHOD.MinMax); + case SKINNED_MESH_COLLIDER_TYPE.Sphere: + return ecc.CreateSphereCollider_MinMax(s.WorldSpaceVertices, properties); + case SKINNED_MESH_COLLIDER_TYPE.Convex_Mesh: + s.WorldSpaceVertices = ToLocalVerts(s.Transform, s.WorldSpaceVertices); + EasyColliderQuickHull qh = EasyColliderQuickHull.CalculateHull(s.WorldSpaceVertices); + Mesh m = qh.Result; + if (AutoSkinnedForce256Triangles && m != null) + { + if (m.triangles.Length / 3 > 255) + { + // weld vertices to an estimated target, then recalculate the hull, and repeat until + // we get a value, or we reach n, which is a failsafe. + int n = 0; + float weldDistance = 0.0f; + while (m.triangles.Length / 3 > 255 && n < 25) + { + weldDistance = ReduceMeshVerticesOnBone(s, m, weldDistance); + qh = EasyColliderQuickHull.CalculateHull(s.WorldSpaceVertices); + m = qh.Result; + n++; + } + } + } + if (m != null) + { +#if (UNITY_EDITOR) + if (Preferences.SaveConvexHullAsAsset && !forPreview) + { + EasyColliderSaving.CreateAndSaveMeshAsset(qh.Result, s.Transform.gameObject); + } +#endif + // ecc.CreateMesh_QuickHull(s.WorldSpaceVertices, s.Transform.gameObject); + return ecc.CreateConvexMeshCollider(qh.Result, s.Transform.gameObject, properties); + } + break; + } + return null; + } + + public void ChangeBoneEnabled(EasyColliderAutoSkinnedBone bone, bool enabled, bool includeChildren) + { + bone.Enabled = enabled; + if (includeChildren) + { + foreach (var b in BoneList) + { + if (b.Transform.IsChildOf(bone.Transform)) + { + b.Enabled = enabled; + } + } + } + if (AutoSkinnedPairing) + { + foreach (var index in bone.PairedBones) + { + ChangeBoneEnabled(BoneList[index], enabled, includeChildren); + } + } + } + + public void ChangeBoneWeight(EasyColliderAutoSkinnedBone bone, float weight, bool includeChildren) + { + bone.BoneWeight = weight; + if (includeChildren) + { + foreach (var b in BoneList) + { + if (b.Transform.IsChildOf(bone.Transform)) + { + b.BoneWeight = weight; + } + } + } + if (AutoSkinnedPairing) + { + foreach (var index in bone.PairedBones) + { + ChangeBoneWeight(BoneList[index], weight, includeChildren); + } + } + } + + public void ChangeBoneColliderType(EasyColliderAutoSkinnedBone bone, SKINNED_MESH_COLLIDER_TYPE colliderType, bool includeChildren) + { + bone.ColliderType = colliderType; + if (includeChildren) + { + foreach (var b in BoneList) + { + if (b.Transform.IsChildOf(bone.Transform)) + { + b.ColliderType = colliderType; + } + } + } + if (AutoSkinnedPairing) + { + foreach (var index in bone.PairedBones) + { + ChangeBoneColliderType(BoneList[index], colliderType, includeChildren); + } + } + } + + + + /// + /// Estimates a target number of vertices to reach under 256 triangles and merges vertices together until that target is reached. + /// returns the final weld distance used to reach below the estimated target. + /// + /// bone + /// quickhull calculated mesh + /// previous weld distance from this method, or 0 + /// weld distance used to reach target vertices. + private float ReduceMeshVerticesOnBone(EasyColliderAutoSkinnedBone bone, Mesh m, float prevWeldDist) + { + // Tends to merge vertices at the start over those at the end, as we detect when we've reached enough and then add the rest. + List vertices = new List(); + HashSet weldedVerts = new HashSet(); + int targetVerts = (int)(256.0f * ((float)bone.WorldSpaceVertices.Count / ((float)m.triangles.Length / 3))); + if (targetVerts >= bone.WorldSpaceVertices.Count || (targetVerts > bone.WorldSpaceVertices.Count * 0.9f)) + { + targetVerts = (int)(bone.WorldSpaceVertices.Count * 0.9f); + } + int currentVerts = bone.WorldSpaceVertices.Count; + float sizeMagnitude = m.bounds.size.magnitude; + float weldDistance = (sizeMagnitude) / 50f; + if (weldDistance < prevWeldDist) + { + weldDistance = prevWeldDist + (weldDistance * 0.25f); + } + // weld close vertices. (n as a failsafe.) + int n = 0; + while (targetVerts < bone.WorldSpaceVertices.Count && n < 25) + { + int iterCount = bone.WorldSpaceVertices.Count; + for (int i = 0; i < bone.WorldSpaceVertices.Count; i++) + { + // if we break when we've reached the target, + // since the method is also called iterative from an outer loop, we tend to only weld the initial vertices. + // so instead we'll try to weld the whole thing + int weldCount = 1; + Vector3 v1 = bone.WorldSpaceVertices[i]; + if (weldedVerts.Contains(v1)) continue; + for (int j = i; j < bone.WorldSpaceVertices.Count; j++) + { + Vector3 v2 = bone.WorldSpaceVertices[j]; + if (weldedVerts.Contains(v2)) continue; + if (Vector3.Distance(v1, v2) < weldDistance) + { + v1 = ((v1 * weldCount) + v2) / (weldCount + 1); + weldCount++; + weldedVerts.Add(v2); + weldedVerts.Add(v1); + } + } + vertices.Add(v1); + } + weldDistance *= 1.25f; + weldedVerts.Clear(); + bone.WorldSpaceVertices = vertices; + vertices = new List(); + n++; + } + return weldDistance; + } + + private List ToLocalVerts(List worldVertices, Matrix4x4 m) + { + List localVerts = new List(worldVertices.Count); + Matrix4x4 inverse = m.inverse; + foreach (Vector3 v in worldVertices) + { + localVerts.Add(inverse.MultiplyPoint3x4(v)); + } + return localVerts; + } + + private EasyColliderData CalculateColliderData(SKINNED_MESH_COLLIDER_TYPE colliderType, EasyColliderAutoSkinnedBone s, EasyColliderProperties properties) + { + EasyColliderCreator ecc = new EasyColliderCreator(); + List localVertices = ToLocalVerts(s.WorldSpaceVertices, s.Matrix); + switch (colliderType) + { + case SKINNED_MESH_COLLIDER_TYPE.Box: + BoxColliderData bd = ecc.CalculateBoxLocal(localVertices); + bd.Matrix = s.Matrix; + return bd; + case SKINNED_MESH_COLLIDER_TYPE.Capsule: + CapsuleColliderData cd = ecc.CalculateCapsuleMinMaxLocal(localVertices, CAPSULE_COLLIDER_METHOD.MinMax); + cd.Matrix = s.Matrix; + return cd; + case SKINNED_MESH_COLLIDER_TYPE.Sphere: + SphereColliderData scd = ecc.CalculateSphereMinMaxLocal(localVertices); + scd.Matrix = s.Matrix; + return scd; + case SKINNED_MESH_COLLIDER_TYPE.Convex_Mesh: + MeshColliderData mcd = ecc.CalculateMeshColliderQuickHullLocal(localVertices); + mcd.Matrix = s.Matrix; + return mcd; + } + return new EasyColliderData(); + } + + + public void SetColliderTypeOnAllBones(SKINNED_MESH_COLLIDER_TYPE colliderType) + { + foreach (EasyColliderAutoSkinnedBone b in BoneList) + { + b.ColliderType = colliderType; + } + } + + /// + /// Sets the collider type and weight on all bones. + /// + /// + /// + public void SetColliderTypeAndWeightOnAllBones(SKINNED_MESH_COLLIDER_TYPE colliderType, float weight) + { + foreach (EasyColliderAutoSkinnedBone b in BoneList) + { + b.BoneWeight = weight; + b.ColliderType = colliderType; + } + } + + void BakeSkinnedMesh(SkinnedMeshRenderer skinnedMesh, Mesh m) + { + Vector3 lossyScale = skinnedMesh.transform.lossyScale; + Vector3 local = skinnedMesh.transform.localScale; + // this works well enough for most cases. + if (lossyScale != Vector3.one) + { + local.x /= lossyScale.x; + local.y /= lossyScale.y; + local.z /= lossyScale.z; + } + skinnedMesh.transform.localScale = local; + skinnedMesh.BakeMesh(m); + } + + /// + /// Calculates the preview data for the auto-skinned secttion. + /// + /// + /// + /// + /// + /// + /// + /// + /// + public List CalculateSkinnedMeshPreview(SkinnedMeshRenderer skinnedMesh, SKINNED_MESH_COLLIDER_TYPE colliderType, EasyColliderProperties properties, float minBoneWeight, bool realignBones = false, float minRealignAngle = 20f, string savePath = "Assets/") + { + if (skinnedMesh == null) return new List(); + List TemporaryGameObjects = new List(); + renderer = skinnedMesh; + List data = new List(); + + // bake and scale. + Mesh m = new Mesh(); + Vector3 prevScale = skinnedMesh.transform.localScale; + BakeSkinnedMesh(skinnedMesh, m); + Vector3[] vertices = m.vertices; + for (int i = 0; i < vertices.Length; i++) + { + //transform to world relative by root object + vertices[i] = skinnedMesh.transform.TransformPoint(vertices[i]); + } + skinnedMesh.transform.localScale = prevScale; + + // get skinned mesh bones + EasyColliderAutoSkinnedBone[] smbs = BoneList.ToArray(); + if (smbs == null || BoneList.Count == 0) + { + return null; + } + foreach (EasyColliderAutoSkinnedBone smb in smbs) + { + smb.WorldSpaceVertices.Clear(); + } + + List generatedColliders = new List(); + + // set the world vertex for each bone. +#if UNITY_2019_1_OR_NEWER + SetWorldVertices(smbs, skinnedMesh.sharedMesh.GetAllBoneWeights(), skinnedMesh.sharedMesh.GetBonesPerVertex(), vertices, minBoneWeight); +#else + SetWorldVertices(smbs, skinnedMesh.sharedMesh.boneWeights, vertices); +#endif + Transform[] bones = skinnedMesh.bones; + + + foreach (EasyColliderAutoSkinnedBone s in smbs) + { + if (!s.Enabled || !s.IsValid || s.renderer != skinnedMesh) continue; + // ignore excluded, null, and bones with no vertices. + if (s == null || s.WorldSpaceVertices.Count == 0) continue; + // the attach to is the skinned bones transform's gameobject. + properties.AttachTo = s.Transform.gameObject; + s.Matrix = s.Transform.localToWorldMatrix; + // when the mesh isn't optimized, the bones transform is filled + if (bones.Length > 0) + { + // if realigning is enabled, and its not convex meshes + if (realignBones && colliderType != SKINNED_MESH_COLLIDER_TYPE.Convex_Mesh && minRealignAngle > 0.0f) + { + // try realigning by finding the single child bone, comparing the angles, and creating a properly aligned transform + Transform child = GetChildBone(s.Transform, bones); + if (child != null) + { + float minAngle = GetMinimumChildAngle(s.Transform, child); + if (minAngle >= minRealignAngle) + { + // TODO: need to readjust how everything is done to use a matrix instead of a transform. + // properties.AttachTo = CreateRealignedObject(s.Transform, child); + s.Matrix = GetMatrixForObject(s.Transform, child); + if (AutoSkinnedDepenetrate) + { + GameObject newObject = CreateRealignedObject(s.Transform, child); + TemporaryGameObjects.Add(newObject); + properties.AttachTo = newObject; + } + } + } + } + } + // finally, calculate the collider + EasyColliderData d = CalculateColliderData(s.ColliderType, s, properties); + // if its not a mesh collider & we're depenetrating. + if (AutoSkinnedDepenetrate) + { + Collider createdCollider = GenerateCollider(s.ColliderType, properties, s, savePath, true); + if (createdCollider != null) + { + generatedColliders.Add(createdCollider); + data.Add(d); + s.Collider = createdCollider; + } + } + else if (!AutoSkinnedDepenetrate) + { + data.Add(d); + } + } + if (generatedColliders.Count == data.Count && AutoSkinnedDepenetrate) + { + CheckDoDepenetration(generatedColliders); + for (int i = 0; i < generatedColliders.Count; i++) + { + Collider c = generatedColliders[i]; + EasyColliderData d = data[i]; + if (c is BoxCollider) + { + BoxColliderData bd = d as BoxColliderData; + if (bd == null) continue; + BoxCollider bc = c as BoxCollider; + bd.Center = bc.center; + bd.Size = bc.size; + } + else if (c is CapsuleCollider) + { + CapsuleColliderData ccd = d as CapsuleColliderData; + if (ccd == null) continue; + CapsuleCollider cc = c as CapsuleCollider; + ccd.Center = cc.center; + ccd.Radius = cc.radius; + ccd.Height = cc.height; + } + else if (c is SphereCollider) + { + SphereColliderData scd = d as SphereColliderData; + if (scd == null) continue; + SphereCollider sc = c as SphereCollider; + scd.Center = sc.center; + scd.Radius = sc.radius; + } + // would work okay for preview, but not generation. + // else if (c is MeshCollider) + // { + // MeshColliderData mcd = d as MeshColliderData; + // if (mcd == null) continue; + // MeshCollider mc = c as MeshCollider; + // mcd.ConvexMesh = mc.sharedMesh; + // } + } + } + // creating and destroying the objects is really the slow part. + // creating and destroying the colliders with an UNDO registered was the slow part + // since they are only temporary, we can safely just destroy em all without undos. + for (int i = 0; i < generatedColliders.Count; i++) + { + DestroyImmediate(generatedColliders[i]); + } + for (int i = 0; i < TemporaryGameObjects.Count; i++) + { + DestroyImmediate(TemporaryGameObjects[i]); + } + return data; + } + + + /// + /// Generates colliders along a bone chain of a skinned mesh renderer. + /// + /// Skinned mesh renderer + /// Type of colliders to use + /// Parameters to set on created colliders + /// Minimum bone weight to include a vertex in a bones collider + /// Should realigning colliders be performed? + /// When the minimum angle of all of a bone's axis (up, down, left, right, forward, back) and the vector to the next bone in the chain is >= minRealignAngle, realigning is performed if enabled. + /// Full path to save mesh's when colliderType is a Convex Mesh. Ie: C:/UnityProjects/ProjectName/Assets/ConvexHulls/BaseHullName + public List GenerateSkinnedMeshColliders(SkinnedMeshRenderer skinnedMesh, SKINNED_MESH_COLLIDER_TYPE colliderType, EasyColliderProperties properties, float minBoneWeight, bool realignBones = false, float minRealignAngle = 20f, string savePath = "Assets/") + { + if (skinnedMesh == null) return new List(); + renderer = skinnedMesh; + + // bake the mesh to get world space vertices as this works in all cases (bone transforms are valid, bind poses are valid, or the allow deoptimization is used) + // also works for incorrectly rotated roots / mesh renderers etc. + // bake and scale. + Mesh m = new Mesh(); + Vector3 prevScale = skinnedMesh.transform.localScale; + BakeSkinnedMesh(skinnedMesh, m); + Vector3[] vertices = m.vertices; + for (int i = 0; i < vertices.Length; i++) + { + //transform to world relative by root object + vertices[i] = skinnedMesh.transform.TransformPoint(vertices[i]); + } + skinnedMesh.transform.localScale = prevScale; + + // get skinned mesh bones + EasyColliderAutoSkinnedBone[] smbs = BoneList.ToArray(); + if (smbs == null || BoneList.Count == 0) + { + return null; + } + foreach (EasyColliderAutoSkinnedBone smb in smbs) + { + smb.WorldSpaceVertices.Clear(); + } + + List generatedColliders = new List(); + + // set the world vertex for each bone. +#if UNITY_2019_1_OR_NEWER + SetWorldVertices(smbs, skinnedMesh.sharedMesh.GetAllBoneWeights(), skinnedMesh.sharedMesh.GetBonesPerVertex(), vertices, minBoneWeight); +#else + SetWorldVertices(smbs, skinnedMesh.sharedMesh.boneWeights, vertices); +#endif + Transform[] bones = skinnedMesh.bones; + + + foreach (EasyColliderAutoSkinnedBone s in smbs) + { + if (!s.Enabled || !s.IsValid || s.renderer != skinnedMesh) continue; + if (s == null || s.WorldSpaceVertices.Count == 0) continue; + // Debug.Log("Generate on valid bone."); + // the attach to is the skinned bones transform's gameobject. + properties.AttachTo = s.Transform.gameObject; + colliderType = s.ColliderType; + // when the mesh isn't optimized, the bones transform is filled + if (bones.Length > 0) + { + // if realigning is enabled, and its not convex meshes + if (realignBones && colliderType != SKINNED_MESH_COLLIDER_TYPE.Convex_Mesh) + { + // try realigning by finding the single child bone, comparing the angles, and creating a properly aligned transform + Transform child = GetChildBone(s.Transform, bones); + if (child != null) + { + float minAngle = GetMinimumChildAngle(s.Transform, child); + if (minAngle >= minRealignAngle) + { + properties.AttachTo = CreateRealignedObject(s.Transform, child); + } + } + } + } + // finally, create the collider. + Collider createdCollider = GenerateCollider(colliderType, properties, s, savePath, false); + if (createdCollider != null) + { + generatedColliders.Add(createdCollider); + s.Collider = createdCollider; + } + } + + // do depenetration. + CheckDoDepenetration(generatedColliders); + + return generatedColliders; + } + + public bool reverse; + /// + /// Runs the depenetration method(s) if needed. + /// + /// + private void CheckDoDepenetration(List generatedColliders) + { + if (AutoSkinnedDepenetrate) + { + List colliders = null; + if (AutoSkinnedDepenetrateOrder == SKINNED_MESH_DEPENETRATE_ORDER.OutsideIn || AutoSkinnedDepenetrateOrder == SKINNED_MESH_DEPENETRATE_ORDER.InsideOut) + { + colliders = new List(generatedColliders.Count); + // order by descending indent level. + IEnumerable query = BoneList.OrderByDescending(bone => bone.IndentLevel); + foreach (var b in query) + { + if (b.Collider == null || !b.Enabled || !b.IsValid) continue; + colliders.Add(b.Collider); + } + // reverse if needed + if (AutoSkinnedDepenetrateOrder == SKINNED_MESH_DEPENETRATE_ORDER.InsideOut) + { + colliders.Reverse(); + } + } + else + { + // just use generated colliders, reverse if needed. + colliders = new List(generatedColliders); + if (AutoSkinnedDepenetrateOrder == SKINNED_MESH_DEPENETRATE_ORDER.Reverse) + { + colliders.Reverse(); + } + } + // iteratively shrink and shift! + IterativeShrinkAndShift(colliders); + } + } + + /// + /// Number of colliders shrunk during the current shrink iteration. + /// + private int ShrinkCount; + + /// + /// Total shrink amount of all colliders during the current shrink iteration. + /// + private Vector3 ShrinkAmount; + + /// + /// Number of colliders shifted during the current shift iteration. + /// + private int ShiftCount; + + /// + /// Total shift amount of all colliders during the current shift iteration. + /// + private Vector3 ShiftAmount; + + /// + /// Iteratively shrinks then shifts the generated colliders a number of times equal to the value set in preferences. + /// Shrinks with a multiplier of 0.5, shifts, then repeats. + /// + /// + private void IterativeShrinkAndShift(List generatedColliders) + { + for (int i = 0; i < AutoSkinnedIterativeDepenetrationCount; i++) + { + // keep track of shrink & shift count so we can exit once an iteration detects no shifts or shrinks. + ShrinkCount = 0; + ShiftCount = 0; + ShrinkAmount = Vector3.zero; + ShiftAmount = Vector3.zero; + // if we can can shrink colliders, do so first. + if (AutoSkinnedShrinkAmount > 0) + { + ShrinkDepenetrate(generatedColliders, AutoSkinnedShrinkAmount); + } + // then shift the colliders. + ShiftDepenetrate(generatedColliders); + // no more shrinking or shifting? we're done! + if (ShrinkCount == 0 && ShiftCount == 0) + { + return; + } + } + } + + /// + /// Gets a vector3 as a string with more precision by manually building the string with x, y, and z values. + /// + /// + /// + public string LogVector(Vector3 vector3) + { + return "(" + vector3.x + "," + vector3.y + "," + vector3.z + ")"; + } + + + /// + /// Simply a helpful method that logs each colliders name and overlap count, and their max shift vectors. + /// At the end it also logs how many colliders are overlapped with at least 1 other, and the total of the max shift vectors. + /// + /// + private void LogRemainingOverlaps(List generatedColliders) + { + List> datas = new List>(); + foreach (Collider c in generatedColliders) + { + List shifts = new List(); + Transform cTransform = c.transform; + foreach (Collider other in generatedColliders) + { + if (c != other) + { + ShiftData data = new ShiftData(); + if (GetShiftWorld(c, cTransform, other, out data)) + { + if (data.Direction == Vector3.zero) continue; + if (data.Size == 0.0f) continue; + shifts.Add(data); + } + } + } + datas.Add(shifts); + } + Vector3 totalShiftRemaining = Vector3.zero; + int totalOverlapped = 0; + for (int i = 0; i < generatedColliders.Count; i++) + { + if (datas[i].Count > 0) + { + Vector3 maxs = Vector3.zero; + foreach (var s in datas[i]) + { + Vector3 dir = generatedColliders[i].transform.InverseTransformVector(s.Direction * s.Size); + dir.x = Mathf.Abs(dir.x); + dir.y = Mathf.Abs(dir.y); + dir.z = Mathf.Abs(dir.z); + maxs.x = Mathf.Max(maxs.x, dir.x); + maxs.y = Mathf.Max(maxs.y, dir.y); + maxs.z = Mathf.Max(maxs.z, dir.z); + } + if (maxs != Vector3.zero) + { + Debug.Log("Collider:" + generatedColliders[i].transform.name + " Overlaps with:" + datas[i].Count + " Max Shift Vector:" + LogVector(maxs)); + totalShiftRemaining += maxs; + totalOverlapped++; + } + } + } + Debug.Log("Total Overlapped Colliders:" + totalOverlapped + " Total Shift Vector:" + LogVector(totalShiftRemaining)); + } + + + /// + /// Attempts to depenetrate colliders by calculating all the penetration shift vectors, then getting the maximum in each local axis, and shrinking the size, height, radius (depeneding on collider type) approriately. + /// Mult is used to multiply the value(s) used to shrink (for iterations) + /// + /// + /// Amount to multiple the shrink values with. + private void ShrinkDepenetrate(List generatedColliders, float mult = 1.0f) + { + foreach (Collider c in generatedColliders) + { + List shiftDatas = new List(generatedColliders.Count - 1); + Transform cTransform = c.transform; + // get all the shift's needed to depenetrate this collider from all other colliders. + foreach (Collider other in generatedColliders) + { + if (c == other) continue; + ShiftData data = new ShiftData(); + if (GetShiftWorld(c, cTransform, other, out data)) + { + shiftDatas.Add(data); + } + } + // no shifts? skip. + if (shiftDatas.Count == 0) continue; + // calculate the maximum amount we would need to shift on each axis to depenetrate from all colliders. + Vector3 maxShifts = Vector3.zero; + foreach (ShiftData data in shiftDatas) + { + Vector3 direction = cTransform.InverseTransformVector(data.Direction * data.Size); + // since we're shrinking (and not shifting), we want the absolute value as sign doesn't matter. + direction.x = Mathf.Abs(direction.x); + direction.y = Mathf.Abs(direction.y); + direction.z = Mathf.Abs(direction.z); + if (direction.x > maxShifts.x) + { + maxShifts.x = direction.x; + } + if (direction.y > maxShifts.y) + { + maxShifts.y = direction.y; + } + if (direction.z > maxShifts.z) + { + maxShifts.z = direction.z; + } + } + // do we have a value to shrink? + if (maxShifts != Vector3.zero) + { + // keep track of how many shrinks we've done this iteration so we can exit early if complete. + ShrinkCount++; + ShrinkAmount += maxShifts; + if (c is BoxCollider) + { + // shrink a box collider + BoxCollider bc = c as BoxCollider; + Vector3 size = bc.size; + size -= maxShifts * mult; + // if we go into a negative size from all the shifts, this collider's size should be zero. + if (size.x < 0 || size.y < 0 || size.z < 0) + { + size = Vector3.zero; + } + bc.size = size; + } + else if (c is CapsuleCollider) + { + // shrink a capsule collider. + CapsuleCollider cc = c as CapsuleCollider; + float hChange = 0.0f; + float rChange = 0.0f; + if (cc.direction == 0) //x + { + hChange = maxShifts.x; + // radius change is the max-shift of the non-height axis. + rChange = maxShifts.y > maxShifts.z ? maxShifts.y : maxShifts.z; + } + else if (cc.direction == 1) //y + { + hChange = maxShifts.y; + rChange = maxShifts.x > maxShifts.z ? maxShifts.x : maxShifts.z; + } + else //z + { + hChange = maxShifts.z; + rChange = maxShifts.y > maxShifts.x ? maxShifts.y : maxShifts.x; + } + hChange *= mult; + rChange *= mult; + cc.height -= hChange; + cc.radius -= rChange; + if (cc.height < 0 || cc.radius < 0) + { + cc.height = 0; + cc.radius = 0; + } + } + else if (c is SphereCollider) + { + // simply shrink a sphere by reducing the radius by the maximum shift on any axis. + SphereCollider sc = c as SphereCollider; + sc.radius -= (Mathf.Max(maxShifts.x, Mathf.Max(maxShifts.y, maxShifts.z))) * mult; + if (sc.radius < 0) + { + sc.radius = 0; + } + } + // works for preview, would need to adjust actual source mesh for generation. + // else if (c is MeshCollider) + // { + // MeshCollider mc = c as MeshCollider; + // Vector3[] vertices = mc.sharedMesh.vertices; + // for (int i = 0; i < vertices.Length; i++) + // { + // vertices[i] -= maxShifts * mult; + // } + // mc.sharedMesh.vertices = vertices; + // } + } + } + } + + /// + /// Shifts a collider by an amount in local space. Essentially just converts the collider to a box, capsule, or sphere, and shifts the center by amount. + /// + /// + /// + private void ShiftCollider(Collider c, Vector3 localAmount) + { + if (c is BoxCollider) + { + BoxCollider bc = c as BoxCollider; + bc.center += localAmount; + } + else if (c is CapsuleCollider) + { + CapsuleCollider cc = c as CapsuleCollider; + cc.center += localAmount; + } + else if (c is SphereCollider) + { + SphereCollider sc = c as SphereCollider; + sc.center += localAmount; + } + // works for preview, would need to adjust source mesh for generation. + // else if (c is MeshCollider) + // { + // MeshCollider mc = c as MeshCollider; + // Vector3[] vertices = mc.sharedMesh.vertices; + // for (int i = 0; i < vertices.Length; i++) + // { + // vertices[i] += localAmount; + // } + // mc.sharedMesh.vertices = vertices; + // } + } + + /// + /// Attempts to depenetrate colliders by calculating the average of all ComputeDepenetration Vectors and apply it to the collider's center. + /// + /// + private void ShiftDepenetrate(List generatedColliders) + { + // TODO: use overlap to get direct array to use instead of n^2? + foreach (Collider c in generatedColliders) + { + // create a list of shifts for each collider. + List datas = new List(generatedColliders.Count - 1); + // cache transform + Transform cTransform = c.transform; + foreach (Collider other in generatedColliders) + { + if (c == other) continue; + ShiftData data = new ShiftData(); + // if there's a shift, add it to that colliders shift data list. + if (GetShiftWorld(c, cTransform, other, out data)) + { + datas.Add(data); + } + } + // no shifts? skip. + if (datas.Count == 0) continue; + Vector3 shiftVector = Vector3.zero; + // calculate the total shift vector. + // since we're doing this on all colliders, colliders's that are "stuck" between child colliders will likely not shift much + // and instead colliders that are free to shift in one direction will shift out first, which makes sense to do. + foreach (ShiftData data in datas) + { + shiftVector += cTransform.InverseTransformVector(data.Direction * data.Size); + } + // if we have a shift vector, shift! + if (shiftVector != Vector3.zero) + { + ShiftCount++; + ShiftAmount += shiftVector; + shiftVector /= datas.Count; + ShiftCollider(c, shiftVector); + } + } + } + + /// + /// Data from a Physics.ComputePenetration result. + /// + private struct ShiftData + { + public Vector3 Direction; + public float Size; + + public ShiftData(Vector3 direction, float size) + { + this.Direction = direction; + this.Size = size; + } + } + + /// + /// Sets set shift data with the penetrations direction and size if there is one. + /// + /// + /// + /// + /// True if there is a penetration, false otherwise + private bool GetShiftWorld(Collider c, Transform ct, Collider other, out ShiftData data) + { + Vector3 direction = Vector3.zero; + float distance = 0.0f; + Transform ot = other.transform; + if (Physics.ComputePenetration(c, ct.position, ct.rotation, other, ot.position, ot.rotation, out direction, out distance)) + { + data.Direction = direction; + data.Size = distance; + return true; + } + data.Direction = Vector3.zero; + data.Size = -1f; + return false; + } + + /// + /// Gets the child bone of a transform if it has a single valid child bone. + /// + /// Bone to get child of + /// Array of bones + /// Transform of child bone if found, otherwise null + private Transform GetChildBone(Transform bone, Transform[] bones) + { + int boneChildCount = bone.childCount; + Transform childBone = null; + Transform currentChildTransform = null; + int totalValidChildBones = 0; + for (int j = 0; j < boneChildCount; j++) + { + currentChildTransform = bone.GetChild(j); + int index = Array.IndexOf(bones, currentChildTransform); + if (index >= 0) + { + totalValidChildBones += 1; + childBone = currentChildTransform; + } + } + if (totalValidChildBones == 1) + { + return childBone; + } + return null; + } + + /// + /// Gets the minimum angle between all of transform's axis and the direction from transform to child. + /// + /// transform to use axis from + /// child to get minimum angle to + /// Minimum angle from all of transform's axis and the direction from transform to child. + private float GetMinimumChildAngle(Transform transform, Transform child) + { + Vector3 childDir = child.position - transform.position; + float minAngle = Mathf.Infinity; + float angle = Vector3.Angle(transform.right, childDir); + minAngle = minAngle > angle ? angle : minAngle; + angle = Vector3.Angle(-transform.right, childDir); + minAngle = minAngle > angle ? angle : minAngle; + angle = Vector3.Angle(transform.forward, childDir); + minAngle = minAngle > angle ? angle : minAngle; + angle = Vector3.Angle(-transform.forward, childDir); + minAngle = minAngle > angle ? angle : minAngle; + angle = Vector3.Angle(transform.up, childDir); + minAngle = minAngle > angle ? angle : minAngle; + angle = Vector3.Angle(-transform.up, childDir); + minAngle = minAngle > angle ? angle : minAngle; + return minAngle; + } + + /// + /// Gets all the skinned mesh bones for the current skinned mesh renderer. + /// + /// + /// Array of skinned mesh bones. + private EasyColliderAutoSkinnedBone[] GetSkinnedMeshBones(SkinnedMeshRenderer skinnedMesh) + { + int validBonesFound = 0; + EasyColliderAutoSkinnedBone[] smbs = null; + // first, if there are transform in the bones array, we use that to get everything + if (skinnedMesh.bones.Length > 0) + { + smbs = new EasyColliderAutoSkinnedBone[skinnedMesh.bones.Length]; + // try to match based on bones + Transform[] bones = skinnedMesh.bones; + for (int i = 0; i < bones.Length; i++) + { + // a bone was deleted.... + if (bones[i] == null) + { + smbs[i] = new EasyColliderAutoSkinnedBone(new Matrix4x4(), i, bones[i]); + } + else + { + smbs[i] = new EasyColliderAutoSkinnedBone(bones[i].localToWorldMatrix, i, bones[i]); + smbs[i].BoneName = bones[i].name; + smbs[i].renderer = skinnedMesh; + validBonesFound++; + } + } + } + return smbs; + } + + // native array and boneweight1 functionality are 2019.1+ +#if UNITY_2019_1_OR_NEWER + /// + /// Sets the skinned mesh bone's world vertices list. + /// + /// Skinned mesh we are trying to set world vertices for + /// Array of all skinned mesh bones + /// Array of all vertices in world space + /// Minimum bone weight to include a vertex in a bone's vertex list. + private void SetWorldVertices(EasyColliderAutoSkinnedBone[] skinnedMeshBones, NativeArray boneWeights, NativeArray bonesPerVertex, Vector3[] worldVertices, float minBoneWeight) + { + int boneIndex = 0; + for (int i = 0; i < worldVertices.Length; i++) + { + int numBonesForVertex = bonesPerVertex[i]; + for (int j = 0; j < numBonesForVertex; j++) + { + BoneWeight1 boneWeight = boneWeights[boneIndex]; + if (boneWeight.boneIndex < skinnedMeshBones.Length && boneWeight.weight >= skinnedMeshBones[boneWeight.boneIndex].BoneWeight) + { + skinnedMeshBones[boneWeight.boneIndex].WorldSpaceVertices.Add(worldVertices[i]); + } + boneIndex++; + } + } + } +#endif + + /// + /// Sets the skinned mesh bone's world vertices list. + /// + /// Array of all skinned mesh bones + /// Array of all bone weights + /// Array of all vertices in world space + /// Minimum bone weight to include a vertex in a bone's vertex list. + private void SetWorldVertices(EasyColliderAutoSkinnedBone[] skinnedMeshBones, BoneWeight[] boneWeights, Vector3[] worldVertices) + { + for (int i = 0; i < boneWeights.Length; i++) + { + // make sure the weight is above the minimum weight. + if (skinnedMeshBones[boneWeights[i].boneIndex0] != null && boneWeights[i].weight0 >= skinnedMeshBones[boneWeights[i].boneIndex0].BoneWeight) + { + // add the vertex to that bone's vertex list + skinnedMeshBones[boneWeights[i].boneIndex0].WorldSpaceVertices.Add(worldVertices[i]); + } + if (skinnedMeshBones[boneWeights[i].boneIndex1] != null && boneWeights[i].weight1 >= skinnedMeshBones[boneWeights[i].boneIndex1].BoneWeight) + { + // add the vertex to that bone's vertex list + skinnedMeshBones[boneWeights[i].boneIndex1].WorldSpaceVertices.Add(worldVertices[i]); + } + if (skinnedMeshBones[boneWeights[i].boneIndex2] != null && boneWeights[i].weight2 >= skinnedMeshBones[boneWeights[i].boneIndex2].BoneWeight) + { + // add the vertex to that bone's vertex list + skinnedMeshBones[boneWeights[i].boneIndex2].WorldSpaceVertices.Add(worldVertices[i]); + } + if (skinnedMeshBones[boneWeights[i].boneIndex3] != null && boneWeights[i].weight3 >= skinnedMeshBones[boneWeights[i].boneIndex3].BoneWeight) + { + // add the vertex to that bone's vertex list + skinnedMeshBones[boneWeights[i].boneIndex3].WorldSpaceVertices.Add(worldVertices[i]); + } + } + } + + public void OnBeforeSerialize() + { + //nothing needed + } + + public void OnAfterDeserialize() + { + // need to relink the classes as otherwise the instances become seperate instances during serialization. + for (int i = 0; i < SortedBoneList.Count; i++) + { + SortedBoneList[i] = BoneList[SortedBoneList[i].BoneIndex]; + } + } + } +} diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderAutoSkinned.cs.meta b/Assets/EasyColliderEditor/Scripts/EasyColliderAutoSkinned.cs.meta new file mode 100644 index 00000000..077b7904 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderAutoSkinned.cs.meta @@ -0,0 +1,20 @@ +fileFormatVersion: 2 +guid: 45eb3a409ea753b44ab3bb6b8585be07 +timeCreated: 1591641873 +licenseType: Store +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/EasyColliderAutoSkinned.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderAutoSkinnedBone.cs b/Assets/EasyColliderEditor/Scripts/EasyColliderAutoSkinnedBone.cs new file mode 100644 index 00000000..c83be8a3 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderAutoSkinnedBone.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using UnityEngine; +namespace ECE +{ + [System.Serializable] + /// + /// Class to hold data for a skinned meshes' bones + /// Used because the bones transform array can be empty in cases where optimize gameobjects is used on import + /// The bindbose index of a bone, is the same as a bone's boneindex, so boneweight's can still be used. + /// + public class EasyColliderAutoSkinnedBone + { + + public SkinnedMeshRenderer renderer; + + /// + /// Create a collider for this bone? + /// + public bool Enabled = true; + /// + /// What kind of collider to create. + /// + public SKINNED_MESH_COLLIDER_TYPE ColliderType = SKINNED_MESH_COLLIDER_TYPE.Box; + /// + /// Minimum skinning bone weight to include a vertex in the bone's collider calculations. + /// + public float BoneWeight = 0.5f; + /// + /// Bone's display name (transform.name) + /// + public string BoneName = "Default"; + /// + /// Is this bone paired with another bone? + /// + public bool IsPaired = false; + /// + /// If this bone is paired, is this the bone that is displayed in the UI? + /// + public bool IsPairDisplayBone = false; + /// + /// Is this bone valid? (Has at least 1 vertex that meets it's boneWeight) + /// + public bool IsValid = false; + + public EasyColliderAutoSkinnedBone(Matrix4x4 bp, int index, Transform t) + { + BoneIndex = index; + BindPose = bp; + Transform = t; + } + + /// + /// Local to world matrix of the bone's transform. + /// + public Matrix4x4 Matrix; + + /// + /// Bind pose of the bone + /// + public Matrix4x4 BindPose; + + /// + /// Bind pose index is the bone index as well. + /// + public int BoneIndex; + + /// + /// Transform of the bone + /// + public Transform Transform; + + /// + /// List of vertices in world space for this bo + /// + /// + /// + public List WorldSpaceVertices = new List(); + + [SerializeField] + /// + /// List of other bone's index's in the BoneList that this bone is paired with. + /// + /// + /// + public List PairedBones = new List(); + + /// + /// Collider for this bone. + /// + public Collider Collider; + + /// + /// Indent level of this bone when displaying in UI. + /// + public int IndentLevel = -1; + } +} \ No newline at end of file diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderAutoSkinnedBone.cs.meta b/Assets/EasyColliderEditor/Scripts/EasyColliderAutoSkinnedBone.cs.meta new file mode 100644 index 00000000..f135b79b --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderAutoSkinnedBone.cs.meta @@ -0,0 +1,20 @@ +fileFormatVersion: 2 +guid: 22d8868ad24ea0c43828e22314004216 +timeCreated: 1625687084 +licenseType: Store +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/EasyColliderAutoSkinnedBone.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderCreator.cs b/Assets/EasyColliderEditor/Scripts/EasyColliderCreator.cs new file mode 100644 index 00000000..150a91fc --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderCreator.cs @@ -0,0 +1,2176 @@ + +using System.Collections.Generic; +using UnityEngine; +#if (UNITY_EDITOR) +using UnityEditor; +#endif +using System.Linq; +namespace ECE +{ + /// + /// Class to calculate and add colliders + /// + public class EasyColliderCreator + { + +#if (UNITY_EDITOR) + + /// + /// Used in ECEAutoSkinned when generating previews / depenetration. + /// + public bool UndoEnabled = true; + + /// + /// Just an easy way to get an instance of preferences to create colliders with. + /// + /// + private EasyColliderPreferences ECEPreferences + { + get { return EasyColliderPreferences.Preferences; } + } +#endif + /// + /// Data struct from calculating a best fit sphere + /// + private struct BestFitSphere + { + /// + /// Center of the sphere + /// + public Vector3 Center; + + /// + /// Radius of the sphere + /// + public float Radius; + + /// + /// Best Fit Sphere + /// + /// Center of the sphere + /// Radius of the sphere + public BestFitSphere(Vector3 center, float radius) + { + this.Center = center; + this.Radius = radius; + } + } + + + + // rotate and duplicate are editor-only +#if (UNITY_EDITOR) + + /// + /// gets a rotation as a quaternion from a transformation matrix. + /// useful for matrix operations in earlier versions of unity + /// + /// + /// + private Quaternion RotationFromMatrix(Matrix4x4 matrix) + { + Vector3 forward = new Vector3(matrix[0, 2], matrix[1, 2], matrix[2, 2]); + Vector3 up = new Vector3(matrix[0, 1], matrix[1, 1], matrix[2, 1]); + return Quaternion.LookRotation(forward, up); + } + + /// + /// gets a scale from a transformation matrix. + /// useful for matrix operations in earlier versions of unity + /// + /// + /// + private Vector3 ScaleFromMatrix(Matrix4x4 matrix) + { + Vector3 mScale = Vector3.zero; + mScale.x = new Vector4(matrix[0, 0], matrix[1, 0], matrix[2, 0], matrix[3, 0]).magnitude; + mScale.y = new Vector4(matrix[0, 1], matrix[1, 1], matrix[2, 1], matrix[3, 1]).magnitude; + mScale.z = new Vector4(matrix[0, 2], matrix[1, 2], matrix[2, 2], matrix[3, 2]).magnitude; + return mScale; + } + + private Vector3 PositionFromMatrix(Matrix4x4 matrix) + { + Vector3 mScale = new Vector3(matrix[0, 3], matrix[1, 3], matrix[2, 3]); + return mScale; + } + + public List CalculateRotateAndDuplicate(EasyColliderPreferences preferences, EasyColliderEditor ece) + { + EasyColliderRotateDuplicate ecrd = preferences.rotatedDupeSettings; + ecrd.attachTo = ece.AttachToObject; + return CalculateRotateAndDuplicate(preferences.PreviewColliderType, ece.GetWorldVertices(true), ecrd); + } + + private List CalculateRotateAndDuplicate(CREATE_COLLIDER_TYPE result, List worldVertices, EasyColliderRotateDuplicate ecrd) + { + Transform attachTo = ecrd.attachTo.transform; + Transform pivot = ecrd.pivot.transform; + List data = new List(); + // we want to create the normal colliders in the attachto's local space, but rotate them around the pivot point. + List localVertices = ToLocalVerts(attachTo, worldVertices); + EasyColliderData baseData = null; + switch (result) + { + case CREATE_COLLIDER_TYPE.BOX: + // we want the normal colliders calculated in local space of the attach to object. + // baseData = CalculateBox(worldVertices, attachTo, false); + baseData = CalculateBoxLocal(localVertices); + //need to manually handle this here since we're not using the calculate(method) methods, just the calculate methods. +#if(UNITY_EDITOR) + BoxColliderData bc = (BoxColliderData)baseData; + bc.Size *= ECEPreferences.ShrinkGrow; +#endif + break; + case CREATE_COLLIDER_TYPE.ROTATED_BOX: + baseData = CalculateBox(worldVertices, pivot, true, true); + break; + case CREATE_COLLIDER_TYPE.SPHERE: + baseData = CalculateSphereMinMaxLocal(localVertices); +#if (UNITY_EDITOR) + SphereColliderData sc = (SphereColliderData)baseData; + sc.Radius *= ECEPreferences.ShrinkGrow; +#endif + break; + case CREATE_COLLIDER_TYPE.CAPSULE: + baseData = CalculateCapsuleMinMaxLocal(localVertices, ECEPreferences.CapsuleColliderMethod); +#if (UNITY_EDITOR) + CapsuleColliderData cc = (CapsuleColliderData)baseData; + cc.Radius *= ECEPreferences.ShrinkGrow; + cc.Height *= ECEPreferences.ShrinkGrow; +#endif + break; + case CREATE_COLLIDER_TYPE.ROTATED_CAPSULE: + baseData = CalculateCapsuleMinMax(worldVertices, pivot, ECEPreferences.CapsuleColliderMethod, true, true); + break; + case CREATE_COLLIDER_TYPE.CONVEX_MESH: +#if (UNITY_EDITOR) + AdjustVerticesForShrinkGrow(localVertices, ECEPreferences.ShrinkGrow); +#endif + baseData = CalculateMeshColliderQuickHullLocal(localVertices); + break; + case CREATE_COLLIDER_TYPE.CYLINDER: +#if (UNITY_EDITOR) + AdjustVerticesForShrinkGrow(localVertices, ECEPreferences.ShrinkGrow); +#endif + baseData = CalculateCylinderColliderLocal(localVertices, ECEPreferences.CylinderNumberOfSides, ECEPreferences.CylinderOrientation, ECEPreferences.CylinderRotationOffset); + break; + } + + + if (result != CREATE_COLLIDER_TYPE.ROTATED_BOX && result != CREATE_COLLIDER_TYPE.ROTATED_CAPSULE) + { + // rotated colliders have thier own matrix. + baseData.Matrix = attachTo.localToWorldMatrix; + } + + // just calculating the angle and axis to rotate + float rotationAngle = (ecrd.EndRotation - ecrd.StartRotation) / (ecrd.NumberOfDuplications); + float currentAngle = ecrd.StartRotation; + Vector3 axis = Vector3.zero; + axis = ecrd.axis == EasyColliderRotateDuplicate.ROTATE_AXIS.X ? pivot.right : axis; + axis = ecrd.axis == EasyColliderRotateDuplicate.ROTATE_AXIS.Y ? pivot.up : axis; + axis = ecrd.axis == EasyColliderRotateDuplicate.ROTATE_AXIS.Z ? pivot.forward : axis; + + Quaternion baseRotation = RotationFromMatrix(baseData.Matrix); + // will just be the attachTo's position at this point. + Vector3 basePosition = PositionFromMatrix(baseData.Matrix); + // lossy scale essentially. + Vector3 baseScale = ScaleFromMatrix(baseData.Matrix); + //Debug.Log("Base Pos:" + basePosition + " Base Rotation:" + baseRotation + " Base Scale:" + baseScale); + // used in the longer version, leaving in here for future reference use. + // Matrix4x4 pivotTranslation = Matrix4x4.Translate(pivot.position); + + for (int i = 0; i < ecrd.NumberOfDuplications; i++) + { + // rotation around pivot's axis. + Quaternion q = Quaternion.AngleAxis(currentAngle, axis); + // We're gonna leave this here for my future self, as matrix math still confuses me + // create rotation matrix + // Matrix4x4 rotationMatrix = Matrix4x4.TRS(Vector3.zero, q * baseRotation, mScale); + // // // calculate rotated forward from the pivot to the attach to object. + //Vector3 forward = q * (attachTo.position - pivot.position); + // // create a matrix for the rotated pivot to attach translation + // var pushTranslate = Matrix4x4.Translate(forward); + // // use the rotation matrix + // m = rotationMatrix; + // // translated it to the pivot point. + // m = pivotTranslation * m; + // // push out from pivot + // m = pushTranslate * m; + // Above: simplified + //m = Matrix4x4.TRS(basePosition + forward, q * baseRotation, baseScale); + // but the preview matrix isn't correclty factoring in non-uniform scale. + //} + Vector3 forward = q * (attachTo.position - pivot.position); + Matrix4x4 m; m = Matrix4x4.TRS(basePosition + forward, q * baseRotation, baseScale); + // preview not correctly using non-uniform scale. + switch (result) + { + case CREATE_COLLIDER_TYPE.CAPSULE: + case CREATE_COLLIDER_TYPE.ROTATED_CAPSULE: + { + CapsuleColliderData d1 = baseData as CapsuleColliderData; + CapsuleColliderData d2 = new CapsuleColliderData(); + d2.Clone(d1); + d2.Matrix = m; + data.Add(d2); + break; + } + case CREATE_COLLIDER_TYPE.BOX: + case CREATE_COLLIDER_TYPE.ROTATED_BOX: + { + BoxColliderData d1 = baseData as BoxColliderData; + BoxColliderData d2 = new BoxColliderData(); + d2.Clone(d1); + d2.Matrix = m; + data.Add(d2); + break; + } + case CREATE_COLLIDER_TYPE.SPHERE: + { + SphereColliderData d1 = baseData as SphereColliderData; + SphereColliderData d2 = new SphereColliderData(); + d2.Clone(d1); + d2.Matrix = m; + data.Add(d2); + break; + } + case CREATE_COLLIDER_TYPE.CONVEX_MESH: + case CREATE_COLLIDER_TYPE.CYLINDER: + { + MeshColliderData d1 = baseData as MeshColliderData; + MeshColliderData d2 = new MeshColliderData(); + d2.Clone(d1); + d2.Matrix = m; + data.Add(d2); + break; + } + } + currentAngle += rotationAngle; + } + return data; + } + + + /// + /// for actually creating the colliders. + /// + /// + /// + /// + /// + public List CreateRotateAndDuplicateColliders(CREATE_COLLIDER_TYPE collider_type, List worldVertices, EasyColliderProperties properties) + { + // Debug.Log("ECC: Create Rotated and Duplicate Colliders"); + List CreatedColliders = new List(); + EasyColliderRotateDuplicate ecrd = ECEPreferences.rotatedDupeSettings; + float rotationPerCollider = (ecrd.EndRotation - ecrd.StartRotation) / ecrd.NumberOfDuplications; + float currentRotation = 0.0f; + Transform parent = properties.AttachTo.transform; + List data = CalculateRotateAndDuplicate(collider_type, worldVertices, ecrd); + foreach (EasyColliderData d in data) + { + GameObject obj = new GameObject("Rotated Collider"); +#if (UNITY_EDITOR) + Undo.RegisterCreatedObjectUndo(obj, " Create Rotated Collider"); +#endif + obj.transform.rotation = RotationFromMatrix(d.Matrix); + obj.transform.SetParent(ecrd.pivot.transform); + obj.transform.position = PositionFromMatrix(d.Matrix); + obj.transform.localScale = Vector3.one; + obj.layer = properties.Layer; + properties.AttachTo = obj; + properties.Orientation = COLLIDER_ORIENTATION.ROTATED; + Collider collider = null; + switch (collider_type) + { + case CREATE_COLLIDER_TYPE.BOX: + obj.name = "Rotated Box Collider"; + collider = CreateBoxCollider(d as BoxColliderData, properties, false); + break; + case CREATE_COLLIDER_TYPE.ROTATED_BOX: + obj.name = "Rotated Box Collider"; + BoxColliderData rbd = d as BoxColliderData; + collider = CreateBoxCollider(rbd, properties, false); + break; + case CREATE_COLLIDER_TYPE.SPHERE: + obj.name = "Rotated Sphere Collider"; + collider = CreateSphereCollider(d as SphereColliderData, properties, false); + break; + case CREATE_COLLIDER_TYPE.CAPSULE: + obj.name = "Rotated Capsule Collider"; + collider = CreateCapsuleCollider(d as CapsuleColliderData, properties, false); + break; + case CREATE_COLLIDER_TYPE.ROTATED_CAPSULE: + obj.name = "Rotated Capsule Collider"; + CapsuleColliderData rcd = d as CapsuleColliderData; + collider = CreateCapsuleCollider(rcd, properties, false); + break; + case CREATE_COLLIDER_TYPE.CONVEX_MESH: + case CREATE_COLLIDER_TYPE.CYLINDER: + MeshColliderData mcd = d as MeshColliderData; + if (ECEPreferences.SaveConvexHullAsAsset) + { + EasyColliderSaving.CreateAndSaveMeshAsset(mcd.ConvexMesh, parent.gameObject); + } + collider = CreateConvexMeshCollider(mcd.ConvexMesh, properties.AttachTo, properties); + break; + } + CreatedColliders.Add(collider); + currentRotation += rotationPerCollider; +#if (UNITY_EDITOR) + Undo.SetTransformParent(obj.transform, parent, "Change Parent"); +#else + obj.transform.SetParent(parent); +#endif + obj.transform.localScale = Vector3.one; + PostColliderCreationProcess(collider, properties); + } + return CreatedColliders; + } + +#endif + + // merge colliders are editor-only. +#if (UNITY_EDITOR) + + #region MergeColliders + + + + /// + /// Merges all colliders in the list to a single resultant collider and returns it. + /// + /// List of colliders to merge + /// Type of collider we want the colliders merged into + /// Properties to set on the new collider + /// Single merged collider. + public Collider MergeColliders(List collidersToMerge, CREATE_COLLIDER_TYPE result, EasyColliderProperties properties) + { + if (properties.Orientation == COLLIDER_ORIENTATION.ROTATED) + { + properties.AttachTo = GetFirstNonNullTransform(collidersToMerge).gameObject; + } + EasyColliderData data = MergeCollidersPreview(collidersToMerge, result, properties.AttachTo.transform); + if (result == CREATE_COLLIDER_TYPE.BOX || result == CREATE_COLLIDER_TYPE.ROTATED_BOX) + { + if (result == CREATE_COLLIDER_TYPE.ROTATED_BOX) + { + properties.AttachTo = GetFirstNonNullTransform(collidersToMerge).gameObject; + } + return CreateBoxCollider(data as BoxColliderData, properties); + } + else if (result == CREATE_COLLIDER_TYPE.CAPSULE || result == CREATE_COLLIDER_TYPE.ROTATED_CAPSULE) + { + if (result == CREATE_COLLIDER_TYPE.ROTATED_CAPSULE) + { + properties.AttachTo = GetFirstNonNullTransform(collidersToMerge).gameObject; + } + return CreateCapsuleCollider(data as CapsuleColliderData, properties); + } + else if (result == CREATE_COLLIDER_TYPE.SPHERE) + { + return CreateSphereCollider(data as SphereColliderData, properties); + } + else if (result == CREATE_COLLIDER_TYPE.CONVEX_MESH || result == CREATE_COLLIDER_TYPE.CYLINDER) + { + MeshColliderData d = data as MeshColliderData; + if (ECEPreferences.SaveConvexHullAsAsset) + { + EasyColliderSaving.CreateAndSaveMeshAsset(d.ConvexMesh, properties.AttachTo); + } + return CreateConvexMeshCollider(d.ConvexMesh, properties.AttachTo, properties); + } + return null; + } + + /// + /// Returns the first transform of a non-null collider + /// + /// list of colliders + /// first non-null collider's transform, null if no non-null colliders + private Transform GetFirstNonNullTransform(List collidersList) + { + foreach (Collider c in collidersList) + { + if (c != null) + { + return c.transform; + } + } + return null; + } + + /// + /// Calculates the preview data for merged colliders. + /// + /// + /// + /// + /// + public EasyColliderData MergeCollidersPreview(List collidersToMerge, CREATE_COLLIDER_TYPE result, Transform attachTo) + { + List worldVertices = GetWorldVertsForColliders(collidersToMerge); + EasyColliderData d = new EasyColliderData(); + if (result == CREATE_COLLIDER_TYPE.CONVEX_MESH) + { + return EasyColliderQuickHull.CalculateHullData(worldVertices, attachTo); + } + else if (result == CREATE_COLLIDER_TYPE.BOX || result == CREATE_COLLIDER_TYPE.ROTATED_BOX) + { + if (result == CREATE_COLLIDER_TYPE.ROTATED_BOX) + { + attachTo = GetFirstNonNullTransform(collidersToMerge); + } + return CalculateBox(worldVertices, attachTo, false); + } + else if (result == CREATE_COLLIDER_TYPE.SPHERE) + { + // does it make sense to allow different sphere methods -> or just min-max method. + if (ECEPreferences.SphereColliderMethod == SPHERE_COLLIDER_METHOD.MinMax) + { + return CalculateSphereMinMax(worldVertices, attachTo); + } + else if (ECEPreferences.SphereColliderMethod == SPHERE_COLLIDER_METHOD.Distance) + { + return CalculateSphereDistance(worldVertices, attachTo); + } + else if (ECEPreferences.SphereColliderMethod == SPHERE_COLLIDER_METHOD.BestFit) + { + return CalculateSphereBestFit(worldVertices, attachTo); + } + } + else if (result == CREATE_COLLIDER_TYPE.CAPSULE || result == CREATE_COLLIDER_TYPE.ROTATED_CAPSULE) + { + if (result == CREATE_COLLIDER_TYPE.ROTATED_CAPSULE) + { + attachTo = GetFirstNonNullTransform(collidersToMerge); + } + // does it make sense to allow capsule methods? -> can use various min max + dia, radius etc. + if (ECEPreferences.CapsuleColliderMethod == CAPSULE_COLLIDER_METHOD.BestFit) + { + return CalculateCapsuleBestFit(worldVertices, attachTo, false); + } + else + { + return CalculateCapsuleMinMax(worldVertices, attachTo, ECEPreferences.CapsuleColliderMethod, false); + } + } + else if (result == CREATE_COLLIDER_TYPE.CYLINDER) + { + return CalculateCylinderCollider(worldVertices, attachTo); + } + return d; + } + + public List GetWorldVertsForColliders(List colliders) + { + List worldVertices = new List(); + foreach (Collider col in colliders) + { + // Get world vertices for mesh collider. + MeshCollider mc = col as MeshCollider; + if (mc != null) + { + AddWorldVerts(mc, worldVertices); + continue; + } + BoxCollider box = col as BoxCollider; + if (box != null) + { + AddWorldVerts(box, worldVertices); + continue; + } + CapsuleCollider capsule = col as CapsuleCollider; + if (capsule != null) + { + AddWorldVerts(capsule, worldVertices); + continue; + } + SphereCollider sphere = col as SphereCollider; + if (sphere != null) + { + AddWorldVerts(sphere, worldVertices); + continue; + } + } + return worldVertices; + } + + /// + /// Adds the vertices of a mesh collider to the world vertices list + /// + /// mesh collider + /// world vertices list + private void AddWorldVerts(MeshCollider meshCollider, List worldVertices) + { + if (meshCollider == null || meshCollider.sharedMesh == null) return; + Vector3[] vertices = meshCollider.sharedMesh.vertices; + Transform t = meshCollider.transform; + for (int i = 0; i < vertices.Length; i++) + { + vertices[i] = t.TransformPoint(vertices[i]); + } + worldVertices.AddRange(vertices); + } + + /// + /// Adds the vertices of a box collider to the world vertices list + /// + /// box collider + /// world vertices list + private void AddWorldVerts(BoxCollider boxCollider, List worldVertices) + { + Vector3 halfSize = boxCollider.size / 2; + Vector3 center = boxCollider.center; + Vector3[] vertices = new Vector3[8]{ + center + halfSize, //0 + center + new Vector3(halfSize.x, halfSize.y, -halfSize.z), //1 + center + new Vector3(halfSize.x, -halfSize.y, halfSize.z), //2 + center + new Vector3(halfSize.x, -halfSize.y, -halfSize.z), //3 + center + new Vector3(-halfSize.x, halfSize.y, halfSize.z), //4 + center + new Vector3(-halfSize.x, halfSize.y, -halfSize.z), //5 + center + new Vector3(-halfSize.x, -halfSize.y, halfSize.z), //6 + center - halfSize, //7 + }; + int triangleOffset = worldVertices.Count; + Transform t = boxCollider.transform; + for (int i = 0; i < vertices.Length; i++) + { + vertices[i] = t.TransformPoint(vertices[i]); + } + // add triangles and verts + worldVertices.AddRange(vertices); + } + + /// + /// Adds the vertices of a sphere collider to the world vertices list + /// + /// sphere collider + /// world vertices list + private void AddWorldVerts(SphereCollider sphereCollider, List worldVertices) + { + AddWorldVertsSphere(sphereCollider.transform, sphereCollider.center, sphereCollider.radius, worldVertices); + } + + /// + /// Adds the vertices of a capsule collider to the world vertices list + /// + /// capsule collider + /// world vertices list + private void AddWorldVerts(CapsuleCollider capsuleCollider, List worldVertices) + { + Vector3 top = Vector3.zero; + Vector3 bottom = Vector3.zero; + if (capsuleCollider.direction == 0) //x + { + top = capsuleCollider.center + Vector3.right * ((capsuleCollider.height - capsuleCollider.radius * 2) / 2); + bottom = capsuleCollider.center - Vector3.right * ((capsuleCollider.height - capsuleCollider.radius * 2) / 2); + } + else if (capsuleCollider.direction == 1) //y + { + top = capsuleCollider.center + Vector3.up * ((capsuleCollider.height - capsuleCollider.radius * 2) / 2); + bottom = capsuleCollider.center - Vector3.up * ((capsuleCollider.height - capsuleCollider.radius * 2) / 2); + } + else if (capsuleCollider.direction == 2) //z + { + top = capsuleCollider.center + Vector3.forward * ((capsuleCollider.height - capsuleCollider.radius * 2) / 2); + bottom = capsuleCollider.center - Vector3.forward * ((capsuleCollider.height - capsuleCollider.radius * 2) / 2); + } + + //manually reselect collider points in an order that maintains rotation for rotated capsule colliders + // again the name method is brittle, but seems the easiest way to identify rotated colliders without cluttering tags. + if (capsuleCollider.gameObject.name.Contains("Rotated")) + { + Transform t = capsuleCollider.transform; + worldVertices.Add(t.TransformPoint(top)); + worldVertices.Add(t.TransformPoint(bottom)); + worldVertices.Add(t.TransformPoint(capsuleCollider.center + Vector3.Cross(top, bottom).normalized * capsuleCollider.radius)); + } + + // Easiest to just add the full top and bottom spheres as the result is the same for any collider. + // as they contain the min and max values of collider, and the middle section is all on the same plane as the halfsphere's base for convex meshes. + // we could write a seperate method to only add the half-spheres, but there's no need. + // top sphere + AddWorldVertsSphere(capsuleCollider.transform, top, capsuleCollider.radius, worldVertices); + // bottom sphere + AddWorldVertsSphere(capsuleCollider.transform, bottom, capsuleCollider.radius, worldVertices); + } + + /// + /// Adds world space points around a sphere + /// + /// transform of the collider + /// center of the sphere + /// radius of the sphere + /// + private void AddWorldVertsSphere(Transform t, Vector3 center, float radius, List worldVertices) + { + int accuracy = ECEPreferences.MergeCollidersRoundnessAccuracy; + // 360 degrees in radians. + float sin, cos = sin = 0.0f; + for (int i = 1; i < accuracy; i++) + { + // center shifted to the z-axis + float h = (i / (float)accuracy) * radius * 2; + Vector3 centerX = center - (radius - (i / (float)accuracy) * radius * 2) * Vector3.right; + Vector3 centerY = center - (radius - (i / (float)accuracy) * radius * 2) * Vector3.up; + Vector3 centerZ = center - (radius - (i / (float)accuracy) * radius * 2) * Vector3.forward; + float newRadius = Mathf.Sqrt(radius * 2 * h - Mathf.Pow(h, 2)); + for (int j = 0; j <= accuracy; j++) + { + float angleStep = ((j / (float)accuracy) * 360f) * Mathf.Deg2Rad; + sin = Mathf.Sin(angleStep); + cos = Mathf.Cos(angleStep); + // constant z. + float xZ = centerZ.x + newRadius * sin; + float yZ = centerZ.y + newRadius * cos; + // constant x. + float yX = centerX.y + newRadius * sin; + float zX = centerX.z + newRadius * cos; + // constant y. + float zY = centerY.z + (newRadius * sin); + float xY = centerY.x + (newRadius * cos); + // + worldVertices.Add(t.TransformPoint(new Vector3(centerX.x, yX, zX))); + worldVertices.Add(t.TransformPoint(new Vector3(xY, centerY.y, zY))); + worldVertices.Add(t.TransformPoint(new Vector3(xZ, yZ, centerZ.z))); + } + } + } + + #endregion + +#endif + + #region ColliderDataCalculation + + /// + /// Calculates the best fit sphere for a series of points. Providing a larger list of points increases accuracy. + /// + /// Local space vertices + /// The best fit sphere + private BestFitSphere CalculateBestFitSphere(List localVertices) + { + // # of points. + int n = localVertices.Count; + // Calculate average x, y, and z value of vertices. + float xAvg, yAvg, zAvg = xAvg = yAvg = 0.0f; + foreach (Vector3 vertex in localVertices) + { + xAvg += vertex.x; + yAvg += vertex.y; + zAvg += vertex.z; + } + xAvg = xAvg * (1.0f / n); + yAvg = yAvg * (1.0f / n); + zAvg = zAvg * (1.0f / n); + // Do some fun math with matrices + // B Vector. + Vector3 B = Vector3.zero; + // Can use a 4x4 as a 3x3 with the 4x4 as 0,0,0,1 in the last row/column. + Matrix4x4 AM = Matrix4x4.zero; + AM.m33 = 1; + float x2, y2, z2 = x2 = y2 = 0.0f; + foreach (Vector3 vertex in localVertices) + { + AM[0, 0] += 2 * (vertex.x * (vertex.x - xAvg)) / n; + AM[0, 1] += 2 * (vertex.x * (vertex.y - yAvg)) / n; + AM[0, 2] += 2 * (vertex.x * (vertex.z - zAvg)) / n; + AM[1, 0] += 2 * (vertex.y * (vertex.x - xAvg)) / n; + AM[1, 1] += 2 * (vertex.y * (vertex.y - yAvg)) / n; + AM[1, 2] += 2 * (vertex.y * (vertex.z - zAvg)) / n; + AM[2, 0] += 2 * (vertex.z * (vertex.x - xAvg)) / n; + AM[2, 1] += 2 * (vertex.z * (vertex.y - yAvg)) / n; + AM[2, 2] += 2 * (vertex.z * (vertex.z - zAvg)) / n; + x2 = vertex.x * vertex.x; + y2 = vertex.y * vertex.y; + z2 = vertex.z * vertex.z; + B.x += ((x2 + y2 + z2) * (vertex.x - xAvg)) / n; + B.y += ((x2 + y2 + z2) * (vertex.y - yAvg)) / n; + B.z += ((x2 + y2 + z2) * (vertex.z - zAvg)) / n; + } + // Calculate the center of the best-fit sphere. + Vector3 center = (AM.transpose * AM).inverse * AM.transpose * B; + // Calculate radius. + float radius = 0.0f; + foreach (Vector3 vertex in localVertices) + { + radius += Mathf.Pow((vertex.x - center.x), 2) + Mathf.Pow(vertex.y - center.y, 2) + Mathf.Pow(vertex.z - center.z, 2); + } + radius = Mathf.Sqrt(radius / localVertices.Count); + BestFitSphere bfs = new BestFitSphere(center, radius); + return bfs; + } + + /// + /// Calculates a box's data from a list of world space vertices + /// + /// list of vertices in world space + /// transform the box will be attached to + /// are we creating a rotated box? + /// true when the attach to transform is not already correctly rotated for a rotated collider + /// adjusts the size of the box collider + /// Data appropriate variables set for a box collider + public BoxColliderData CalculateBox(List worldVertices, Transform attachTo, bool isRotated = false, bool requiresMatrixCalc = false, float shrinkGrow = 1f) + { + if (isRotated && worldVertices.Count < 3) + { + return new BoxColliderData(); + } + else if (worldVertices.Count < 2) + { + return new BoxColliderData(); + } + Quaternion q = Quaternion.identity; + Matrix4x4 m; + List localVertices = new List(); + if (isRotated && worldVertices.Count >= 3 && requiresMatrixCalc) + { + Vector3 forward = worldVertices[1] - worldVertices[0]; + Vector3 up = Vector3.Cross(forward, worldVertices[2] - worldVertices[1]); + q = Quaternion.LookRotation(forward, up); + // world space matrix, pos doesn't matter here in preview. + m = Matrix4x4.TRS(attachTo.position, q, attachTo.lossyScale); + Matrix4x4 mi = m.inverse; + // dont actually have the object in the preview. + for (int i = 0; i < worldVertices.Count; i++) + { + localVertices.Add(mi.MultiplyPoint3x4(worldVertices[i])); + } + } + else + { + localVertices = ToLocalVerts(attachTo, worldVertices); + m = attachTo.localToWorldMatrix; + } + BoxColliderData data = CalculateBoxLocal(localVertices); + data.ColliderType = isRotated ? CREATE_COLLIDER_TYPE.ROTATED_BOX : CREATE_COLLIDER_TYPE.BOX; + data.Matrix = m; +#if (UNITY_EDITOR) + shrinkGrow = ECEPreferences.ShrinkGrow; +#endif + data.Size *= shrinkGrow; + return data; + } + + /// + /// Calculates box collider data for a list of local space vertices + /// + /// list of local space vertices + /// box collider data with center and size set + public BoxColliderData CalculateBoxLocal(List vertices) + { + float xMin, yMin, zMin = xMin = yMin = Mathf.Infinity; + float xMax, yMax, zMax = xMax = yMax = -Mathf.Infinity; + foreach (Vector3 vertex in vertices) + { + //x min & max. + xMin = (vertex.x < xMin) ? vertex.x : xMin; + xMax = (vertex.x > xMax) ? vertex.x : xMax; + //y min & max + yMin = (vertex.y < yMin) ? vertex.y : yMin; + yMax = (vertex.y > yMax) ? vertex.y : yMax; + //z min & max + zMin = (vertex.z < zMin) ? vertex.z : zMin; + zMax = (vertex.z > zMax) ? vertex.z : zMax; + } + Vector3 max = new Vector3(xMax, yMax, zMax); + Vector3 min = new Vector3(xMin, yMin, zMin); + Vector3 size = max - min; + Vector3 center = (max + min) / 2; + // set data from calculated values + BoxColliderData data = new BoxColliderData(); + data.Center = center; + data.ColliderType = CREATE_COLLIDER_TYPE.BOX; + data.IsValid = true; + data.Size = size; + return data; + } + + /// + /// Calculates a capsule's data from the given values using the best fit method and a list of world space vertices + /// + /// list of vertices in world space + /// transform the capsule will be attached to + /// are we creating a rotated capsule? + /// true when the attach to transform is not already correctly rotated for a rotated collider + /// adjust the height/radius of the capsule collider + /// Data with appropriate variables set for a capsule collider + public CapsuleColliderData CalculateCapsuleBestFit(List worldVertices, Transform attachTo, bool isRotated, bool requiresMatrixCalc = false, float shrinkGrow = 1f) + { + if (worldVertices.Count >= 3) + { + Quaternion q = Quaternion.identity; + Matrix4x4 m; + List localVertices = new List(); + if (isRotated && requiresMatrixCalc) + { + // for rotated colliders we also re-calculate the to local even though the transform is changed. + // this better handles scale-shearing. + Vector3 forward = worldVertices[1] - worldVertices[0]; + Vector3 up = Vector3.Cross(forward, worldVertices[2] - worldVertices[1]); + q = Quaternion.LookRotation(forward, up); + m = Matrix4x4.TRS(attachTo.position, q, attachTo.lossyScale); + for (int i = 0; i < worldVertices.Count; i++) + { + localVertices.Add(m.inverse.MultiplyPoint3x4(worldVertices[i])); + } + } + else + { + localVertices = ToLocalVerts(attachTo, worldVertices); + m = attachTo.localToWorldMatrix; + } + CapsuleColliderData data = CalculateCapsuleBestFitLocal(localVertices); + data.ColliderType = isRotated ? CREATE_COLLIDER_TYPE.ROTATED_CAPSULE : CREATE_COLLIDER_TYPE.CAPSULE; + data.Matrix = m; +#if (UNITY_EDITOR) + shrinkGrow = ECEPreferences.ShrinkGrow; +#endif + data.Radius *= shrinkGrow; + data.Height *= shrinkGrow; + return data; + } + return new CapsuleColliderData(); + } + + /// + /// Calculates a best-fit capsule collider from a list of local space vertices + /// + /// local space vertices + /// Capsule collider data with center, direction and height + public CapsuleColliderData CalculateCapsuleBestFitLocal(List localVertices) + { + if (localVertices.Count < 3) + { + Debug.LogWarning("EasyColliderCreator: Too few vertices passed to calculate a best fit capsule collider."); + return new CapsuleColliderData(); + } + // height from first 2 verts selected. + Vector3 v0 = localVertices[0]; + Vector3 v1 = localVertices[1]; + float height = Vector3.Distance(v0, v1); + float dX = Mathf.Abs(v1.x - v0.x); + float dY = Mathf.Abs(v1.y - v0.y); + float dZ = Mathf.Abs(v1.z - v0.z); + localVertices.RemoveAt(1); + localVertices.RemoveAt(0); + BestFitSphere bfs = CalculateBestFitSphere(localVertices); + Vector3 center = bfs.Center; + int direction = 0; + if (dX > dY && dX > dZ) + { + direction = 0; + center.x = (v1.x + v0.x) / 2; + } + else if (dY > dX && dY > dZ) + { + direction = 1; + center.y = (v1.y + v0.y) / 2; + } + else + { + direction = 2; + center.z = (v1.z + v0.z) / 2; + } + CapsuleColliderData data = new CapsuleColliderData(); + data.Center = center; + data.ColliderType = CREATE_COLLIDER_TYPE.CAPSULE; + data.Direction = direction; + data.Height = height; + data.IsValid = true; + data.Radius = bfs.Radius; + return data; + } + + /// + /// Calculates a capsule's data from the given values using the min max method and a list of world space vertices + /// + /// list of vertices in world space + /// transform the capsule will be attached to + /// method we are using to create the capsule (ie MinMaxPlusRadius) + /// are we creating a rotated capsule? + /// true when the attach to transform is not already correctly rotated for a rotated collider + /// adjust the height/radius of the capsule collider + /// Data with appropriate variables set for a capsule collider + public CapsuleColliderData CalculateCapsuleMinMax(List worldVertices, Transform attachTo, CAPSULE_COLLIDER_METHOD method, bool isRotated, bool requiresMatrixCalc = false, float shrinkGrow = 1f) + { + if (isRotated && worldVertices.Count < 3) + { + return new CapsuleColliderData(); + } + else if (worldVertices.Count < 2) + { + return new CapsuleColliderData(); + } + List localVertices = new List(); + Matrix4x4 m; + Quaternion q; + if (isRotated && worldVertices.Count >= 3 && requiresMatrixCalc) + { + Vector3 forward = worldVertices[1] - worldVertices[0]; + Vector3 up = Vector3.Cross(forward, worldVertices[2] - worldVertices[1]); + q = Quaternion.LookRotation(forward, up); + m = Matrix4x4.TRS(attachTo.position, q, attachTo.lossyScale); + for (int i = 0; i < worldVertices.Count; i++) + { + localVertices.Add(m.inverse.MultiplyPoint3x4(worldVertices[i])); + } + } + else + { + localVertices = ToLocalVerts(attachTo.transform, worldVertices); + m = attachTo.localToWorldMatrix; + } + CapsuleColliderData data = CalculateCapsuleMinMaxLocal(localVertices, method); + data.ColliderType = isRotated ? CREATE_COLLIDER_TYPE.ROTATED_CAPSULE : CREATE_COLLIDER_TYPE.CAPSULE; + data.Matrix = m; +#if (UNITY_EDITOR) + shrinkGrow = ECEPreferences.ShrinkGrow; +#endif + data.Radius *= shrinkGrow; + data.Height *= shrinkGrow; + return data; + } + + /// + /// Calculates a capsule collider from a list of local space vertices + /// + /// List of local space vertices + /// method to use when calculating (used to add radius or diameter to height of capsule) + /// Capsule collider data with center, direction, and height + public CapsuleColliderData CalculateCapsuleMinMaxLocal(List localVertices, CAPSULE_COLLIDER_METHOD method) + { + // calculate min and max points from vertices. + Vector3 min = new Vector3(Mathf.Infinity, Mathf.Infinity, Mathf.Infinity); + Vector3 max = new Vector3(-Mathf.Infinity, -Mathf.Infinity, -Mathf.Infinity); + foreach (Vector3 vertex in localVertices) + { + // Calc minimums + min.x = vertex.x < min.x ? vertex.x : min.x; + min.y = vertex.y < min.y ? vertex.y : min.y; + min.z = vertex.z < min.z ? vertex.z : min.z; + // Calc maximums + max.x = vertex.x > max.x ? vertex.x : max.x; + max.y = vertex.y > max.y ? vertex.y : max.y; + max.z = vertex.z > max.z ? vertex.z : max.z; + } + // Deltas for max-min + float dX = max.x - min.x; + float dY = max.y - min.y; + float dZ = max.z - min.z; + // center is between min and max values. + Vector3 center = (max + min) / 2; + int direction = 0; + float height = 0; + // set direction and height. + if (dX > dY && dX > dZ) // direction is x + { + direction = 0; + // height is the max difference in x. + height = dX; + } + else if (dY > dX && dY > dZ) // direction is y + { + direction = 1; + height = dY; + } + else // direction is z. + { + direction = 2; + height = dZ; + } + // Calculate radius, makes sure that all vertices are within the radius. + // Esentially to points on plane defined by direction axis, and find the furthest distance. + float maxRadius = -Mathf.Infinity; + Vector3 current = Vector3.zero; + foreach (Vector3 vertex in localVertices) + { + current = vertex; + if (direction == 0) + { + current.x = center.x; + } + else if (direction == 1) + { + current.y = center.y; + } + else if (direction == 2) + { + current.z = center.z; + } + float d = Vector3.Distance(current, center); + if (d > maxRadius) + { + maxRadius = d; + } + } + // method add radius / diameter + if (method == CAPSULE_COLLIDER_METHOD.MinMaxPlusRadius) + { + height += maxRadius; + } + else if (method == CAPSULE_COLLIDER_METHOD.MinMaxPlusDiameter) + { + height += maxRadius * 2; + } + CapsuleColliderData data = new CapsuleColliderData(); + data.Center = center; + data.ColliderType = CREATE_COLLIDER_TYPE.CAPSULE; + data.Direction = direction; + data.Height = height; + data.IsValid = true; + data.Radius = maxRadius; + return data; + } + + + + //TODO: Do a local-only method for cylinders. + + /// + /// Calculates the data needed to create a cylinder shaped convex mesh collider using a list of world space vertices + /// + /// list of selected world space vertices + /// transform the collider will be attached to + /// number of sides on the cylinder + /// Data to create a a cylinder collider with type, convex mesh, validity, and matrix set + public MeshColliderData CalculateCylinderCollider(List worldVertices, Transform attachTo, int numberOfSides = 12, CYLINDER_ORIENTATION orientation = CYLINDER_ORIENTATION.Automatic, float cylinderOffset = 0.0f, float shrinkGrow = 1f) + { + MeshColliderData data = new MeshColliderData(); + // convert to local + List localVerts = ToLocalVerts(attachTo, worldVertices); +#if (UNITY_EDITOR) + List cylinderLocalPoints = CalculateCylinderPointsLocal(localVerts, attachTo, ECEPreferences.CylinderNumberOfSides, ECEPreferences.CylinderOrientation, ECEPreferences.CylinderRotationOffset); +#else + List cylinderLocalPoints = CalculateCylinderPointsLocal(localVerts, attachTo, numberOfSides, orientation, cylinderOffset); +#endif +#if (UNITY_EDITOR) + shrinkGrow = ECEPreferences.ShrinkGrow; +#endif + if (shrinkGrow != 1.0f) + { + AdjustVerticesForShrinkGrow(cylinderLocalPoints, shrinkGrow); + } + // build the mesh using quickhull and the cylinder points. + EasyColliderQuickHull qh = EasyColliderQuickHull.CalculateHull(cylinderLocalPoints); + data.ColliderType = CREATE_COLLIDER_TYPE.CONVEX_MESH; + data.ConvexMesh = qh.Result; + if (qh.Result != null) + { + data.IsValid = true; + } + data.Matrix = attachTo.transform.localToWorldMatrix; + return data; + } + + /// + /// Calculates the mesh collider data for a cylinder shaped convex mesh collider using a list of local space vertices + /// + /// list of local space vertices + /// number of sides on the cylinder + /// Automatic: Height is along largest axis. X,Y,Z orient height along X, Y, or Z respectively. + /// angle to offset cylinder rotation in degrees. + /// Mesh collider data with convex mesh set> + public MeshColliderData CalculateCylinderColliderLocal(List vertices, int numberOfSides = 12, CYLINDER_ORIENTATION orientation = CYLINDER_ORIENTATION.Automatic, float cylinderOffset = 0.0f) + { + MeshColliderData data = new MeshColliderData(); +#if (UNITY_EDITOR) + List cylinderLocalPoints = CalculateCylinderPointsLocal(vertices, null, ECEPreferences.CylinderNumberOfSides, ECEPreferences.CylinderOrientation, ECEPreferences.CylinderRotationOffset); +#else + List cylinderLocalPoints = CalculateCylinderPointsLocal(vertices, null, numberOfSides, orientation, cylinderOffset); +#endif + EasyColliderQuickHull qh = EasyColliderQuickHull.CalculateHull(cylinderLocalPoints); + data.ColliderType = CREATE_COLLIDER_TYPE.CONVEX_MESH; + data.ConvexMesh = qh.Result; + if (qh.Result != null) + { + data.IsValid = true; + } + data.Matrix = new Matrix4x4(); + return data; + } + + /// + /// Calculates mesh collider data for a list of local space vertices + /// + /// list of local space vertices + /// Mesh collider data with convex mesh set + public MeshColliderData CalculateMeshColliderQuickHullLocal(List localVertices) + { + MeshColliderData data = new MeshColliderData(); + EasyColliderQuickHull qh = EasyColliderQuickHull.CalculateHull(localVertices); + data.ConvexMesh = qh.Result; + if (qh.Result != null) + { + data.ColliderType = CREATE_COLLIDER_TYPE.CONVEX_MESH; + data.IsValid = true; + } + return data; + } + + /// + /// Calculates a sphere using the best fit method and a list of world space vertices + /// + /// list of vertex positions in world space + /// transform sphere would be attached to + /// adjusts the radius of the sphere collider + /// Data with appropriate variables set for a sphere collider + public SphereColliderData CalculateSphereBestFit(List worldVertices, Transform attachTo, float shrinkGrow = 1f) + { + if (worldVertices.Count < 2) + { + return new SphereColliderData(); + } + List localVertices = ToLocalVerts(attachTo, worldVertices); + // set data from values + SphereColliderData data = CalculateSphereBestFitLocal(localVertices); + data.Matrix = attachTo.localToWorldMatrix; +#if (UNITY_EDITOR) + shrinkGrow = ECEPreferences.ShrinkGrow; +#endif + data.Radius *= shrinkGrow; + return data; ; + } + + /// + /// Calculates a best fit sphere collider using a list of local space vertices + /// + /// list of local space vertices + /// Sphere collider data with center and radius set + public SphereColliderData CalculateSphereBestFitLocal(List localVertices) + { + BestFitSphere bfs = CalculateBestFitSphere(localVertices); + // set data from values + SphereColliderData data = new SphereColliderData(); + data.Center = bfs.Center; + data.ColliderType = CREATE_COLLIDER_TYPE.SPHERE; + data.IsValid = true; + data.Radius = bfs.Radius; + return data; + } + + + // distance sphere is editor-only for now + /// + /// Calculates a sphere using the distance method and a list of world space vertices + /// + /// list of vertex positions in world space + /// transform sphere would be attached to + /// adjusts the radius of the sphere collider + /// Data with appropriate variables set for a sphere collider + public SphereColliderData CalculateSphereDistance(List worldVertices, Transform attachTo, float shrinkGrow = 1f) + { + if (worldVertices.Count < 2) + { + return new SphereColliderData(); + } + List localVertices = ToLocalVerts(attachTo, worldVertices); + // set data from values + SphereColliderData data = CalculateSphereDistanceLocal(localVertices); + data.Matrix = attachTo.localToWorldMatrix; +#if (UNITY_EDITOR) + shrinkGrow = ECEPreferences.ShrinkGrow; +#endif + data.Radius *= shrinkGrow; + return data; + } + + /// + /// Calculates a sphere collider using a list of local space vertices + /// + /// list of local space vertices + /// Sphere collider data with center and radius + public SphereColliderData CalculateSphereDistanceLocal(List localVertices) + { + // if calculations take to long, it switches to a faster less accurate algorithm using the mean. + bool switchToFasterAlgorithm = false; +#if (UNITY_EDITOR) + double startTime = EditorApplication.timeSinceStartup; +#else + double startTime = Time.realtimeSinceStartup; +#endif + double maxTime = 0.1f; + Vector3 distanceVert1 = Vector3.zero; + Vector3 distanceVert2 = Vector3.zero; + float maxDistance = -Mathf.Infinity; + float distance = 0; + for (int i = 0; i < localVertices.Count; i++) + { + for (int j = i + 1; j < localVertices.Count; j++) + { + distance = Vector3.Distance(localVertices[i], localVertices[j]); + if (distance > maxDistance) + { + maxDistance = distance; + distanceVert1 = localVertices[i]; + distanceVert2 = localVertices[j]; + } + } +#if (UNITY_EDITOR) + if (EditorApplication.timeSinceStartup - startTime > maxTime) + { + switchToFasterAlgorithm = true; + break; + } +#else + if (Time.realtimeSinceStartup - startTime > maxTime) + { + switchToFasterAlgorithm = true; + break; + } +#endif + } + if (switchToFasterAlgorithm) + { + // use a significantly faster algorithm that is less accurate for a large # of points. + Vector3 mean = Vector3.zero; + foreach (Vector3 vertex in localVertices) + { + mean += vertex; + } + mean = mean / localVertices.Count; + foreach (Vector3 vertex in localVertices) + { + distance = Vector3.Distance(vertex, mean); + if (distance > maxDistance) + { + distanceVert1 = vertex; + maxDistance = distance; + } + } + maxDistance = -Mathf.Infinity; + foreach (Vector3 vertex in localVertices) + { + distance = Vector3.Distance(vertex, distanceVert1); + if (distance > maxDistance) + { + maxDistance = distance; + distanceVert2 = vertex; + } + } + } + // set data from values + SphereColliderData data = new SphereColliderData(); + data.Center = (distanceVert1 + distanceVert2) / 2; + data.ColliderType = CREATE_COLLIDER_TYPE.SPHERE; + data.IsValid = true; + data.Radius = maxDistance / 2; + return data; + } + + /// + /// Calculates a sphere using the min max method and a list of world space vertices + /// + /// list of vertex positions in world space + /// transform sphere would be attached to + /// adjusts the radius of the sphere collider + /// Data with appropriate variables set for a sphere collider + public SphereColliderData CalculateSphereMinMax(List worldVertices, Transform attachTo, float shrinkGrow = 1f) + { + if (worldVertices.Count < 2) + { + return new SphereColliderData(); + } + // use local space verts. + List localVertices = ToLocalVerts(attachTo, worldVertices); + SphereColliderData data = CalculateSphereMinMaxLocal(localVertices); + data.Matrix = attachTo.localToWorldMatrix; +#if (UNITY_EDITOR) + shrinkGrow = ECEPreferences.ShrinkGrow; +#endif + data.Radius *= shrinkGrow; + return data; + } + + /// + /// Calculates a sphere collider using a list of local space vertices + /// + /// local space vertices + /// Sphere collider data with center and radius set + public SphereColliderData CalculateSphereMinMaxLocal(List localVertices) + { + float xMin, yMin, zMin = xMin = yMin = Mathf.Infinity; + float xMax, yMax, zMax = xMax = yMax = -Mathf.Infinity; + for (int i = 0; i < localVertices.Count; i++) + { + //x min & max. + xMin = (localVertices[i].x < xMin) ? localVertices[i].x : xMin; + xMax = (localVertices[i].x > xMax) ? localVertices[i].x : xMax; + //y min & max + yMin = (localVertices[i].y < yMin) ? localVertices[i].y : yMin; + yMax = (localVertices[i].y > yMax) ? localVertices[i].y : yMax; + //z min & max + zMin = (localVertices[i].z < zMin) ? localVertices[i].z : zMin; + zMax = (localVertices[i].z > zMax) ? localVertices[i].z : zMax; + } + // calculate center + Vector3 center = (new Vector3(xMin, yMin, zMin) + new Vector3(xMax, yMax, zMax)) / 2; + // calculate radius to contain all points + float maxDistance = 0.0f; + float distance = 0.0f; + // this is what makes it slightly differnt than just converting a box into a sphere. + foreach (Vector3 vertex in localVertices) + { + distance = Vector3.Distance(vertex, center); + if (distance > maxDistance) + { + maxDistance = distance; + } + } + SphereColliderData data = new SphereColliderData(); + data.Center = center; + data.ColliderType = CREATE_COLLIDER_TYPE.SPHERE; + data.IsValid = true; + data.Radius = maxDistance; + return data; + } + + #endregion + + // editor only + #region CreateMeshColliders +#if (UNITY_EDITOR) + /// + /// Create and saves a mesh with a minimum number of triangles that includes all selected vertices. Editor only. + /// + /// Full path to save location including a base object name ie: "C:/UnityProjects/ProjectName/Assets/ConvexHulls/SaveNameBase" + /// Vertices to create the mesh with in world space + /// Gameobject the mesh will be attached to + /// shrinks/grows the resulting mesh collider, similar to scaling an object + /// The created mesh + public Mesh CreateMesh_Messy(List worldSpaceVertices, GameObject attachTo, GameObject selected, float shrinkGrow = 1.0f) + { + // use vertices to make a useable mesh that contains all the selected points. + // The mesh is only used to generate the convex hull + Mesh mesh = new Mesh(); + // get all vertices in world space and convert to local space. + List localVertices = worldSpaceVertices.Select(vertex => attachTo.transform.InverseTransformPoint(vertex)).ToList(); +#if(UNITY_EDITOR) + shrinkGrow = ECEPreferences.ShrinkGrow; +#endif + AdjustVerticesForShrinkGrow(localVertices, shrinkGrow); + while (localVertices.Count % 3 != 0) + { + localVertices.Add(localVertices[localVertices.Count % 3]); + } + // attempt to deal with degenerate triangles (so if user changes the mesh collider flags manually, no crashes will occur) + Vector3 p0, p1, p2 = p1 = p0 = Vector3.zero; + Vector3 s1, s2 = s1 = Vector3.zero; + List verts = new List(); + int index = localVertices.Count - 1; + while (index >= 0) // need to make sure we include the last vertex. + { + p0 = localVertices[index]; + p1 = localVertices[(index - 1 >= 0) ? index - 1 : localVertices.Count - 1]; + p2 = localVertices[(index - 2 >= 0) ? index - 2 : localVertices.Count - 2]; + s1 = (p0 - p1).normalized; + s2 = (p0 - p2).normalized; + int degenIndex = localVertices.Count; // so we can automatically re-use the last vertices if needed. + bool degenFixed = false; + while (s1 == s2 || -s1 == s2 || (s2 == Vector3.zero && s1 != Vector3.zero)) + { + degenFixed = true; + degenIndex--; + if (degenIndex < 0) + { + Debug.LogError("Easy Collider Editor: Unable to generate a valid mesh collider from the selected points. This happens when all points are in a straight line."); + return null; + } + p2 = localVertices[degenIndex]; + s2 = (p0 - p2).normalized; + } + // if we fixed a degenerate we still need to do the last vertex, so only move back 2 indexs' in that case. + index -= degenFixed ? 2 : 3; + verts.Add(p0); + verts.Add(p1); + verts.Add(p2); + } + + int[] triangles = new int[verts.Count]; + for (int i = 0; i < verts.Count; i++) + { + triangles[i] = i; + } + // mesh.vertices = vertices; + mesh.vertices = verts.ToArray(); + mesh.triangles = triangles; + // the mesh has to be saved somewhere so it can actually be used (although this is still just optional) + try + { + if (selected == null) + { + EasyColliderSaving.CreateAndSaveMeshAsset(mesh, attachTo); + } + else + { + EasyColliderSaving.CreateAndSaveMeshAsset(mesh, selected); + } + return mesh; + } + catch + { + Debug.LogError("EasyColliderEditor: Error saving mesh at path:" + EasyColliderSaving.GetValidConvexHullPath(attachTo)); + return null; + } + } + + /// + /// Calculates a convex hull for a list of world-space points for use by the previwer. + /// + /// world-space points to generate a hull on + /// transform the collider will be attached to, used to convert to local space + /// shrinks/grows the resulting mesh collider, similar to scaling an object + /// EasyColliderData with mesh, matrix, and validity + public MeshColliderData CalculateHullData(List points, Transform attachTo, float shrinkGrow = 1.0f) + { + if (points == null || points.Count < 4) + { + // can't calculate yet. + return new MeshColliderData(); + } +#if(UNITY_EDITOR) + shrinkGrow = ECEPreferences.ShrinkGrow; +#endif + AdjustVerticesForShrinkGrow(points, shrinkGrow); + return EasyColliderQuickHull.CalculateHullData(points, attachTo); + } + + /// + /// Creates and saves (if set in preferences) a convex mesh collider using QuickHull. Editor only. + /// + /// Local or world space vertices + /// Gameobject the collider will be attached to + /// are the vertices already in local space? + /// if in the editor, and not-null the output mesh will be saved in a location according to this and preferences, otherwise it will use the attach to. + /// shrinks/grows the resulting mesh collider, similar to scaling an object + /// + public Mesh CreateMesh_QuickHull(List vertices, GameObject attachTo, bool isLocal = false, GameObject selected = null, float shrinkGrow = 1.0f) + { + List localVerts = isLocal ? vertices : ToLocalVerts(attachTo.transform, vertices); +#if (UNITY_EDITOR) + shrinkGrow = ECEPreferences.ShrinkGrow; +#endif + AdjustVerticesForShrinkGrow(localVerts, shrinkGrow); + EasyColliderQuickHull qh = EasyColliderQuickHull.CalculateHull(localVerts); + if (ECEPreferences.SaveConvexHullAsAsset) + { + if (selected == null) + { + EasyColliderSaving.CreateAndSaveMeshAsset(qh.Result, attachTo); + } + else + { + EasyColliderSaving.CreateAndSaveMeshAsset(qh.Result, selected); + } + } + return qh.Result; + } +#endif + + /// + /// Creates a convex mesh collider component from the mesh using all cooking options, so mesh does not have to be "valid" + /// + /// Mesh to make a convex hull from + /// Gameobject the convex hull will be attached to + /// Parameters to set on created collider + public MeshCollider CreateConvexMeshCollider(Mesh mesh, GameObject attachToObject, EasyColliderProperties properties, bool postProcess = true) + { + // Create a mesh collider +#if (UNITY_EDITOR) + MeshCollider createdCollider = Undo.AddComponent(attachToObject); +#else + MeshCollider createdCollider = attachToObject.AddComponent(); +#endif + createdCollider.sharedMesh = mesh; + //enable all cooking options. +#if UNITY_2018_3_OR_NEWER + createdCollider.cookingOptions = ~MeshColliderCookingOptions.None; +#elif UNITY_2017_3_OR_NEWER + createdCollider.cookingOptions = ~MeshColliderCookingOptions.None; + // Auto inflate mesh to the minimum amount + createdCollider.skinWidth = 0.000001f; +#else + // for very old unity versions that didn't have cooking options. + createdCollider.inflateMesh = true; + createdCollider.skinWidth = 0.000001f; +#endif + // Would be nice if we could do a try/catch on the baking to only inflate if we have to, but that doesn't work. + createdCollider.convex = true; + PostColliderCreation(createdCollider, properties, postProcess); + //disable read/write to save memory based on user preferences. + //for limitations see bottom of: https://docs.unity3d.com/Manual/class-MeshCollider.html +#if (UNITY_EDITOR) + if (!ECEPreferences.ConvexMeshReadWriteEnabled) + { + mesh.UploadMeshData(true); + // fixes issue where mesh read/write was not correctly saving. + EditorUtility.SetDirty(mesh); + } +#endif + return createdCollider; + } + + + #endregion + + // creating colliders uses undos, the data itself can be used during runtime. + #region CreatePrimitiveColliders + + /// + /// Creates a Box collider + /// + /// data to create box from + /// properties to set on collider + /// Created collider + private BoxCollider CreateBoxCollider(BoxColliderData data, EasyColliderProperties properties, bool postProcess = true) + { +#if (UNITY_EDITOR) + BoxCollider boxCollider; + if (UndoEnabled) + { + boxCollider = Undo.AddComponent(properties.AttachTo); + } + else + { + boxCollider = properties.AttachTo.AddComponent(); + } +#else + BoxCollider boxCollider = properties.AttachTo.AddComponent(); +#endif + boxCollider.size = data.Size; + boxCollider.center = data.Center; + PostColliderCreation(boxCollider, properties, postProcess); + return boxCollider; + } + + + + /// + /// Creates a box collider using a list of world space vertices + /// + /// List of world space vertices + /// Properties of collider + /// The created box collider + public BoxCollider CreateBoxCollider(List vertices, EasyColliderProperties properties, bool isLocal = false) + { + if (vertices.Count >= 2) + { + BoxColliderData data; + if (properties.Orientation == COLLIDER_ORIENTATION.ROTATED) + { + if (vertices.Count >= 3) + { + GameObject obj = CreateGameObjectOrientation(vertices, properties.AttachTo, "Rotated Box Collider"); + // still want to recalculate using the transform matrix, as it better handles uneven scale / shearing across multiple children + if (obj != null) + { + obj.layer = properties.Layer; + properties.AttachTo = obj; + } + data = CalculateBox(vertices, properties.AttachTo.transform, true); + } + else + { + Debug.LogWarning("Easy Collider Editor: Creating a Rotated Box Collider requires at least 3 points to be selected."); + return null; + } + } + else + { + if (!isLocal) + { + data = CalculateBox(vertices, properties.AttachTo.transform); + } + else + { + data = CalculateBoxLocal(vertices); + } + } + return CreateBoxCollider(data, properties); + } + return null; + } + + /// + /// Creates a capsule collider (editor undoable) + /// + /// data to create capsule from + /// properties to set on collider + /// created capsule collider + private CapsuleCollider CreateCapsuleCollider(CapsuleColliderData data, EasyColliderProperties properties, bool postProcess = true) + { +#if (UNITY_EDITOR) + CapsuleCollider capsuleCollider; + if (UndoEnabled) + { + capsuleCollider = Undo.AddComponent(properties.AttachTo); + } + else + { + capsuleCollider = properties.AttachTo.AddComponent(); + } +#else + CapsuleCollider capsuleCollider = properties.AttachTo.AddComponent(); +#endif + capsuleCollider.direction = data.Direction; + capsuleCollider.height = data.Height; + capsuleCollider.center = data.Center; + capsuleCollider.radius = data.Radius; + // set properties + PostColliderCreation(capsuleCollider, properties); + return capsuleCollider; + } + + /// + /// Creates a capsule collider usintg the best fit algorithm and a list of world space vertices + /// + /// List of world vertices + /// Properties of collider + /// The created capsule collider + public CapsuleCollider CreateCapsuleCollider_BestFit(List worldVertices, EasyColliderProperties properties) + { + if (worldVertices.Count >= 3) + { + CapsuleColliderData data = new CapsuleColliderData(); + if (properties.Orientation == COLLIDER_ORIENTATION.ROTATED) + { + GameObject obj = CreateGameObjectOrientation(worldVertices, properties.AttachTo, "Rotated Capsule Collider"); + if (obj != null) + { + properties.AttachTo = obj; + obj.layer = properties.Layer; + } + data = CalculateCapsuleBestFit(worldVertices, properties.AttachTo.transform, true); + } + else + { + data = CalculateCapsuleBestFit(worldVertices, properties.AttachTo.transform, false); + } + return CreateCapsuleCollider(data, properties); + } + return null; + } + + /// + /// Creates a capsule collider using the Min-Max method and a list of world space vertices + /// + /// List of world space vertices + /// Properties to set on collider + /// Min-Max method to use to add radius' to height. + /// The created capsule collider + public CapsuleCollider CreateCapsuleCollider_MinMax(List worldVertices, EasyColliderProperties properties, CAPSULE_COLLIDER_METHOD method, bool isLocal = false) + { + CapsuleColliderData data; + if (properties.Orientation == COLLIDER_ORIENTATION.ROTATED && worldVertices.Count >= 3) + { + GameObject obj = CreateGameObjectOrientation(worldVertices, properties.AttachTo, "Rotated Capsule Collider"); + if (obj != null) + { + properties.AttachTo = obj; + obj.layer = properties.AttachTo.layer; + } + data = CalculateCapsuleMinMax(worldVertices, properties.AttachTo.transform, method, true); + } + else + { + if (!isLocal) + { + data = CalculateCapsuleMinMax(worldVertices, properties.AttachTo.transform, method, false); + } + else + { + data = CalculateCapsuleMinMaxLocal(worldVertices, method); + } + } + return CreateCapsuleCollider(data, properties); + } + + /// + /// Creates a sphere collider, editor undo-able + /// + /// data to create the sphere collider from + /// properties to set on the collider + /// the created sphere collider + private SphereCollider CreateSphereCollider(SphereColliderData data, EasyColliderProperties properties, bool postProcess = true) + { +#if (UNITY_EDITOR) + SphereCollider sphereCollider; + if (UndoEnabled) + { + sphereCollider = Undo.AddComponent(properties.AttachTo); + } + else + { + sphereCollider = properties.AttachTo.AddComponent(); + } +#else + SphereCollider sphereCollider = properties.AttachTo.AddComponent(); +#endif + sphereCollider.radius = data.Radius; + sphereCollider.center = data.Center; + PostColliderCreation(sphereCollider, properties, postProcess); + return sphereCollider; + } + + /// + /// Creates a sphere collider using the best fit sphere algorithm and a list of world space vertices + /// + /// List of world space vertices + /// Properties of collider + /// + public SphereCollider CreateSphereCollider_BestFit(List worldVertices, EasyColliderProperties properties) + { + if (worldVertices.Count >= 2) + { + // Convert to local space. + SphereColliderData data = CalculateSphereBestFit(worldVertices, properties.AttachTo.transform); + return CreateSphereCollider(data, properties); + } + return null; + } + + /// + /// Creates a Sphere Collider using the distance method and a list of world space vertices + /// + /// List of world space vertices + /// Properties of collider + /// + public SphereCollider CreateSphereCollider_Distance(List worldVertices, EasyColliderProperties properties) + { + if (worldVertices.Count >= 2) + { + SphereColliderData data = CalculateSphereDistance(worldVertices, properties.AttachTo.transform); + return CreateSphereCollider(data, properties); + } + return null; + } + + /// + /// Creates a sphere collider using the min max method and a list of world space vertices + /// + /// List of world space vertices + /// Properties of collider + /// + public SphereCollider CreateSphereCollider_MinMax(List worldVertices, EasyColliderProperties properties, bool isLocal = false) + { + if (worldVertices.Count >= 2) + { + if (!isLocal) + { + SphereColliderData data = CalculateSphereMinMax(worldVertices, properties.AttachTo.transform); + return CreateSphereCollider(data, properties); + } + else + { + SphereColliderData data = CalculateSphereMinMaxLocal(worldVertices); + return CreateSphereCollider(data, properties); + } + } + return null; + } + + #endregion + + #region PostCreationProcessing + + /// + /// can be used at runtime to set a specific custom post processor if desired, otherwise defaults to EasyColliderPostProccessor + /// + public static IEasyColliderPostProcessor EasyColliderPostProcessor; + + /// + /// Can add any custom processing to a manually created collider here. + /// Current this handles re-centering the pivot of colliders if specified in preferences. (IE: always make collider holders & pivot at center) + /// These should only be things that have show no visual effect on the collider preview. + /// IE: things like rotating and re-aligning a collider along the new axis'. Or recentering the position of a collider center to 0, and moving it's collider holder to compensate. + /// + /// + /// + public void PostColliderCreationProcess(Collider createdCollider, EasyColliderProperties properties) + { + // set to default if null + if (EasyColliderPostProcessor == null) { EasyColliderPostProcessor = new EasyColliderPostProccessor(); } + + if (createdCollider is BoxCollider) + { + // adjust pivot to the center of the collider if specified. + BoxCollider bc = (BoxCollider)createdCollider; +#if(UNITY_EDITOR) + if (ECEPreferences.RotatedColliderPivotAtCenter && properties.Orientation == COLLIDER_ORIENTATION.ROTATED) + { + bc.transform.position = bc.transform.TransformPoint(bc.center); + bc.center = Vector3.zero; + // bc.transform.localPosition = bc.center; + // bc.center = Vector3.zero; + } +#endif + // handle other box related things in future here. + if (EasyColliderPostProcessor != null) + { + EasyColliderPostProcessor.PostProcessCollider(bc, properties); + } + + } + else if (createdCollider is SphereCollider) + { + SphereCollider sc = (SphereCollider)createdCollider; +#if(UNITY_EDITOR) + if (ECEPreferences.RotatedColliderPivotAtCenter && properties.Orientation == COLLIDER_ORIENTATION.ROTATED) + { + sc.transform.position = sc.transform.TransformPoint(sc.center); + sc.center = Vector3.zero; + } +#endif + //handle other sphere collider things here. + if (EasyColliderPostProcessor != null) + { + EasyColliderPostProcessor.PostProcessCollider(sc, properties); + } + } + else if (createdCollider is CapsuleCollider) + { + CapsuleCollider cc = (CapsuleCollider)createdCollider; +#if (UNITY_EDITOR) + Transform t = cc.transform; + if (ECEPreferences.RotatedColliderPivotAtCenter && properties.Orientation == COLLIDER_ORIENTATION.ROTATED) + { + cc.transform.position = cc.transform.TransformPoint(cc.center); + cc.center = Vector3.zero; + } + if (ECEPreferences.CylinderAsCapsuleOrientation) + { + // aligning to cylinder axis as well. + int dir = cc.direction; + int prefDir = (int)ECEPreferences.CylinderOrientation - 1; + // not set to automatic alignment, so user wants a specific alignment (above would make it -1) + if (prefDir >= 0 && prefDir != dir) + { + Vector3 forward = Vector3.zero; + Vector3 up = Vector3.zero; + if (dir == 0) + { + // currently aligned with x-axis (right) + if (prefDir == 1) // up + { + forward = t.transform.up; + up = t.transform.right; + } + else if (prefDir == 2) // forward + { + forward = t.transform.right; + up = t.transform.forward; + } + } + else if (dir == 1) + { + // current aligned with y-axis (up) + if (prefDir == 0) // right + { + forward = t.transform.right; + up = t.transform.forward; + } + else if (prefDir == 2) // forward + { + forward = t.transform.up; + up = t.transform.right; + } + } + else if (dir == 2) + { + if (prefDir == 0) // right + { + forward = t.transform.right; + up = -t.transform.up; + } + else if (prefDir == 1) // up + { + forward = t.transform.up; + up = t.transform.forward; + } + } + + if (!cc.transform.name.Contains("Rotated Capsule Collider")) + { + // need to create a collider holder to align if it's not a rotated collider. + GameObject o = new GameObject("EasyColliderHolder"); + Undo.RegisterCreatedObjectUndo(o, "Create Collider Holder"); + o.transform.SetParent(cc.transform); + // move the capsule to the new object. + CapsuleCollider replacedCapsule = Undo.AddComponent(o); + replacedCapsule.center = cc.center; + replacedCapsule.radius = cc.radius; + replacedCapsule.direction = cc.direction; + replacedCapsule.height = cc.height; + GameObject.DestroyImmediate(cc); + // correct it's position. + cc = replacedCapsule; + cc.transform.localPosition = cc.center; + cc.center = Vector3.zero; + // correct it's rotation and direction to align. + cc.transform.rotation = Quaternion.LookRotation(forward, up); + cc.direction = prefDir; + } + else + { + if (!ECEPreferences.RotatedColliderPivotAtCenter) + { + // the pivot isn't at center, so we need to transform the point to world space, then back to local after rotating it's transform. + Vector3 wsCenter = cc.transform.TransformPoint(cc.center); + cc.transform.rotation = Quaternion.LookRotation(forward, up); + cc.center = cc.transform.InverseTransformPoint(wsCenter); + cc.direction = prefDir; + } + else + { + // the pivot is already at the center, so we're good, just align it. + cc.transform.rotation = Quaternion.LookRotation(forward, up); + cc.direction = prefDir; + } + } + } + } +#endif + if (EasyColliderPostProcessor != null) + { + EasyColliderPostProcessor.PostProcessCollider(cc, properties); + } + } + else if (createdCollider is MeshCollider) + { + MeshCollider mc = (MeshCollider)createdCollider; + if (EasyColliderPostProcessor != null) + { + EasyColliderPostProcessor.PostProcessCollider(mc, properties); + } + } + } + #endregion + + + #region OtherHelperMethods + + public List CalculateCylinderPointsLocal(List vertices, Transform attachTo, int numberOfSides, CYLINDER_ORIENTATION orientation, float cylinderOffset) + { + BoxColliderData data = CalculateBoxLocal(vertices); + // calculate height and direction based on the height. + // height is the max value between the box's directions. + float height = 0.0f; + // direction is the height's axis + int direction = 0; + if (orientation == CYLINDER_ORIENTATION.Automatic) + { + // calculate height as max of each axis, and direction as whichever value it is. + height = Mathf.Max(Mathf.Max(data.Size.x, data.Size.y), data.Size.z); + direction = (height == data.Size.x) ? 0 : (height == data.Size.y) ? 1 : 2; + } + else + { + height = data.Size[(int)orientation - 1]; // x=1, y=2, z=3 to their vector3 indexs. + direction = (int)orientation - 1; // direction is also the same + } + // calculate max distance to the center of the box. + float distance = 0.0f; + // max distance is the radius + float maxDistance = 0.0f; + Vector3 current = Vector3.zero; + + // calculate radius for the given dimensions by squasing along height axis and measuring distance. + foreach (Vector3 v in vertices) + { + current.x = (direction == 0) ? data.Center.x : v.x; + current.y = (direction == 1) ? data.Center.y : v.y; + current.z = (direction == 2) ? data.Center.z : v.z; + distance = Vector3.Distance(current, data.Center); + if (distance > maxDistance) maxDistance = distance; + } + + // half height for offset of points from center. + float halfHeight = height / 2; + // amount to increment the angle when adding points. + float angleIncrement = 360f / numberOfSides; + // top and bottom points to build a circle around. + Vector3 top, bottom = top = data.Center; + // adjust the top / bottom x or y or z depending on direction by the half height. + top.x = (direction == 0) ? top.x + halfHeight : top.x; + top.y = (direction == 1) ? top.y + halfHeight : top.y; + top.z = (direction == 2) ? top.z + halfHeight : top.z; + bottom.x = (direction == 0) ? bottom.x - halfHeight : bottom.x; + bottom.y = (direction == 1) ? bottom.y - halfHeight : bottom.y; + bottom.z = (direction == 2) ? bottom.z - halfHeight : bottom.z; + // list of points + List points = new List(); + for (float a = 0 + cylinderOffset; a < 360f + cylinderOffset; a += angleIncrement) + { + // calculate pair of offset to use for a circle + float b = maxDistance * Mathf.Sin(a * Mathf.Deg2Rad); + float c = maxDistance * Mathf.Cos(a * Mathf.Deg2Rad); + // only adjust the axis if it is not the height axis. + if (direction == 0) + { + top.y = b + data.Center.y; + top.z = c + data.Center.z; + bottom.y = b + data.Center.y; + bottom.z = c + data.Center.z; + } + else if (direction == 1) + { + top.x = b + data.Center.x; + top.z = c + data.Center.z; + bottom.x = b + data.Center.x; + bottom.z = c + data.Center.z; + } + else + { + top.y = b + data.Center.y; + top.x = c + data.Center.x; + bottom.y = b + data.Center.y; + bottom.x = c + data.Center.x; + } + points.Add(top); + points.Add(bottom); + } + return points; + } + + /// + /// Creates a gameobject attach to parent with it's local position at zero, and it's up direction oriented in the direction of the first 2 world vertices. + /// + /// List of world space vertices + /// Parent to attach gameobject to + /// Name of gameobject to create + /// + private GameObject CreateGameObjectOrientation(List worldVertices, GameObject parent, string name) + { + GameObject obj = new GameObject(name); + if (worldVertices.Count >= 3) + { + // calculate forward and up. + Vector3 forward = worldVertices[1] - worldVertices[0]; + Vector3 up = Vector3.Cross(forward, worldVertices[2] - worldVertices[1]); + obj.transform.rotation = Quaternion.LookRotation(forward, up); + obj.transform.SetParent(parent.transform); + obj.transform.localPosition = Vector3.zero; + // we definitely want the local scale of a rotated object to be uniform 1,1,1 + obj.transform.localScale = Vector3.one; +#if (UNITY_EDITOR) // Rotated collider pivot at center is editor-only, + if (ECEPreferences.RotatedColliderPivotAtCenter) + { + Vector3 min = new Vector3(Mathf.Infinity, Mathf.Infinity, Mathf.Infinity); + Vector3 max = new Vector3(-Mathf.Infinity, -Mathf.Infinity, -Mathf.Infinity); + foreach (Vector3 v in worldVertices) + { + Vector3 localV = obj.transform.InverseTransformPoint(v); + min.x = localV.x < min.x ? localV.x : min.x; + min.y = localV.y < min.y ? localV.y : min.y; + min.z = localV.z < min.z ? localV.z : min.z; + + max.x = localV.x > max.x ? localV.x : max.x; + max.y = localV.y > max.y ? localV.y : max.y; + max.z = localV.z > max.z ? localV.z : max.z; + } + Vector3 center = (max + min) / 2; + center = obj.transform.TransformPoint(center); + center = parent.transform.InverseTransformPoint(center); + obj.transform.localPosition = center; + } + Undo.RegisterCreatedObjectUndo(obj, "Create Rotated GameObject"); +#endif + return obj; + } + return null; + } + + + + + /// + /// Just a helper method to draw a point in world space + /// + /// + /// + private void DebugDrawPoint(Vector3 worldLoc, Color color, float dist = 0.01f) + { + Debug.DrawLine(worldLoc - Vector3.up * dist, worldLoc + Vector3.up * dist, color, 0.01f, false); + Debug.DrawLine(worldLoc - Vector3.left * dist, worldLoc + Vector3.left * dist, color, 0.01f, false); + Debug.DrawLine(worldLoc - Vector3.forward * dist, worldLoc + Vector3.forward * dist, color, 0.01f, false); + } + + /// + /// A method that can be ran on any collider for things like setting collider properties or post processing. + /// + /// Collider that was created + /// Properties object with the properties to set + private void PostColliderCreation(Collider collider, EasyColliderProperties properties, bool postProcess = true) + { + SetPropertiesOnCollider(collider, properties); + if (postProcess) + { + PostColliderCreationProcess(collider, properties); + } + } + + private void SetPropertiesOnCollider(Collider collider, EasyColliderProperties properties) + { + if (collider != null) + { + collider.isTrigger = properties.IsTrigger; + collider.sharedMaterial = properties.PhysicMaterial; + +#if (UNITY_2022_2_OR_NEWER) + collider.layerOverridePriority = properties.LayerOverridePriority; + collider.includeLayers = properties.IncludeLayers; + collider.excludeLayers = properties.ExcludeLayers; + collider.providesContacts = properties.ProvidesContacts; +#endif + } + } + + /// + /// Converts the list of world vertices to local positions + /// + /// Transform to use for local space + /// World space position of vertices + /// Localspace position w.r.t transform of worldVertices + private List ToLocalVerts(Transform transform, List worldVertices) + { + List localVerts = new List(worldVertices.Count); + foreach (Vector3 v in worldVertices) + { + localVerts.Add(transform.InverseTransformPoint(v)); + } + return localVerts; + } + + + /// + /// positions vertices closer to the center of all vertices based on the shrink grow value. Used for creating mesh colliders (including cylinder colliders) to essentially scale the output. + /// + /// + /// + void AdjustVerticesForShrinkGrow(List localVerts, float shrinkGrow) + { + // since this is for a mesh collider, we'll just calc the center and move the verts towards it by an equal distance. + // works for cylinder collider, adjusts it exactly like scaling the cylinder mesh type. + Vector3 center = Vector3.zero; + float count = localVerts.Count; + foreach (var v in localVerts) + { + center += v; + } + center /= count; + for (int i = 0; i < localVerts.Count; i++) + { + Vector3 fromCenter = localVerts[i] - center; + float magnitudeFromCenter = fromCenter.magnitude; + magnitudeFromCenter *= shrinkGrow; + Vector3 newPos = center + (fromCenter.normalized * magnitudeFromCenter); + localVerts[i] = newPos; + } + } + + #endregion + } +} diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderCreator.cs.meta b/Assets/EasyColliderEditor/Scripts/EasyColliderCreator.cs.meta new file mode 100644 index 00000000..138f5f32 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderCreator.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 241873a664ee81d418528c165ef0dd12 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/EasyColliderCreator.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderData.cs b/Assets/EasyColliderEditor/Scripts/EasyColliderData.cs new file mode 100644 index 00000000..fdc1baf2 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderData.cs @@ -0,0 +1,125 @@ +using UnityEngine; +namespace ECE +{ + /// + /// Data holder for collider calculations. + /// + public class EasyColliderData + { + /// + /// Type of collider data + /// + public CREATE_COLLIDER_TYPE ColliderType; + + /// + /// Did the collider calculation complete + /// + public bool IsValid = false; + + /// + /// TRS matrix of the attach to object, or TRS matrix of what the rotated collider will have + /// + public Matrix4x4 Matrix; + + public void Clone(EasyColliderData data) + { + this.ColliderType = data.ColliderType; + this.IsValid = data.IsValid; + this.Matrix = data.Matrix; + } + + } + + /// + /// Data for creating a sphere collider + /// + public class SphereColliderData : EasyColliderData + { + /// + /// Radius of the collider + /// + public float Radius; + + /// + /// Center of the collider + /// + public Vector3 Center; + + public void Clone(SphereColliderData data) + { + base.Clone(data); + this.Radius = data.Radius; + this.Center = data.Center; + } + } + + /// + /// Data for creating a capsule collider + /// + public class CapsuleColliderData : SphereColliderData + { + /// + /// Direction of the capsule collider + /// + public int Direction; + + /// + /// Height of the capsule collider + /// + public float Height; + + public void Clone(CapsuleColliderData data) + { + base.Clone(data); + this.Direction = data.Direction; + this.Height = data.Height; + } + } + + + /// + /// Data for creating a box collider + /// + public class BoxColliderData : EasyColliderData + { + /// + /// Center of the box collider + /// + public Vector3 Center; + + /// + /// Size of the box collider + /// + public Vector3 Size; + + public void Clone(BoxColliderData data) + { + base.Clone(data); + this.Center = data.Center; + this.Size = data.Size; + this.Matrix = data.Matrix; + } + + public override string ToString() + { + return "Rotated box collider. Center:" + Center.ToString() + " Size:" + Size.ToString(); + } + } + + /// + /// Data for creating a mesh collider + /// + public class MeshColliderData : EasyColliderData + { + /// + /// Mesh of the convex mesh collider + /// + public Mesh ConvexMesh; + + public void Clone(MeshColliderData data) + { + base.Clone(data); + this.ConvexMesh = data.ConvexMesh; + } + } +} diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderData.cs.meta b/Assets/EasyColliderEditor/Scripts/EasyColliderData.cs.meta new file mode 100644 index 00000000..5b239d98 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderData.cs.meta @@ -0,0 +1,20 @@ +fileFormatVersion: 2 +guid: f703485b98da54c489209f1fd7ad2db9 +timeCreated: 1593006697 +licenseType: Store +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/EasyColliderData.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderDraw.cs b/Assets/EasyColliderEditor/Scripts/EasyColliderDraw.cs new file mode 100644 index 00000000..98533bd1 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderDraw.cs @@ -0,0 +1,160 @@ +#if (UNITY_EDITOR) +using UnityEngine; +using UnityEditor; +namespace ECE +{ + public static class EasyColliderDraw + { + /// + /// Draws a box + /// + /// Transform of the box + /// Center of the box in local space + /// Half the size of the box + /// Color of lines used to draw the box + private static void DrawBox(Transform transform, Vector3 center, Vector3 halfSize, Color color) + { + Vector3 p1 = transform.TransformPoint(center + halfSize); + Vector3 p2 = transform.TransformPoint(center - halfSize); + Vector3 p3 = transform.TransformPoint(center + new Vector3(halfSize.x, halfSize.y, -halfSize.z)); + Vector3 p4 = transform.TransformPoint(center + new Vector3(halfSize.x, -halfSize.y, halfSize.z)); + Vector3 p5 = transform.TransformPoint(center + new Vector3(halfSize.x, -halfSize.y, -halfSize.z)); + Vector3 p6 = transform.TransformPoint(center + new Vector3(-halfSize.x, halfSize.y, halfSize.z)); + Vector3 p7 = transform.TransformPoint(center + new Vector3(-halfSize.x, -halfSize.y, halfSize.z)); + Vector3 p8 = transform.TransformPoint(center + new Vector3(-halfSize.x, halfSize.y, -halfSize.z)); + Handles.color = color; + Handles.DrawLine(p1, p3); + Handles.DrawLine(p1, p4); + Handles.DrawLine(p1, p6); + Handles.DrawLine(p8, p3); + Handles.DrawLine(p8, p6); + Handles.DrawLine(p8, p2); + Handles.DrawLine(p7, p6); + Handles.DrawLine(p7, p2); + Handles.DrawLine(p7, p4); + Handles.DrawLine(p5, p4); + Handles.DrawLine(p5, p2); + Handles.DrawLine(p5, p3); + } + + /// + /// Draws a box collider + /// + /// Box collider to draw + /// Color of lines used to draw + private static void DrawBoxCollider(BoxCollider boxCollider, Color color) + { + DrawBox(boxCollider.transform, boxCollider.center, boxCollider.size / 2, color); + } + + private static EasyColliderPreviewer previewer; + public static EasyColliderPreviewer Previewer + { + get + { + if (previewer == null) + { + previewer = ScriptableObject.CreateInstance(); + } + return previewer; + } + } + + /// + /// Draws a capsule collider + /// + /// Capsule collider to draw + /// Color of lines to draw + private static void DrawCapsuleCollider(CapsuleCollider capsuleCollider, Color color) + { + CapsuleColliderData data = new CapsuleColliderData(); + data.ColliderType = CREATE_COLLIDER_TYPE.CAPSULE; + data.Height = capsuleCollider.height; + data.Radius = capsuleCollider.radius; + data.Center = capsuleCollider.center; + data.Direction = capsuleCollider.direction; + data.Matrix = capsuleCollider.transform.localToWorldMatrix; + Previewer.DrawCapsuleCollider(data, color); + return; + } + + /// + /// Draws a primitive collider (box, sphere, capsule) using lines. + /// + /// Collider to draw + /// Color of lines to draw with + public static void DrawCollider(Collider collider, Color color) + { + if (collider == null) return; + if (collider is BoxCollider) + { + DrawBoxCollider(collider as BoxCollider, color); + } + else if (collider is SphereCollider) + { + DrawSphereCollider(collider as SphereCollider, color); + } + else if (collider is CapsuleCollider) + { + DrawCapsuleCollider(collider as CapsuleCollider, color); + } + else if (collider is MeshCollider) + { + DrawMeshCollider(collider as MeshCollider, color); + } + } + + + /// + /// Shader used to draw mesh colliders. + /// + static Shader MeshColliderShader; + + /// + /// Draws a mesh collider by drawing lines connecting it's meshs vertices. + /// + /// Mesh Collider + /// Color to draw lines with + private static void DrawMeshCollider(MeshCollider collider, Color color) + { + // try to find mesh collider. + if (MeshColliderShader == null) + { + MeshColliderShader = Shader.Find("Custom/EasyColliderMeshColliderPreview"); + } + // if we have the shader, draw it using the wireframe and the color + if (MeshColliderShader != null && collider != null && collider.sharedMesh != null) + { + Material wireMat = new Material(MeshColliderShader); + wireMat.SetColor("_Color", color); + wireMat.SetPass(0); + GL.wireframe = true; + Graphics.DrawMeshNow(collider.sharedMesh, collider.transform.localToWorldMatrix); + GL.wireframe = false; + } + else + { + // no shader? fall back to old draw box method. + DrawBox(collider.transform, collider.transform.InverseTransformPoint(collider.bounds.center), collider.transform.InverseTransformVector(collider.bounds.extents), color); + } + } + + + // Draws a sphere collider, taken from previous version + /// + /// Draws a sphere collider. + /// + /// Sphere collider to draw + /// Color of lines used to draw + private static void DrawSphereCollider(SphereCollider sphereCollider, Color color) + { + SphereColliderData data = new SphereColliderData(); + data.ColliderType = CREATE_COLLIDER_TYPE.SPHERE; + data.Center = sphereCollider.center; + data.Radius = sphereCollider.radius; + data.Matrix = sphereCollider.transform.localToWorldMatrix; + Previewer.DrawSphereCollider(data, color); + } + } +} +#endif \ No newline at end of file diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderDraw.cs.meta b/Assets/EasyColliderEditor/Scripts/EasyColliderDraw.cs.meta new file mode 100644 index 00000000..9a712102 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderDraw.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 3c95b61c7ebb7b14e9688b95552cef80 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/EasyColliderDraw.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderEditor.cs b/Assets/EasyColliderEditor/Scripts/EasyColliderEditor.cs new file mode 100644 index 00000000..bf9490b8 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderEditor.cs @@ -0,0 +1,3112 @@ +#if (UNITY_EDITOR) +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +namespace ECE +{ + [System.Serializable] + public class EasyColliderEditor : ScriptableObject, ISerializationCallbackReceiver + { + #region VHACD + // VHACD section + /// + /// An instance of VHACD for use in each step when using RunVHACDStep + /// + private EasyColliderVHACD _VHACD; + + /// + /// Array of meshes created from VHACDRunStep + /// + private Mesh[] _VHACDMeshes; + + /// + /// Maximum number of vhacd recalculations + /// + private int _VHACDMaxCalculations = 3; + + /// + /// Current vhacd calculation + /// + private int _VHACDCurrentCalculation = 0; + + /// + /// List of colliders created by vhacd + /// + public List VHACDCreatedColliders = new List(); + + public List _VHACDConvertedData = new List(); + + /// + /// Checks if the computation is finished and valid. + /// + /// If force under 256 triangles is enabled, if the computation is finished but not valid, + /// will start a recomputation with a lower vertex limit. + /// Only recomputes a certain # of times before logging a warning and finishing. + /// + /// If it's not enabled, just checks if the computation of convex hull data is complete + /// + /// Parameters of current vhacd calculation + /// True if complete or complete and valid, false otherwise. + public bool VHACDCheckCompute(VHACDParameters parameters) + { + // check to see if we're forcing under 256 triangles and can still recalculate. + if (parameters.forceUnder256Triangles && _VHACDCurrentCalculation < _VHACDMaxCalculations) + { + // if the computation is finished + if (_VHACD.IsComputeFinished()) + { + // and valid + if (_VHACD.IsValid()) + { + // were done. + return true; + } + else + { + // recompute the colliders (this changes the max number of vertices per hull so we get under 256) + _VHACD.RecomputeVHACD(); + // increase the current calculation count. + _VHACDCurrentCalculation += 1; + return false; + } + } + // compute isn't finished. + else { return false; } + } + else + { + // if it's finished and we reached calculation max, tell user to reduce number of vertices per CH. + if (_VHACD.IsComputeFinished() + && _VHACDCurrentCalculation == _VHACDMaxCalculations + && _VHACD.IsValid() == false) + { + Debug.LogWarning("EasyColliderEditor: VHACD computation completed, but the final result had a number of triangles > 255. Try decreasing max vertices per hull, or increasing the number of hulls to prevent these errors."); + } + // return if the compute is finished. + return _VHACD.IsComputeFinished(); + } + } + + /// + /// Checks if VHACD is null + /// + /// false if null + public bool VHACDExists() + { + if (_VHACD == null) { return false; } + return true; + } + + private Dictionary _VHACDPreviewResult = new Dictionary(); + public Dictionary VHACDGetPreview() + { + if (_VHACDPreviewResult != null && _VHACDPreviewResult.Count > 0) + { + return _VHACDPreviewResult; + } + return null; + } + + public void VHACDClearPreviewResult() + { + _VHACDPreviewResult = new Dictionary(); + _VHACDConvertedData = new List(); + } + + /// + /// Runs VHACD step by step + /// + /// Current step to run, 0-5 + /// Parameters to use + /// Save path for meshes + /// Gameobject to attach mesh collider's to. + /// true if step is valid and completes, false otherwise + public bool VHACDRunStep(int step, VHACDParameters parameters, bool saveAsAsset) + { + switch (step) + { + case 0: // setup + // clean before another run which re-initializes itself. + // fix for an issue with the newer version of vhacd (although it's not used yet) + if (_VHACD != null) + { + if (!_VHACD.IsComputeFinished()) + { + return false; + } + _VHACD.Clean(); + } + _VHACD = new EasyColliderVHACD(); + _VHACDCurrentCalculation = 0; + _VHACD.Init(true); + VHACDCreatedColliders = new List(); + if (parameters.SeparateChildMeshes && parameters.UseSelectedVertices) + { // occasionally this can happen somehow even though it shouldn't, this should treat the issue but not the cause. + parameters.SeparateChildMeshes = false; + parameters.UseSelectedVertices = true; + } + return _VHACD.SetParameters(parameters); + case 1: // prepare mesh data. + // if we're not using just the selected vertices, ie we are using the whole mesh at once. + if (!parameters.UseSelectedVertices) + { + // for seperate child meshes, we attach each result to each mesh. + // this allows one to run it on multiple meshes all with the same parameters using a common (or temporary) parent object. + if (parameters.SeparateChildMeshes && parameters.MeshFilters[parameters.CurrentMeshFilter] != null) + { + + //TODO: figure out a smart wayt hat communicates that attach to objects are used with separate child meshes + // child object as the attach to with separate child meshes? as that functionality is then the same as the normal child object one? + // \nChild Object: Attach colliders to a child of Attach To field.\nIndividual Child Objects: Each collider is attached to its own child whos parent is a child of the Attach To field. + // so since attach to says it attachs to the thing int he attach to field, we should probably change the behaviour on the IndividualChildObjects thing. + + // if (parameters.vhacdResultMethod == VHACD_RESULT_METHOD.AttachTo && parameters.SeparateChildMeshes && !parameters.PerMeshAttachOverride) + // { + + // default to using the attach to object, only if it's null so we don't override anything that was already set through previous separated child meshes. + if (parameters.AttachTo == null) + { + parameters.AttachTo = AttachToObject; + } + // if we're overriding to per mesh, use the mesh filters gameobject as the base attach to object. + if (parameters.PerMeshAttachOverride) + { + parameters.AttachTo = parameters.MeshFilters[parameters.CurrentMeshFilter].gameObject; + } + + // it's a temporary added component. + if (AddedInstanceIDs.Contains(GetIDFor(parameters.AttachTo))) + { + if (parameters.AttachTo.transform.parent != null) + { + // update attach to so we dont lose created colliders, and adjust save name. + parameters.AttachTo = parameters.AttachTo.transform.parent.gameObject; + if (!parameters.IsCalculationForPreview) + { + parameters.SavePath = parameters.SavePath.Remove(parameters.SavePath.LastIndexOf("/") + 1) + parameters.AttachTo.name; + } + } + } + } + // if we're including child meshes, we need to prepare them differently, + // all the child meshes essentially get merged into 1 mesh and sent into VHACD. + if (IncludeChildMeshes && !parameters.SeparateChildMeshes) + { + // mesh filters need to be passed as the vertices need to be transformed to attach to's local space. + return _VHACD.PrepareMeshData(parameters.MeshFilters, parameters.AttachTo.transform); + } + else + { + // a single mesh, or each individual seperated child mesh gets prepared. + // attach to the attach to object. + if (parameters.MeshFilters[parameters.CurrentMeshFilter] != null) + { + bool prepared = _VHACD.PrepareMeshData(parameters.MeshFilters[parameters.CurrentMeshFilter], parameters.AttachTo.transform, parameters.MeshFilters[parameters.CurrentMeshFilter].sharedMesh); + if (!prepared) + { + Debug.LogWarning("EasyColliderEditor: Unable to run VHACD on: " + parameters.MeshFilters[parameters.CurrentMeshFilter].name + ". Likely due to missing a mesh in the mesh filter.", parameters.MeshFilters[parameters.CurrentMeshFilter].gameObject); + } + } + return true; + } + } + else // use selected verts. + { + // Use selected verts is enabled, so we need to create a mesh from the selected vertices. + Mesh m = CreateVHACDSelectedVerticesPreviewMesh(parameters); + // prepare the mesh + return _VHACD.PrepareMeshData(parameters.MeshFilters[parameters.CurrentMeshFilter], parameters.AttachTo.transform, m); + } + case 2: // calculate (run VHACD on the prepared-mesh) + if (_VHACDCurrentCalculation == 0) + { + _VHACD.RunVHACD(); + _VHACDCurrentCalculation = 1; + return false; + } + else if (_VHACD.IsComputeFinished()) // check if compute is finished + { + // we recalculate if necessary, otherwise calculation is done. + return VHACDCheckCompute(parameters); + } + return false; + case 3: // save meshes as assets if needed / get the data for each mesh from VHACD and build a mesh. + if (!parameters.IsCalculationForPreview) + { + _VHACDMeshes = _VHACD.CreateConvexHullMeshes(); + if (saveAsAsset && parameters.ConvertTo == VHACD_CONVERSION.None) + { + _VHACDMeshes = EasyColliderSaving.CreateAndSaveMeshAssets(_VHACDMeshes, parameters.SavePath, parameters.SaveSuffix); + } + } + else if (parameters.IsCalculationForPreview) + { + // for the preview. + _VHACDMeshes = _VHACD.CreateConvexHullMeshes(); + if (parameters.ConvertTo == VHACD_CONVERSION.None) + { + if (_VHACDPreviewResult.ContainsKey(parameters.AttachTo.transform)) + { + _VHACDPreviewResult[parameters.AttachTo.transform] = _VHACDPreviewResult[parameters.AttachTo.transform].Concat(_VHACDMeshes).ToArray(); + } + else + { + _VHACDPreviewResult.Add(parameters.AttachTo.transform, _VHACDMeshes); + } + } + else + { + EasyColliderCreator convert = new EasyColliderCreator(); + foreach (Mesh m in _VHACDMeshes) + { + EasyColliderData ecd = null; + if (parameters.ConvertTo == VHACD_CONVERSION.Boxes) + { + ecd = convert.CalculateBoxLocal(m.vertices.ToList()); + } + else if (parameters.ConvertTo == VHACD_CONVERSION.Spheres) + { + ecd = convert.CalculateSphereMinMaxLocal(m.vertices.ToList()); + } + else if (parameters.ConvertTo == VHACD_CONVERSION.Capsules) + { + ecd = convert.CalculateCapsuleMinMaxLocal(m.vertices.ToList(), CAPSULE_COLLIDER_METHOD.MinMax); + } + ecd.Matrix = parameters.AttachTo.transform.localToWorldMatrix; + _VHACDConvertedData.Add(ecd); + } + } + } + return true; + case 4: // use the data from VHACD to generate convex mesh colliders. + if (parameters.IsCalculationForPreview) { return true; } // skip step 4 for previews. + if (parameters.vhacdResultMethod != VHACD_RESULT_METHOD.AttachTo) + { + // prevents non-per mesh override duplication of "VHACDColliders" when using separate child meshes + // otherwise each mesh would create another "VHACDColliders" as a child of the original base VHACDColliders object. + if (parameters.AttachTo == null || (parameters.AttachTo != null && !parameters.AttachTo.name.Contains("VHACDColliders"))) + { + // if the method isn't the default attach to, we create a parent to hold colliders + GameObject parent = new GameObject("VHACDColliders"); + // since all verts were coverted to the attach to's location/position/rotation we use it's settings. + parent.transform.parent = parameters.AttachTo.transform; + parent.transform.position = parameters.AttachTo.transform.position; + parent.transform.rotation = parameters.AttachTo.transform.rotation; + parent.transform.localScale = Vector3.one; + if (ECEPreferences.CopyParentObjectLayer) + { + parent.layer = parameters.AttachTo.layer; + } + else + { + parent.layer = GameObjectLayerOverride; + } + Undo.RegisterCreatedObjectUndo(parent, "Create VHACD Collider Holder"); + parameters.AttachTo = parent; + } + } + // keep the attach to object in case we need to create children. + GameObject attachTo = parameters.AttachTo; + EasyColliderCreator ecc = new EasyColliderCreator(); + for (int i = 0; i < _VHACDMeshes.Length; i++) + { + // for individual child objects. + if (parameters.vhacdResultMethod == VHACD_RESULT_METHOD.IndividualChildObjects) + { + // we create a child at the same position and rotation as the common parent (the attachto object) + GameObject child = new GameObject("VHACDCollider"); + child.transform.parent = parameters.AttachTo.transform; + child.transform.position = parameters.AttachTo.transform.position; + child.transform.rotation = parameters.AttachTo.transform.rotation; + child.transform.localScale = Vector3.one; + if (ECEPreferences.CopyParentObjectLayer) + { + child.layer = parameters.AttachTo.layer; + } + else + { + child.layer = GameObjectLayerOverride; + } + attachTo = child; + Undo.RegisterCreatedObjectUndo(child, "Create VHACD Collider"); + } + if (parameters.ConvertTo == VHACD_CONVERSION.None) + { + // create a convex mesh collider. + MeshCollider mc = ecc.CreateConvexMeshCollider(_VHACDMeshes[i], attachTo, GetProperties()); + CreatedColliders.Add(mc); + VHACDCreatedColliders.Add(mc); + AddedColliderIDs.Add(GetIDFor(mc)); + } + else if (parameters.ConvertTo == VHACD_CONVERSION.Boxes) + { + EasyColliderProperties p = GetProperties(); + p.AttachTo = attachTo; + BoxCollider bc = ecc.CreateBoxCollider(_VHACDMeshes[i].vertices.ToList(), p, true); + CreatedColliders.Add(bc); + AddedColliderIDs.Add(GetIDFor(bc)); + } + else if (parameters.ConvertTo == VHACD_CONVERSION.Spheres) + { + EasyColliderProperties p = GetProperties(); + p.AttachTo = attachTo; + SphereCollider sc = ecc.CreateSphereCollider_MinMax(_VHACDMeshes[i].vertices.ToList(), p, true); + CreatedColliders.Add(sc); + AddedColliderIDs.Add(GetIDFor(sc)); + } + else if (parameters.ConvertTo == VHACD_CONVERSION.Capsules) + { + EasyColliderProperties p = GetProperties(); + p.AttachTo = attachTo; + CapsuleCollider cc = ecc.CreateCapsuleCollider_MinMax(_VHACDMeshes[i].vertices.ToList(), p, CAPSULE_COLLIDER_METHOD.MinMax, true); + CreatedColliders.Add(cc); + AddedColliderIDs.Add(GetIDFor(cc)); + } + } + return true; + case 5: // clean up + if (parameters.IsCalculationForPreview) { return true; } // skip step 5 for previews. + if (parameters.UseSelectedVertices) + { + Undo.RecordObject(this, "Run VHACD"); + ClearSelectedVertices(); + } + // compute buffer gets all points set to origin when VHACD is run without use only selected vertices. So let's reupdate it. + if (Compute != null) + { + Compute.UpdateSelectedBuffer(GetWorldVertices()); + } + _VHACD.Clean(); + return true; + } + return false; + } + + /// + /// Creates a mesh from the current VHACD preview using the "use selected vertices" method + /// + /// Current VHACD parameters + /// Mesh created from the current VHACD preview using full-triangles, and adding remaining vertices by closest distance + private Mesh CreateVHACDSelectedVerticesPreviewMesh(VHACDParameters parameters) + { + // list of created meshes, 1 for each mesh filter. + List createdMeshList = new List(); + // arrays to hold vertices of triangles of each mesh filter, and transform for each mesh filter. + Vector3[] vertices = new Vector3[0]; + int[] triangles = new int[0]; + Vector3[] normals = new Vector3[0]; + Transform t = null; + // variables to hold easy collider vertices for each triangle of the mesh + EasyColliderVertex ecv0, ecv1, ecv2 = ecv1 = ecv0 = null; + foreach (MeshFilter mf in MeshFilters) + { + if (mf == null) continue; + // hashset of used vertices for use after full triangle pass. + HashSet usedVerticesSet = new HashSet(); + // dictionary of vertex : vertex index to update. + Dictionary ecvVertIndexDictionary = new Dictionary(); + // calculated mesh vertices and triangles to create a mesh with. + List verticesList = new List(); + List trianglesList = new List(); + // transform, verts, and tris of current mesh filter. + t = mf.transform; + vertices = mf.sharedMesh.vertices; + triangles = mf.sharedMesh.triangles; + normals = mf.sharedMesh.normals; + // keep track of how many verts have been added just to make it a little easier. + int vertexCount = 0; + // go through triangles to see if the whole triangle is selected. + for (int i = 0; i < triangles.Length; i += 3) + { + // vertices of the triangle. + ecv0 = new EasyColliderVertex(t, vertices[triangles[i]]); + ecv1 = new EasyColliderVertex(t, vertices[triangles[i + 1]]); + ecv2 = new EasyColliderVertex(t, vertices[triangles[i + 2]]); + // if the full triangle is selected, add it. + if (SelectedVerticesSet.Contains(ecv0) && SelectedVerticesSet.Contains(ecv1) && SelectedVerticesSet.Contains(ecv2)) + { + // Debug.Log("Full triangle."); + // add verts in world-space to convert later. + Vector3 v0 = vertices[triangles[i]]; + Vector3 v1 = vertices[triangles[i + 1]]; + Vector3 v2 = vertices[triangles[i + 2]]; + // if it's been used before. + if (ecvVertIndexDictionary.ContainsKey(ecv0)) + { + // Debug.Log("Vertex already in dictionary"); + // it's in the dictionary, so add the value in the dict to the triangle list + trianglesList.Add(ecvVertIndexDictionary[ecv0]); + } + else + { + // we haven't used this vertex yet, so add it + verticesList.Add(v0); + vertexCount++; + trianglesList.Add(vertexCount - 1); + // remember to add the vertex to the dictionary with the appropriate index value. + ecvVertIndexDictionary.Add(ecv0, vertexCount - 1); + // and add them to the used vertices set + usedVerticesSet.Add(ecv0); + } + // ecv1 - repeat above. + if (ecvVertIndexDictionary.ContainsKey(ecv1)) + { + trianglesList.Add(ecvVertIndexDictionary[ecv1]); + } + else + { + verticesList.Add(v1); + vertexCount++; + trianglesList.Add(vertexCount - 1); + ecvVertIndexDictionary.Add(ecv1, vertexCount - 1); + usedVerticesSet.Add(ecv1); + } + // ecv2 - repeat above + if (ecvVertIndexDictionary.ContainsKey(ecv2)) + { + trianglesList.Add(ecvVertIndexDictionary[ecv2]); + } + else + { + verticesList.Add(v2); + vertexCount++; + trianglesList.Add(vertexCount - 1); + ecvVertIndexDictionary.Add(ecv2, vertexCount - 1); + usedVerticesSet.Add(ecv2); + } + } + } + + // hashset of selected vertices. + HashSet selectedVerticesSet = new HashSet(SelectedVerticesSet); + // int count = selectedVerticesSet.Count; + // remove unused vertices that are full triangles. + selectedVerticesSet.ExceptWith(usedVerticesSet); + // Debug.Log("Remaining vertices count:" + selectedVerticesSet.Count + " Total At Start:" + count); + // list of reamining verts (vertices that are selected that arent' a part of at least 1 full triangle.) + List remainingVertsLocal = new List(); + // transform remaining verts to world-space. + foreach (EasyColliderVertex ecv in selectedVerticesSet) + { + // make sure the transform is the same as the current mesh filter. + if (ecv.T == t) + { + remainingVertsLocal.Add(ecv.LocalPosition); + } + } + // here we are checking to see if there was at least 1 full triangle to build from, if there wasn't then we make one. + // need at least 1 triangle to build from, but need at least 3 verts to do so. + if (trianglesList.Count < 3 && remainingVertsLocal.Count >= 3) + { + // find closest 3 in-order points (faster.. less accurate) + float totalMinDistance = Mathf.Infinity; + int[] vertIndexs = new int[3]; + for (int i = 0; i < remainingVertsLocal.Count - 3; i++) + { + // d0 -> d1 + float d01 = Vector3.Distance(remainingVertsLocal[i], remainingVertsLocal[i + 1]); + // d1 -> d2 + float d12 = Vector3.Distance(remainingVertsLocal[i + 1], remainingVertsLocal[i + 2]); + // d2 -> d0 + float d20 = Vector3.Distance(remainingVertsLocal[i + 2], remainingVertsLocal[i]); + // new "smallest" in-order triangle + if (d01 + d12 + d20 < totalMinDistance) + { + totalMinDistance = d01 + d12 + d20; + vertIndexs[0] = i; + vertIndexs[1] = i + 1; + vertIndexs[2] = i + 2; + } + } + // add the vertices + verticesList.Add(remainingVertsLocal[vertIndexs[0]]); + verticesList.Add(remainingVertsLocal[vertIndexs[1]]); + verticesList.Add(remainingVertsLocal[vertIndexs[2]]); + // add the triangle + trianglesList.Add(verticesList.Count - 3); + trianglesList.Add(verticesList.Count - 2); + trianglesList.Add(verticesList.Count - 1); + } + // add the remaining left-over non-full triangle vertices. + float minDistance = Mathf.Infinity; + int vertIndex0, vertIndex1 = vertIndex0 = -1; + + foreach (Vector3 pos in remainingVertsLocal) + { + // find "closest" triangle edge quickly. + minDistance = Mathf.Infinity; + for (int i = 0; i < trianglesList.Count; i += 3) + { + // calc distance from vertex to each triangle point. + float d0, d1, d2 = d1 = d0 = 0; + d0 = Vector3.Distance(pos, verticesList[trianglesList[i]]); + d1 = Vector3.Distance(pos, verticesList[trianglesList[i + 1]]); + d2 = Vector3.Distance(pos, verticesList[trianglesList[i + 2]]); + // find lowest distance from remaining vert to a triangle vertex + if (d0 < minDistance) + { + // set the min distance + minDistance = d0; + // update the i0 index + vertIndex0 = trianglesList[i]; + // update i1 index based on which other vertex in the triangle is closer. + vertIndex1 = d1 < d2 ? trianglesList[i + 1] : trianglesList[i + 2]; + } + // repeat for d1. + if (d1 < minDistance) + { + minDistance = d1; + vertIndex0 = trianglesList[i + 1]; + vertIndex1 = d0 < d2 ? trianglesList[i] : trianglesList[i + 2]; + } + // d2 not needed because d0 -> d1, d2 or d1 -> d0, d2 would give the same result if d2 was lowest. + } + // now we have the "closest" triangle.. + // add the vertex + if (vertIndex0 >= 0 && vertIndex1 >= 0) + { + verticesList.Add(pos); + // add the the triangle. + trianglesList.Add(verticesList.Count - 1); + trianglesList.Add(vertIndex0); + trianglesList.Add(vertIndex1); + } + else + { + // we have a single lonely vertex that needs to be added. + // luckily, just adding it 3 times and setting is a triangle works for VHACD.... + verticesList.Add(pos); + verticesList.Add(pos); + verticesList.Add(pos); + trianglesList.Add(verticesList.Count - 1); + trianglesList.Add(verticesList.Count - 2); + trianglesList.Add(verticesList.Count - 3); + } + + } + + + // extrude selected vertices. + if (parameters.NormalExtrudeMultiplier > 0.0f) + { + int vCount = verticesList.Count; + Dictionary vertexIndexToTriangleIndexDictionary = new Dictionary(); + int count = trianglesList.Count; + for (int i = 0; i < count; i++) + { + if (vertexIndexToTriangleIndexDictionary.ContainsKey(trianglesList[i])) + { + trianglesList.Add(vertexIndexToTriangleIndexDictionary[trianglesList[i]]); + } + else + { + int nCount = 0; + Vector3 p = verticesList[trianglesList[i]]; + Vector3 n = Vector3.zero; + for (int j = 0; j < vertices.Length; j++) + { + if (vertices[j] == p) + { + nCount++; + n += normals[j]; + } + } + n.Normalize(); + trianglesList.Add(verticesList.Count); + vertexIndexToTriangleIndexDictionary.Add(trianglesList[i], verticesList.Count); + verticesList.Add(p + n * parameters.NormalExtrudeMultiplier); + } + } + } + + // covert from local mesh space to world then + // convert verts from world to attach to local space. + Transform attachTo = parameters.AttachTo.transform; + for (int i = 0; i < verticesList.Count; i++) + { + verticesList[i] = t.TransformPoint(verticesList[i]); + verticesList[i] = attachTo.InverseTransformPoint(verticesList[i]); + } + + // create and return the mesh for use in vhacd. + Mesh m = new Mesh(); +#if (UNITY_2017_3_OR_NEWER) + m.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; +#endif + m.vertices = verticesList.ToArray(); + m.triangles = trianglesList.ToArray(); + createdMeshList.Add(m); + } + Mesh result = new Mesh(); + // use 32 bit index format for high # of verts. +#if (UNITY_2017_3_OR_NEWER) + result.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; +#endif + List cis = new List(); + // create combine instances for each created mesh. + for (int i = 0; i < createdMeshList.Count; i++) + { + if (createdMeshList[i] != null) + { + CombineInstance ci = new CombineInstance(); + ci.mesh = createdMeshList[i]; + cis.Add(ci); + } + } + //combine the mesh with the combine instances, NOT using the matrix. + result.CombineMeshes(cis.ToArray(), true, false); + return result; + } + + #endregion + + + + + +#if UNITY_6000_4_OR_NEWER + //switch to EntityIDs + + [SerializeField] + private List _AddedColliderIDs; + /// + /// List of added colliders through GetInstanceID() + /// + public List AddedColliderIDs + { + get + { + if (_AddedColliderIDs == null) + { + _AddedColliderIDs = new List(); + } + return _AddedColliderIDs; + } + set { _AddedColliderIDs = value; } + } + + + [SerializeField] + private List _AddedInstanceIDs; + /// + /// List of all object's instance IDs that should be destroyed on cleanup. These are things like + /// MeshCollider used for vertex selection. + /// MeshFilter for skinned mesh renderers for mesh colliders. + /// Compute shader component. + /// Gizmo drawing component. + /// + public List AddedInstanceIDs + { + get + { + if (_AddedInstanceIDs == null) + { + _AddedInstanceIDs = new List(); + } + return _AddedInstanceIDs; + } + set { _AddedInstanceIDs = value; } + } + +#else + + [SerializeField] + private List _AddedColliderIDs; + /// + /// List of added colliders through GetInstanceID() + /// + public List AddedColliderIDs + { + get + { + if (_AddedColliderIDs == null) + { + _AddedColliderIDs = new List(); + } + return _AddedColliderIDs; + } + set { _AddedColliderIDs = value; } + } + + [SerializeField] + private List _AddedInstanceIDs; + /// + /// List of all object's instance IDs that should be destroyed on cleanup. These are things like + /// MeshCollider used for vertex selection. + /// MeshFilter for skinned mesh renderers for mesh colliders. + /// Compute shader component. + /// Gizmo drawing component. + /// + public List AddedInstanceIDs + { + get + { + if (_AddedInstanceIDs == null) + { + _AddedInstanceIDs = new List(); + } + return _AddedInstanceIDs; + } + set { _AddedInstanceIDs = value; } + } +#endif + [SerializeField] + private GameObject _AttachToObject; + /// + /// If different from the selected gameobject, the attach to object is used to convert to local vertices / attach the collider to. + /// + public GameObject AttachToObject + { + get + { + if (_AttachToObject == null) + { + return SelectedGameObject; + } + return _AttachToObject; + } + set { _AttachToObject = value; } + } + + + + /// + /// Are we automatically including child skinned meshes when include child meshes is enabled? + /// + public bool AutoIncludeChildSkinnedMeshes { get { return ECEPreferences.AutoIncludeChildSkinnedMeshes; } } + + [SerializeField] + private bool _ColliderSelectEnabled; + /// + /// Is Collider Selection Enabled? Toggles colliders on and off when changed. + /// + public bool ColliderSelectEnabled + { + get { return _ColliderSelectEnabled; } + set + { + _ColliderSelectEnabled = value; + } + } + + [SerializeField] + private EasyColliderCompute _Compute; + /// + /// Compute shader script + /// + public EasyColliderCompute Compute + { + get + { + if (_Compute == null && SelectedGameObject != null) + { + _Compute = SelectedGameObject.GetComponent(); + } + return _Compute; + } + set + { + _Compute = value; + if (value != null && DisplayMeshVertices) + { + _Compute.SetDisplayAllBuffer(GetAllWorldMeshVertices()); + } + } + } + + [SerializeField] + private List _CreatedColliders; + /// + /// List of colliders we created + /// + public List CreatedColliders + { + get + { + if (_CreatedColliders == null) + { + _CreatedColliders = new List(); + } + return _CreatedColliders; + } + set { _CreatedColliders = value; } + } + + [SerializeField] + /// + /// Volume per vertex density. Used in displaying vertices so less verts in more space displays larger and vice versa. + /// + public float DensityScale = 1.0f; + + public bool DisplayMeshVertices + { + get { return ECEPreferences.DisplayAllVertices; } + } + + private static EasyColliderPreferences _ECEPreferences; + public static EasyColliderPreferences ECEPreferences + { + get + { + if (_ECEPreferences == null) + { + _ECEPreferences = EasyColliderPreferences.Preferences; + } + return _ECEPreferences; + } + } + + [SerializeField] + /// + /// Does the selected gameboject have a skinned mesh renderer as a child somewhere? + /// + public bool HasSkinnedMeshRenderer = false; + + [SerializeField] + private EasyColliderGizmos _Gizmos; + /// + /// Gizmos component for drawing vertices and selections. + /// + public EasyColliderGizmos Gizmos + { + get + { + if (_Gizmos == null && SelectedGameObject != null) + { + _Gizmos = SelectedGameObject.GetComponent(); + } + return _Gizmos; + } + set + { + _Gizmos = value; + if (value != null && DisplayMeshVertices) + { + _Gizmos.DisplayVertexPositions = GetAllWorldMeshVertices(); + } + } + } + + [SerializeField] + private bool _IncludeChildMeshes; + /// + /// Are we including child meshes for vertex selection? + /// + public bool IncludeChildMeshes + { + get { return _IncludeChildMeshes; } + set + { + if (_IncludeChildMeshes != value) + { + OnlyDeselectableColliders.Clear(); + // lets store things. + // List selectedVertsBeforeChange = new List(SelectedVertices); + //HashSet selectedNonVerts = new HashSet(SelectedNonVerticesSet); + HashSet selectedChildColliders = new HashSet(SelectedColliders); + if (!value && SelectedGameObject != null) + { + selectedChildColliders.ExceptWith(SelectedGameObject.GetComponents()); + } + // why do we do this? + GameObject selected = SelectedGameObject; + GameObject attach = AttachToObject; + SelectedGameObject = null; + _IncludeChildMeshes = value; + //OnIncludeChildMeshesChanged(value); + SelectedGameObject = selected; + AttachToObject = attach; + + /* foreach(var v in selectedVertsBeforeChange) + { + SelectVertex(v, true); + }*/ + foreach (var c in selectedChildColliders) + { + SelectCollider(c); + OnlyDeselectableColliders.Add(c); + } + } + // _IncludeChildMeshes = value; + // SetupChildObjects(value); + // CalculateDensity(); + if (value == false) + { + // CleanChildSelectedVertices(); + } + } + } + + public HashSet OnlyDeselectableColliders = new HashSet(); + + /* void OnIncludeChildMeshesChanged(bool value) + { + HashSet rootMeshFilters = new HashSet(); + HashSet childMeshFilters = new HashSet(); + rootMeshFilters.UnionWith(SelectedGameObject.GetComponents()); + childMeshFilters.UnionWith(SelectedGameObject.GetComponentsInChildren()); + childMeshFilters.ExceptWith + if (!value) + { + // CleanUpObject() + } + }*/ + + [SerializeField] + private bool _IsTrigger; + /// + /// Created colliders marked as trigger? + /// + /// + public bool IsTrigger { get { return _IsTrigger; } set { _IsTrigger = value; } } + + [SerializeField] + private List _LastSelectedVertices; + /// + /// List of the vertices that were selected last + /// + public List LastSelectedVertices + { + get + { + if (_LastSelectedVertices == null) + { + _LastSelectedVertices = new List(); + } + return _LastSelectedVertices; + } + set { _LastSelectedVertices = value; } + } + + [SerializeField] + private List _MeshFilters; + /// + /// List of mesh filters on SelectedGameobject + children (if IncludeChildMeshes) + /// + public List MeshFilters + { + get + { + if (_MeshFilters == null) + { + _MeshFilters = new List(); + } + return _MeshFilters; + } + set { _MeshFilters = value; } + } + + [SerializeField] + private List _NonKinematicRigidbodies; + /// + /// Rigidbodies already on the objects that were marked as kinematic during setup. + /// + public List NonKinematicRigidbodies + { + get + { + if (_NonKinematicRigidbodies == null) + { + _NonKinematicRigidbodies = new List(); + } + return _NonKinematicRigidbodies; + } + set + { + _NonKinematicRigidbodies = value; + } + } + +#if (UNITY_6000_0_OR_NEWER) + [SerializeField] + private PhysicsMaterial _PhysicMaterial; + /// + /// Physic material to add to colliders upon creation. + /// + public PhysicsMaterial PhysicMaterial { get { return _PhysicMaterial; } set { _PhysicMaterial = value; } } +#else + [SerializeField] + private PhysicMaterial _PhysicMaterial; + /// + /// Physic material to add to colliders upon creation. + /// + public PhysicMaterial PhysicMaterial { get { return _PhysicMaterial; } set { _PhysicMaterial = value; } } +#endif + + [SerializeField] + private List _RaycastableColliders; + public List RaycastableColliders + { + get + { + if (_RaycastableColliders == null) + { + _RaycastableColliders = new List(); + } + return _RaycastableColliders; + } + set { _RaycastableColliders = value; } + } + + /// + /// Method we use to render points with. Either using a shader or gizmos. + /// + private RENDER_POINT_TYPE RenderPointType + { + get { return ECEPreferences.RenderPointType; } + } + + public void ChangeRenderPointType(RENDER_POINT_TYPE value) + { + // add or remove one if the value is changing and it's already added + if (value != RENDER_POINT_TYPE.SHADER && Compute != null) + { + TryDestroyComponent(Compute); + } + if (value != RENDER_POINT_TYPE.GIZMOS && Gizmos != null) + { + TryDestroyComponent(Gizmos); + } + // add the new component if needed. + if (value == RENDER_POINT_TYPE.GIZMOS && Gizmos == null && SelectedGameObject != null) + { + Gizmos = Undo.AddComponent(SelectedGameObject); + AddedInstanceIDs.Add(GetIDFor(Gizmos)); + } + if (value == RENDER_POINT_TYPE.SHADER && Compute == null && SelectedGameObject != null) + { + Compute = Undo.AddComponent(SelectedGameObject); + AddedInstanceIDs.Add(GetIDFor(Compute)); + } + } + + [SerializeField] + private int _GameObjectLayerOverride; + /// + /// Layer to set on rotated collider's gameobject if not using selected gameobject's layer. + /// + public int GameObjectLayerOverride { get { return _GameObjectLayerOverride; } set { _GameObjectLayerOverride = value; } } + + [SerializeField] + private List _SelectedColliders; + /// + /// List of currently selected colliders + /// + public List SelectedColliders + { + get + { + if (_SelectedColliders == null) + { + _SelectedColliders = new List(); + } + return _SelectedColliders; + } + set { _SelectedColliders = value; } + } + + [SerializeField] + private GameObject _SelectedGameObject; + /// + /// The currently selected gameobject. Changing this causes a cleanup of the previous selected object, and initialization of the object you are setting. + /// + public GameObject SelectedGameObject + { + get { return _SelectedGameObject; } + set + { + if (value == null) + { + CleanUpObject(_SelectedGameObject, false); + _SelectedGameObject = value; + AttachToObject = value; + } + else if (!EditorUtility.IsPersistent(value)) + { + // new selected object. + if (value != _SelectedGameObject) + { + // Had a selected object, clean it up. + if (_SelectedGameObject != null) + { + CleanUpObject(_SelectedGameObject, false); + } + // Value is an actual object, so set up everything that's needed. + if (value != null) + { + _SelectedGameObject = value; + AttachToObject = value; + SelectObject(value); + + // log a warning if there is a kinematic rigidbody on a parent, we dont track it because it can get lost more easily than child-rigidbodies. + // so just alert the user that vertices would not be able to be selected. + Rigidbody[] rbs = _SelectedGameObject.GetComponentsInParent(); + foreach (Rigidbody rb in rbs) + { + if (rb.gameObject != _SelectedGameObject && !rb.isKinematic) + { + Debug.LogWarning("EasyColliderEditor: A parent (" + rb.gameObject.name + ") of the selected object has a non-kinematic rigidbody, you may not be able to select vertices while it is marked as non-kinematic.", rb.gameObject); + } + } + + } + } + _SelectedGameObject = value; + AttachToObject = value; + } + else + { + Debug.LogError("Easy Collider Editor's Selected GameObject must be located in the scene. Select a gameobject from the scene hierarchy. If you wish to use a prefab, enter prefab isolation mode then select the object. For more information of editing prefabs, see the included documentation pdf."); + } + } + } + + [SerializeField] + private List _SelectedVertices; + /// + /// Selected Vertices list (Needs to be a list, as hashsets are unordered, and some of the collider methods require specific order selection (like rotated ones)) + /// + public List SelectedVertices + { + get + { + if (_SelectedVertices == null) + { + _SelectedVertices = new List(); + } + return _SelectedVertices; + } + private set { _SelectedVertices = value; } + } + + [SerializeField] + private HashSet _SelectedVerticesSet; + /// + /// HashSet of SelectedVertices. Used to make things a little faster to search through. + /// + public HashSet SelectedVerticesSet + { + get + { + if (_SelectedVerticesSet == null) + { + _SelectedVerticesSet = new HashSet(); + } + return _SelectedVerticesSet; + } + set { _SelectedVerticesSet = value; } + } + + + public HashSet SelectedNonVerticesSet = new HashSet(); + + //Serializing our hashsets. + [SerializeField] + private List _SerializedSelectedVertexSet; + + [SerializeField] + private List _TransformPositions; + /// + /// List of mesh filter world positions + /// + public List TransformPositions + { + get + { + if (_TransformPositions == null) + { + _TransformPositions = new List(); + } + return _TransformPositions; + } + set { _TransformPositions = value; } + } + + [SerializeField] + private List _TransformRotations; + /// + /// List of mesh filter rotations + /// + public List TransformRotations + { + get + { + if (_TransformRotations == null) + { + _TransformRotations = new List(); + } + return _TransformRotations; + } + set { _TransformRotations = value; } + } + + [SerializeField] + private List _TransformLocalScales; + /// + /// List of mesh filter local scales + /// + public List TransformLocalScales + { + get + { + if (_TransformLocalScales == null) + { + _TransformLocalScales = new List(); + } + return _TransformLocalScales; + } + set { _TransformLocalScales = value; } + } + + [SerializeField] private List _TransformLossyScales; + public List TransformLossyScales + { + get + { + if (_TransformLossyScales == null) + { + _TransformLossyScales = new List(); + } + return _TransformLossyScales; + } + set { _TransformLossyScales = value; } + } + + + [SerializeField] + /// + /// Is vertex selection enabled? + /// + public bool VertexSelectEnabled; + + HashSet _WorldMeshVertices; + /// + /// Set of all world space vertices for meshes that are able to have their vertices selected + /// + /// + public HashSet WorldMeshVertices + { + get + { + if (_WorldMeshVertices == null) + { + _WorldMeshVertices = new HashSet(); + } + return _WorldMeshVertices; + } + } + + HashSet _WorldMeshPositions; + /// + /// Set of mesh filters positions, for update world mesh vertices. + /// + /// + HashSet WorldMeshPositions + { + get + { + if (_WorldMeshPositions == null) + { + _WorldMeshPositions = new HashSet(); + } + return _WorldMeshPositions; + } + } + + HashSet _WorldMeshRotations; + /// + /// Set of world mesh rotations, for updating world mesh vertices. + /// + HashSet WorldMeshRotations + { + get + { + if (_WorldMeshRotations == null) + { + _WorldMeshRotations = new HashSet(); + } + return _WorldMeshRotations; + } + } + + HashSet _WorldMeshTransforms; + /// + /// Set of world mesh transforms, for updating world mesh vertices. + /// + HashSet WorldMeshTransforms + { + get + { + if (_WorldMeshTransforms == null) + { + _WorldMeshTransforms = new HashSet(); + } + return _WorldMeshTransforms; + } + } + + /// + /// Removes all vertices that have index's greater than MeshFilter's count. + /// + private void CleanChildSelectedVertices() + { + // SelectedVertices.RemoveAll(vert => vert.MeshFilterIndex >= MeshFilters.Count); + SelectedVertices.RemoveAll(vert => vert.T != SelectedGameObject.transform); + } + + /// + /// Cleans up the object and children if required. Destroys components based on if going into play mode. Reenables/disables components to pre-selection values. + /// + /// Parent object to clean up + /// Is the editor going into play mode? + public void CleanUpObject(GameObject go, bool intoPlayMode) + { + // var so it works with 6000_3 and 6000_4 or newer (int to entity id switch) + foreach (var id in AddedInstanceIDs) + { + Object o = GetObjectForID(id); + if (o != null) + { + if (intoPlayMode) + { + if (o is Component) + { + TryDestroyComponent((Component)o); + } + else + { + TryDestroyObject((GameObject)o); + } + } + else + { + if (o is Component) + { + TryDestroyComponent((Component)o); + } + else + { + TryDestroyObject((GameObject)o); + } + } + } + } + foreach (Rigidbody rb in NonKinematicRigidbodies) + { + if (rb != null) + { + rb.isKinematic = false; + } + } + // force cleanup gizmos and compute. + if (Gizmos != null) + { + TryDestroyComponent(Gizmos); + } + if (Compute != null) + { + TryDestroyComponent(Compute); + } + if (go != null) + { + // lets be safe and search for additional compute and gizmos. + EasyColliderGizmos[] extraGizmos = go.GetComponentsInChildren(); + for (int i = 0; i < extraGizmos.Length; i++) + { + TryDestroyComponent(extraGizmos[i]); + } + EasyColliderCompute[] extraComputes = go.GetComponentsInChildren(); + for (int i = 0; i < extraComputes.Length; i++) + { + TryDestroyComponent(extraComputes[i]); + } + + } + if (go != null) + { + // Enable by added collider instance ids incase they were lost. + Collider[] cols = go.GetComponentsInChildren(); + foreach (Collider col in cols) + { + if (AddedColliderIDs.Contains(GetIDFor(col))) + { + col.enabled = true; + } + } + } + HasSkinnedMeshRenderer = false; + ClearListsForNewObject(); + } + + /// + /// Creates new lists for all the lists used. + /// + void ClearListsForNewObject() + { +#if UNITY_6000_4_OR_NEWER + AddedInstanceIDs = new List(); +#else + AddedInstanceIDs = new List(); +#endif + CreatedColliders = new List(); + LastSelectedVertices = new List(); + MeshFilters = new List(); + NonKinematicRigidbodies = new List(); + OnlyDeselectableColliders = new HashSet(); + RaycastableColliders = new List(); + SelectedColliders = new List(); + SelectedVertices = new List(); + SelectedVerticesSet = new HashSet(); + SelectedNonVerticesSet = new HashSet(); + _SkinnedMeshFilterPairs = new Dictionary(); + BoneTransforms = new List(); + _selectedBones = new List(); + _selectedBoneVertices = new List(); + LastSelectedBone = null; + _VHACDPreviewResult = new Dictionary(); + } + + /// + /// Deselects all currently selected vertices. + /// + public void ClearSelectedVertices() + { + this.SelectedVertices = new List(); + this.SelectedVerticesSet = new HashSet(); + this.SelectedNonVerticesSet = new HashSet(); + } + + /// + /// Creates a box colider with a given orientation + /// + /// Orientation of box collider + public void CreateBoxCollider(COLLIDER_ORIENTATION orientation = COLLIDER_ORIENTATION.NORMAL) + { + EasyColliderCreator ecc = new EasyColliderCreator(); + Collider createdCollider = ecc.CreateBoxCollider(GetWorldVertices(true), GetProperties(orientation)); + if (createdCollider != null) + { + DisableCreatedCollider(createdCollider); + } + ClearSelectedVertices(); + } + + + /// + /// Creates a capsule collider using the method and orientation provided. + /// + /// Capsule collider algoirthm to use + /// Orientation to use + public void CreateCapsuleCollider(CAPSULE_COLLIDER_METHOD method, COLLIDER_ORIENTATION orientation = COLLIDER_ORIENTATION.NORMAL) + { + EasyColliderCreator ecc = new EasyColliderCreator(); + Collider createdCollider = null; + switch (method) + { + // use the same method for all min max' but pass the method in. + case CAPSULE_COLLIDER_METHOD.MinMax: + case CAPSULE_COLLIDER_METHOD.MinMaxPlusDiameter: + case CAPSULE_COLLIDER_METHOD.MinMaxPlusRadius: + createdCollider = ecc.CreateCapsuleCollider_MinMax(GetWorldVertices(true), GetProperties(orientation), method); + break; + case CAPSULE_COLLIDER_METHOD.BestFit: + createdCollider = ecc.CreateCapsuleCollider_BestFit(GetWorldVertices(true), GetProperties(orientation)); + break; + } + if (createdCollider != null) + { + DisableCreatedCollider(createdCollider); + } + ClearSelectedVertices(); + } + + public void CreateCylinderCollider() + { + EasyColliderCreator ecc = new EasyColliderCreator(); + // convert the selected world points to local points that represent a cylinder. + MeshColliderData data = ecc.CalculateCylinderCollider(GetWorldVertices(true), AttachToObject.transform); + if (ECEPreferences.SaveConvexHullAsAsset) + { + EasyColliderSaving.CreateAndSaveMeshAsset(data.ConvexMesh, SelectedGameObject); + } + // generate the hull from the cylinder points. + MeshCollider createdCollider = ecc.CreateConvexMeshCollider(data.ConvexMesh, AttachToObject, GetProperties()); + if (createdCollider != null) + { + DisableCreatedCollider(createdCollider); + } + ClearSelectedVertices(); + } + + /// + /// Creates a convex mesh collider from the currently selected points using the given method + /// + /// + public void CreateConvexMeshCollider(MESH_COLLIDER_METHOD method) + { + Mesh mesh = CreateConvexMesh(method); + if (mesh == null) + { + Debug.LogWarning("EasyColliderEditor: Unable to create a valid convex mesh collider from the selected vertices, likely because all selected vertices are coplanar."); + } + else + { + EasyColliderCreator ecc = new EasyColliderCreator(); + MeshCollider createdCollider = ecc.CreateConvexMeshCollider(mesh, AttachToObject, GetProperties()); + if (createdCollider != null) + { + DisableCreatedCollider(createdCollider); + } + ClearSelectedVertices(); + } + } + + /// + /// Creates a convex mesh from the currently selected points using the given method + /// + /// + /// Convex Mesh + private Mesh CreateConvexMesh(MESH_COLLIDER_METHOD method) + { + if (method == MESH_COLLIDER_METHOD.QuickHull) + { + return new EasyColliderCreator().CreateMesh_QuickHull(GetWorldVertices(true), AttachToObject, false, SelectedGameObject); + } + else + { + return new EasyColliderCreator().CreateMesh_Messy(GetWorldVertices(true), AttachToObject, SelectedGameObject); + } + } + + + public void CreateRotatedAndDuplicatedColliders(CREATE_COLLIDER_TYPE type) + { + EasyColliderCreator ecc = new EasyColliderCreator(); + List createdColliders = ecc.CreateRotateAndDuplicateColliders(type, GetWorldVertices(true), GetProperties()); + foreach (Collider c in createdColliders) + { + if (c != null) + { + DisableCreatedCollider(c); + } + } + ClearSelectedVertices(); + } + + /// + /// Creates a sphere collider + /// + /// Algorith to use to create the sphere collider. + public void CreateSphereCollider(SPHERE_COLLIDER_METHOD method) + { + EasyColliderCreator ecc = new EasyColliderCreator(); + Collider createdCollider = null; + switch (method) + { + case SPHERE_COLLIDER_METHOD.BestFit: + createdCollider = ecc.CreateSphereCollider_BestFit(GetWorldVertices(true), GetProperties()); + break; + case SPHERE_COLLIDER_METHOD.Distance: + createdCollider = ecc.CreateSphereCollider_Distance(GetWorldVertices(true), GetProperties()); + break; + case SPHERE_COLLIDER_METHOD.MinMax: + createdCollider = ecc.CreateSphereCollider_MinMax(GetWorldVertices(true), GetProperties()); + break; + } + if (createdCollider != null) + { + DisableCreatedCollider(createdCollider); + } + ClearSelectedVertices(); + } + + /// + /// Disables a created collider based on preferences + /// + /// Collider to disable + public void DisableCreatedCollider(Collider col) + { + // keep track fo the collider that was created. + CreatedColliders.Add(col); + AddedColliderIDs.Add(GetIDFor(col)); + } + + /// + /// Gets all colliders on parent + children if including children. + /// + /// Array of all colliders + private Collider[] GetAllColliders() + { + if (SelectedGameObject != null) + { + HashSet selectedColliders = new HashSet(); + if (IncludeChildMeshes) + { + selectedColliders.UnionWith(SelectedGameObject.GetComponentsInChildren()); + selectedColliders.UnionWith(AttachToObject.GetComponentsInChildren()); + } + else + { + selectedColliders.UnionWith(SelectedGameObject.GetComponents()); + selectedColliders.UnionWith(AttachToObject.GetComponents()); + // "child meshes" toggle would ennable this, but collider holders don't have meshes, but generally should be selectable. + for (int i = 0; i < SelectedGameObject.transform.childCount; i++) + { + Transform t = SelectedGameObject.transform.GetChild(i); + // special names are Rotated X (x = box, capsule), VHACDCollider, and EasyColliderHolder + if ((t.name.Contains("Rotated") && t.name.Contains("Collider")) || t.name.Contains("VHACDCollider") || t.name.Contains("EasyColliderHolder")) + { + // collider holders can also hold rotated colliders, so get children as well. + selectedColliders.UnionWith(t.gameObject.GetComponentsInChildren()); + } + } + } + selectedColliders.ExceptWith(RaycastableColliders); + // Allow selecting colliders from selected gameobject, and it's children, and attach to / it's children. + return selectedColliders.ToArray(); + } + return new Collider[0]; + } + + /// + /// Gets all the locations in world space of all MeshFilters vertices. + /// + /// World space locations of all mesh filters + public HashSet GetAllWorldMeshVertices() + { + bool hasChanged = false; + // if the pos, rot, or transform count is different than the mesh filter count we know we need to update. + if (WorldMeshPositions.Count != MeshFilters.Count || WorldMeshRotations.Count != MeshFilters.Count || WorldMeshTransforms.Count != MeshFilters.Count) + { + hasChanged = true; + } + if (!hasChanged) + { + // we need to update if any of the mesh transforms have moved, or translated, + // or the transform itself is different (ie. 2 different objects with same pos and rotation still means we need to update) + foreach (MeshFilter filter in MeshFilters) + { + if (!WorldMeshPositions.Contains(filter.transform.position)) + { + hasChanged = true; + break; + } + if (!WorldMeshRotations.Contains(filter.transform.rotation)) + { + hasChanged = true; + break; + } + if (!WorldMeshTransforms.Contains(filter.transform)) + { + hasChanged = true; + break; + } + } + } + // need to recalculate all the world locations. + if (hasChanged) + { + // Clear our lists to rebuild them. + WorldMeshVertices.Clear(); + WorldMeshPositions.Clear(); + WorldMeshRotations.Clear(); + WorldMeshTransforms.Clear(); + foreach (MeshFilter filter in MeshFilters) + { + if (filter != null) + { + Transform t = filter.transform; + WorldMeshPositions.Add(t.position); + WorldMeshRotations.Add(t.rotation); + WorldMeshTransforms.Add(t); + Vector3[] vertices = filter.sharedMesh.vertices; + foreach (Vector3 vert in vertices) + { + WorldMeshVertices.Add(t.TransformPoint(vert)); + } + } + } + } + // nothings changed? just return our hashset of world points. + return WorldMeshVertices; + } + + /// + /// Gets all the mesh filters on the object. Gets the child meshes if include children is enabled, and creates mesh filters for any skinned mesh renderers if required. + /// + /// Parent object to get mesh filters from. + /// List of mesh filters for the object, children, and skinned mesh renderers. + List GetMeshFilters(GameObject go) + { + if (go == null) return null; + List meshFilters = new List(); + if (IncludeChildMeshes) + { + MeshFilter[] childMeshFilters = go.GetComponentsInChildren(false); + foreach (MeshFilter childMeshFilter in childMeshFilters) + { + if (childMeshFilter != null) + { + meshFilters.Add(childMeshFilter); + } + } + SkinnedMeshRenderer[] childSkinnedMeshRenderers = go.GetComponentsInChildren(false); + if (AutoIncludeChildSkinnedMeshes) + { + foreach (SkinnedMeshRenderer smr in childSkinnedMeshRenderers) + { + meshFilters.Add(SetupFilterForSkinnedMesh(smr)); + } + } + if (childSkinnedMeshRenderers.Length > 0) + { + HasSkinnedMeshRenderer = true; + } + } + else + { + MeshFilter meshFilter = go.GetComponent(); + if (meshFilter != null) + { + meshFilters.Add(meshFilter); + } + else + { + SkinnedMeshRenderer smr = go.GetComponent(); + if (smr != null) + { + meshFilters.Add(SetupFilterForSkinnedMesh(smr)); + HasSkinnedMeshRenderer = true; + } + } + } + return meshFilters; + } + + +#if (UNITY_2022_2_OR_NEWER) + public LayerMask IncludeLayers; + public LayerMask ExcludeLayers; + public int LayerOverridePriority; + public bool ProvidesContacts; +#endif + + /// + /// Creates an EasyColliderProperties based on the ECEditors values. + /// + /// Orientation property + /// EasyColliderProperties to pass to collider creation methods + public EasyColliderProperties GetProperties(COLLIDER_ORIENTATION orientation = COLLIDER_ORIENTATION.NORMAL) + { + EasyColliderProperties ecp = new EasyColliderProperties(); + ecp.IsTrigger = IsTrigger; + + ecp.PhysicMaterial = PhysicMaterial; + + if (SelectedGameObject != null) + { + ecp.Layer = ECEPreferences.CopyParentObjectLayer ? AttachToObject.layer : GameObjectLayerOverride; + } + else + { + ecp.Layer = GameObjectLayerOverride; + } + + ecp.AttachTo = AttachToObject; + ecp.Orientation = orientation; + //overrides added in 2022.2. +#if (UNITY_2022_2_OR_NEWER) + ecp.LayerOverridePriority = LayerOverridePriority; + ecp.IncludeLayers = IncludeLayers; + ecp.ExcludeLayers = ExcludeLayers; + ecp.ProvidesContacts = ProvidesContacts; +#endif + return ecp; + } + + + +#if (UNITY_6000_4_OR_NEWER) + /// + /// method to get id for any unityengine.object + /// + /// + /// entity id or instance id depending on version of unity. + EntityId GetIDFor(Object obj) + { + return obj.GetEntityId(); + } + /// + /// gets the object an id (entity or instance id depending on unity version) represents + /// + /// + /// + Object GetObjectForID(EntityId id) + { + return EditorUtility.EntityIdToObject(id); + } +#else + /// + /// method to get id for any unityengine.object + /// + /// + /// entity id or instance id depending on version of unity. + int GetIDFor(Object obj) + { + return obj.GetInstanceID(); + } + /// + /// gets the object an id (entity or instance id depending on unity version) represents + /// + /// + /// + Object GetObjectForID(int id) + { + return EditorUtility.InstanceIDToObject(id); + } +#endif + + + + + /// + /// Gets all colliders that can be converted for DOTS (ie anything but the mesh colliders added for selection process) + /// + /// + public Collider[] GetConvertibleColliders() + { + Collider[] colliders = GetAllColliders(); + for (int i = 0; i < colliders.Length; i++) + { + if (AddedInstanceIDs.Contains(GetIDFor(colliders[i]))) + { + colliders[i] = null; + } + } + return colliders; + } + + + /// + /// Gets a list of the selected vertices in world space positions. + /// + /// List of world space positions. + public List GetWorldVertices(bool extrude = false) + { + if (!extrude) + { + // just return the world verts. + List verts = new List(SelectedVertices.Count); + foreach (EasyColliderVertex ecv in SelectedVertices) + { + if (ecv.T == null) continue; + verts.Add(ecv.T.TransformPoint(ecv.LocalPosition)); + } + return verts; + } + bool extrudeOut = (extrude && ECEPreferences.VertexNormalOffsetType != NORMAL_OFFSET.In && ECEPreferences.VertexNormalOffset != 0); + bool extrudeIn = (extrude && ECEPreferences.VertexNormalOffsetType != NORMAL_OFFSET.Out && ECEPreferences.VertexNormalInset != 0); + // create list with enough space for all the vertices + int count = ((extrudeIn && extrudeOut) ? SelectedVertices.Count * 3 : SelectedVertices.Count) + (extrudeOut ? SelectedVertices.Count : 0) + (extrudeIn ? SelectedVertices.Count : 0); + List worldVertices = new List(count); + // order of first 3 can matter for rotated colliders, so need to add the actual selected vertices first if needed. + if ((!extrudeOut && !extrudeIn) || ECEPreferences.VertexNormalOffsetType == NORMAL_OFFSET.Both) + { + // both? add selected vertices, otherwise only offset the + foreach (EasyColliderVertex ecv in SelectedVertices) + { + if (ecv.T == null) continue; + worldVertices.Add(ecv.T.TransformPoint(ecv.LocalPosition)); + } + } + foreach (EasyColliderVertex ecv in SelectedVertices) + { + if (ecv.T == null) continue; + if (extrudeOut) + { + worldVertices.Add(ecv.T.TransformPoint(ecv.LocalPosition + ecv.Normal * ECEPreferences.VertexNormalOffset)); + } + if (extrudeIn) + { + worldVertices.Add(ecv.T.TransformPoint(ecv.LocalPosition - ecv.Normal * ECEPreferences.VertexNormalInset)); + } + } + return worldVertices; + } + + public List GetNormals() + { + List normals = new List(SelectedVertices.Count); + foreach (EasyColliderVertex ecv in SelectedVertices) + { + if (ecv.T == null) continue; + normals.Add(ecv.Normal); + } + return normals; + } + + /// + /// Grows selected vertices out from all selected vertices + /// + public void GrowAllSelectedVertices() + { + GrowVertices(SelectedVerticesSet); + } + + /// + /// Grows selected vertices out from all selected vertices until it can no longer grow. + /// + public void GrowAllSelectedVerticesMax() + { + int startCount = 0; + int currentCount = 0; + do + { + startCount = SelectedVerticesSet.Count; + GrowVertices(SelectedVerticesSet); + currentCount = SelectedVerticesSet.Count; + } while (startCount != currentCount); + } + + /// + /// Grows selected vertices out from the last selected vertex(s) + /// + public void GrowLastSelectedVertices() + { + HashSet set = new HashSet(); + set.UnionWith(LastSelectedVertices); + GrowVertices(set); + } + + /// + /// Grows selected vertices from the last selected vertices unttil it can no longer be grown. + /// + public void GrowLastSelectedVerticesMax() + { + int startCount = 0; + int currentCount = 0; + do + { + startCount = SelectedVerticesSet.Count; + GrowLastSelectedVertices(); + currentCount = SelectedVerticesSet.Count; + } while (startCount != currentCount); + } + + /// + /// Grows the list of vertices by shared triangles + /// + /// The list of vertices to expand out from + public void GrowVertices(HashSet verticesToGrow) + { + HashSet newSelectedVertices = new HashSet(); + Transform t; + Vector3[] vertices; + int[] triangles; + // Go through every filter & triangle, seems the fastest way to do it without storing the vertices & triangles of every mesh. + foreach (MeshFilter filter in MeshFilters) + { + if (filter == null || filter.sharedMesh == null) continue; + triangles = filter.sharedMesh.triangles; + vertices = filter.sharedMesh.vertices; + t = filter.transform; + for (int i = 0; i < triangles.Length; i += 3) + { + EasyColliderVertex ecv1 = new EasyColliderVertex(t, vertices[triangles[i]]); + EasyColliderVertex ecv2 = new EasyColliderVertex(t, vertices[triangles[i + 1]]); + EasyColliderVertex ecv3 = new EasyColliderVertex(t, vertices[triangles[i + 2]]); + if (verticesToGrow.Contains(ecv1) || verticesToGrow.Contains(ecv2) || verticesToGrow.Contains(ecv3)) + { + newSelectedVertices.Add(ecv1); + newSelectedVertices.Add(ecv2); + newSelectedVertices.Add(ecv3); + } + } + } + // newly selected vertices are the ones where they are in the new set, but aren't currently in the selected set. + HashSet newVertices = new HashSet(newSelectedVertices.Where(value => !SelectedVerticesSet.Contains(value))); + SelectVertices(newVertices); + } + + /// + /// Checks if the transforms the mesh filters of the currently selected gameobject have moved or rotated + /// + /// Should the list of transforms be updated? + /// True if any of the valid transform meshes are on have moved + public bool HasTransformMoved(bool update = false) + { + bool hasMoved = false; + Transform t = null; + + foreach (MeshFilter filter in MeshFilters) + { + if (filter == null) { continue; } + t = filter.transform; + if (filter != null + && t != null + && !TransformPositions.Contains(t.position) + || !TransformRotations.Contains(t.rotation) + || !TransformLocalScales.Contains(t.localScale) + || !TransformLossyScales.Contains(t.lossyScale)) + { + hasMoved = true; + break; + } + } + if (hasMoved && update) + { + TransformPositions.Clear(); + TransformRotations.Clear(); + TransformLocalScales.Clear(); + foreach (MeshFilter filter in MeshFilters) + { + if (filter == null) { continue; } + t = filter.transform; + if (filter != null) + { + TransformRotations.Add(t.rotation); + TransformPositions.Add(t.position); + TransformLocalScales.Add(t.localScale); + TransformLossyScales.Add(t.lossyScale); + } + } + } + return hasMoved; + } + + /// + /// Inverts the currently selected vertices + /// + public void InvertSelectedVertices() + { + // just a hash set to add all the vertices to. + HashSet invs = new HashSet(); + // Variables to hold values + Vector3[] vertices; + Transform transform; + for (int i = 0; i < MeshFilters.Count; i++) + { + if (MeshFilters[i] != null && MeshFilters[i].sharedMesh != null) + { + // we only assign vertices array once per filter. + transform = MeshFilters[i].transform; + vertices = MeshFilters[i].sharedMesh.vertices; + for (int j = 0; j < vertices.Length; j++) + { + invs.Add(new EasyColliderVertex(transform, vertices[j])); + } + } + } + invs.UnionWith(SelectedVertices); + // select all the vertices (will deselect selected, and select unselected) + SelectVertices(invs); + } + + /// + /// Checks to see if a collider is already selected + /// + /// Collider to check + /// True if selected + public bool IsColliderSelected(Collider collider) + { + if (SelectedColliders.Contains(collider)) + { + return true; + } + return false; + } + + /// + /// Checks to make sure the collider is in the list of colliders that were added, or disabled. + /// + /// Collider to check + /// true if selectable + public bool IsSelectableCollider(Collider collider) + { + if (OnlyDeselectableColliders.Contains(collider)) return true; + if (GetAllColliders().Contains(collider)) + { + // we don't want to allow selecting mesh colliders that we've added for vertex selection. + // they wouldn't be actually removed, but it makes selecting colliders more difficult if people can. + if (AddedInstanceIDs.Contains(GetIDFor(collider))) return false; + return true; + } + return false; + } + + + + + [SerializeField] private List _SerializedSelectedNonVertexSet = new List(); + [SerializeField] private List _SerializedSkinnedMeshRenderers = new List(); + [SerializeField] private List _SerializedSkinnedMeshMeshFilters = new List(); + + /// + /// Called after deserializing, used to deserilize our serializable list of selected points back into the hashset. + /// Otherwise saving scripts and stuff that reloads domain will break things. + /// + public void OnAfterDeserialize() + { + // Deserialize our hashsets. + if (_SerializedSelectedVertexSet.Count > 0) + { + SelectedVerticesSet = new HashSet(_SerializedSelectedVertexSet); + } + else + { + SelectedVerticesSet = new HashSet(); + } + if (_SerializedSelectedNonVertexSet.Count > 0) + { + SelectedNonVerticesSet = new HashSet(_SerializedSelectedNonVertexSet); + } + else + { + SelectedNonVerticesSet = new HashSet(); + } + + if (_SerializedSkinnedMeshRenderers.Count > 0) + { + _SkinnedMeshFilterPairs = new Dictionary(); + for (int i = 0; i < _SerializedSkinnedMeshRenderers.Count; i++) + { + _SkinnedMeshFilterPairs.Add(_SerializedSkinnedMeshRenderers[i], _SerializedSkinnedMeshMeshFilters[i]); + } + } + } + + /// + /// Called before serialization, used to store our hashset of selected vertices into a serializable list. + /// + public void OnBeforeSerialize() + { + // Serialize ours hashsets. + if (_SerializedSelectedVertexSet == null) + { + _SerializedSelectedVertexSet = new List(); + } + _SerializedSelectedVertexSet = SelectedVerticesSet.ToList(); + + if (_SerializedSelectedNonVertexSet == null) + { + _SerializedSelectedNonVertexSet = new List(); + } + _SerializedSelectedNonVertexSet = SelectedNonVerticesSet.ToList(); + + if (_SerializedSkinnedMeshMeshFilters == null) + { + _SerializedSkinnedMeshMeshFilters = new List(); + } + if (_SerializedSkinnedMeshRenderers == null) + { + _SerializedSkinnedMeshRenderers = new List(); + } + _SerializedSkinnedMeshMeshFilters.Clear(); + _SerializedSkinnedMeshRenderers.Clear(); + foreach (var kvp in _SkinnedMeshFilterPairs) + { + _SerializedSkinnedMeshRenderers.Add(kvp.Key); + _SerializedSkinnedMeshMeshFilters.Add(kvp.Value); + } + } + + /// + /// Removes all colliders on the currently selected gameobject + attach to, and it's children. + /// + public void RemoveAllColliders() + { + // Get colliders from either selected or selected + children. + Collider[] colliders = GetAllColliders(); + // set selcted colliders + SelectedColliders = colliders.ToList(); + // remove them. + RemoveSelectedColliders(); + // traverse children to remove vhacd colliders + List vhacdHolders = AttachToObject.GetComponentsInChildren().Where(x => x.gameObject.name.Contains("VHACDColliders")).Select(a => a.gameObject).ToList(); + foreach (GameObject obj in vhacdHolders) + { + // Undo.DestroyObjectImmediate(obj); + TryDestroyObject(obj); + } + } + + /// + /// Removes the currently selected colliders. + /// + public void RemoveSelectedColliders() + { + foreach (Collider col in SelectedColliders) + { + // skip if null, or it's a collider we've added for functionality. + if (col == null || AddedInstanceIDs.Contains(GetIDFor(col))) continue; + CreatedColliders.Remove(col); + if (col.transform.childCount == 0 && ((col.gameObject.name.Contains("Rotated") && col.gameObject.name.Contains("Collider")) || col.gameObject.name.Contains("VHACDCollider") || col.gameObject.name.Contains("EasyColliderHolder"))) + { // is a rotated collider, or a vhacd collider. + Collider[] collidersOnRotatedGameObject = col.GetComponents(); + bool removeRotated = true; + foreach (Collider collider in collidersOnRotatedGameObject) + { + if (!SelectedColliders.Contains(collider)) + { + removeRotated = false; + break; + } + } + if (removeRotated) + { + Transform parent = null; + if (col.transform.parent != null && col.transform.parent.name.Contains("EasyColliderHolder")) + { + parent = col.transform.parent; + } + Undo.RecordObject(col.gameObject, "Remove collider"); + Component[] c = col.GetComponents(); + if (c.Length == 2) + { // don't destroy the collider holder that a user has attached other components to. + TryDestroyObject(col.gameObject); + } + // can remove components from prefab instances, but not the object itself. + TryDestroyComponent(col); + if (parent != null && parent.childCount == 0) + { + c = parent.GetComponents(); + if (c.Length == 1) + { // don't remove collider holder parent that users have attached their own components to. + // Undo.DestroyObjectImmediate(parent.gameObject); + TryDestroyObject(parent.gameObject); + } + } + } + else + { + // just remove the selected collider. + TryDestroyComponent(col); + } + } + else // has children, not a rotated / collider holder or vhacd collider. + { + Undo.RecordObject(col, "Remove collider"); + TryDestroyComponent(col); + } + } + SelectedColliders = new List(); + } + + /// + /// Rings around the last 2 selected vertices, selecting all the vertices in the ring. + /// + public void RingSelectVertices() + { + if (SelectedVertices.Count < 2) + { + Debug.LogWarning("Easy Collider Editor: Ring select requires 2 selected vertices."); + return; + } + // last 2 selected vertices must come from the same transform, otherwise you can't really ring around a mesh.. + if (SelectedVertices[SelectedVertices.Count - 1].T != SelectedVertices[SelectedVertices.Count - 2].T) + { + Debug.LogWarning("Easy Collider Editor: Ring select from different transforms not allowed."); + return; + } + // list of all the vertice's were going to add at the end + List newVerticesToAdd = new List(); + // add the last 2 vertices initially so we know where to end. + newVerticesToAdd.Add(SelectedVertices[SelectedVertices.Count - 1]); + newVerticesToAdd.Add(SelectedVertices[SelectedVertices.Count - 2]); + // get mesh's vertices & triangles. + Vector3[] vertices = new Vector3[0]; + int[] triangles = new int[3]; + Vector3[] normals = new Vector3[0]; + Transform t = SelectedVertices[SelectedVertices.Count - 1].T; + foreach (MeshFilter filter in MeshFilters) + { + if (filter == null) continue; + if (filter.transform == t) + { + vertices = filter.sharedMesh.vertices; + triangles = filter.sharedMesh.triangles; + normals = filter.sharedMesh.normals; + } + } + // start vertex + Vector3 currentVertex = SelectedVertices[SelectedVertices.Count - 1].LocalPosition; + // previous vertex. + Vector3 prevVertex = SelectedVertices[SelectedVertices.Count - 2].LocalPosition; + // directon vector for first 2 points. + Vector3 currentDirection = (currentVertex - prevVertex).normalized; + // Directions for calculations + Vector3 directionA, directionB = directionA = Vector3.zero; + // points for calculations + Vector3 pointA, pointB = pointA = Vector3.zero; + // angle from calculations + float angleA, angleB = angleA = 0.0f; + + // best point found in each iteration + Vector3 bestPoint = Vector3.zero; + // direction fot he best point (from current point) + Vector3 bestDirection = Vector3.zero; + // best angle from the best point (angle between current direction and best points direction from current point) + float bestAngle = Mathf.Infinity; + while (true) + { + // reset best angle for each iteration. + bestAngle = Mathf.Infinity; + // go through all the triangles. + for (int i = 0; i < triangles.Length; i += 3) + { + // if the triangle doesn't contain both the current and previous vertex. + // (as it's by the position, it allows cross edges that match position but not actual vertices' index) + if ((vertices[triangles[i]] == currentVertex || vertices[triangles[i + 1]] == currentVertex || vertices[triangles[i + 2]] == currentVertex) + && (vertices[triangles[i]] != prevVertex && vertices[triangles[i + 1]] != prevVertex && vertices[triangles[i + 2]] != prevVertex)) + { + // if it's the first vertex. + if (vertices[triangles[i]] == currentVertex) + { + // set the values for the pointA, pointB, directionA, and directionB to calculate. + pointA = vertices[triangles[i + 1]]; + pointB = vertices[triangles[i + 2]]; + directionA = pointA - currentVertex; + directionB = pointB - currentVertex; + } + else if (vertices[triangles[i + 1]] == currentVertex) + { + pointA = vertices[triangles[i]]; + pointB = vertices[triangles[i + 2]]; + directionA = pointA - currentVertex; + directionB = pointB - currentVertex; + } + else if (vertices[triangles[i + 2]] == currentVertex) + { + pointA = vertices[triangles[i]]; + pointB = vertices[triangles[i + 1]]; + directionA = pointA - currentVertex; + directionB = pointB - currentVertex; + } + // calculate angles between current direction and the direction to point A and point B. + angleA = Vector3.Angle(currentDirection, directionA); + angleB = Vector3.Angle(currentDirection, directionB); + // if the angle is less than our current best angle, and less than the other triangles angle + if (angleA < bestAngle && angleA < angleB) + { + // set our new best angle, best point, and best direction. + bestAngle = angleA; + bestPoint = pointA; + bestDirection = directionA; + } + else if (angleB < bestAngle && angleB < angleA) + { + bestAngle = angleB; + bestPoint = pointB; + bestDirection = directionB; + } + } + } + currentDirection = bestDirection; + prevVertex = currentVertex; + currentVertex = bestPoint; + EasyColliderVertex ecv = new EasyColliderVertex(t, bestPoint); + if (newVerticesToAdd.Contains(ecv)) + { + // reach some kind of end (newest point is already to be added.) + break; + } + else + { + newVerticesToAdd.Add(ecv); + } + } + // create a hash set from the verts as we need it for the select vertices method which also handles normal smoothing. + HashSet newVerts = new HashSet(newVerticesToAdd); + // except with the current selected vertices so they dont get deselected when doing a ring. + newVerts.ExceptWith(SelectedVerticesSet); + // select the vertices. + SelectVertices(newVerts); + } + + /// + /// Selects or deselects a collider + /// + /// collider to select or deselect. + public void SelectCollider(Collider collider) + { + // dont try to select a null collider. + if (collider == null) return; + if (SelectedColliders.Contains(collider)) + { + SelectedColliders.Remove(collider); + OnlyDeselectableColliders.Remove(collider); + } + else + { + SelectedColliders.Add(collider); + } + } + + public void DeselectAllColliders() + { + SelectedColliders = new List(); + } + + public void InvertSelection() + { + HashSet set = new HashSet(); + set.UnionWith(GetAllColliders()); + set.ExceptWith(SelectedColliders); + set.RemoveWhere(x => x.enabled == false || AddedInstanceIDs.Contains(GetIDFor(x))); + SelectedColliders = set.ToList(); + } + + /// + /// Selects the gameobject. Sets up the require components based for the object. + /// + /// GameObject to select + void SelectObject(GameObject obj) + { + // set up mesh filter list + MeshFilters = GetMeshFilters(obj); + // add / disable rigidbodies + colliders + SetRequiredComponentsFrom(obj, MeshFilters); + // add display vertices component. + if (RenderPointType == RENDER_POINT_TYPE.GIZMOS) + { + Gizmos = Undo.AddComponent(obj); + AddedInstanceIDs.Add(GetIDFor(Gizmos)); + } + else if (RenderPointType == RENDER_POINT_TYPE.SHADER) + { + Compute = Undo.AddComponent(obj); + AddedInstanceIDs.Add(GetIDFor(Compute)); + } + } + + /// + /// Selects a bunch of vertices at once. + /// Also handles smoothing of the selected normals. + /// + /// Set of vertices + public void SelectVertices(HashSet vertices, bool calculateNormals = true) + { + SelectedNonVerticesSet.ExceptWith(vertices); + + // removes selected vertices that are in the vertices hashset. (deselects already selected vertices) + List stillSelectedVertices = SelectedVertices.Where((value, index) => !vertices.Contains(value)).ToList(); + // adds vertices in the vertices set that aren't already selected (selects unselected vertices) + List newlySelectedVertices = vertices.Where((value) => !SelectedVerticesSet.Contains(value)).ToList(); + // Debug.Log("Select:" + vertices.Count + " Still selected:" + stillSelectedVertices.Count + " New:" + newlySelectedVertices.Count); + if (calculateNormals) + { + // could calculate smooth normals before removing vertices, as that would be more consistently slow. + // but then it's the same speed of inverting from all to none. + HashSet newSelectedSet = new HashSet(); + newSelectedSet.UnionWith(newlySelectedVertices); + // calculates the smoothed normals for the selected vertices. + Vector3[] verts = new Vector3[0]; + Vector3[] normals = new Vector3[0]; + MeshFilter mf = null; + // dictionary of vert to list of normals found. + Dictionary> modified = new Dictionary>(); + for (int i = 0; i < MeshFilters.Count; i++) + { + mf = MeshFilters[i]; + if (mf == null || mf.sharedMesh == null) continue; + verts = MeshFilters[i].sharedMesh.vertices; + normals = MeshFilters[i].sharedMesh.normals; + Transform t = mf.transform; + for (int j = 0; j < verts.Length; j++) + { + EasyColliderVertex v = new EasyColliderVertex(t, verts[j]); + if (modified.ContainsKey(v)) + { + // add the normals. + modified[v].Add(normals[j]); + } + else if (newSelectedSet.Contains(v)) + { + // new vertex, create a list of normals. + modified.Add(v, new List() { normals[j] }); + } + } + } + // to re-add all the vertices after calculating the normals. + newSelectedSet.Clear(); + foreach (var item in modified) + { + Vector3 v = item.Value.Aggregate(new Vector3(0, 0, 0), (cur, val) => cur + val); + v.Normalize(); + EasyColliderVertex ecv = new EasyColliderVertex(item.Key); + ecv.Normal = v; + newSelectedSet.Add(ecv); + } + newlySelectedVertices = newSelectedSet.ToList(); + } + + + + // last selected are the newly selected vertices. + LastSelectedVertices.Clear(); + LastSelectedVertices = newlySelectedVertices; + // Combine the lists for the all currently selected vertices. + stillSelectedVertices.AddRange(newlySelectedVertices); + SelectedVertices = stillSelectedVertices; + // clear the selected vertices set + SelectedVerticesSet.Clear(); + // add all the currently selected vertices to it with a union. + SelectedVerticesSet.UnionWith(SelectedVertices); + } + + + public void EditColliders(List colliders, bool remove = true) + { + SelectVerticesFromColliders(colliders); + if (remove) + { + RemoveSelectedColliders(); + } + ColliderSelectEnabled = false; + VertexSelectEnabled = true; + } + + /// + /// Converts a list of colliders to EasyColliderVertex's and selects them. + /// + /// + public void SelectVerticesFromColliders(List colliders) + { + EasyColliderCreator ecc = new EasyColliderCreator(); + List worldVertices = ecc.GetWorldVertsForColliders(colliders); + HashSet worldVerticesSet = new HashSet(); + worldVerticesSet.UnionWith(worldVertices); + + + HashSet newSelected = new HashSet(); + Vector3[] verts; + foreach (MeshFilter mf in _MeshFilters) + { + if (mf == null) continue; + verts = mf.sharedMesh.vertices; + Transform tra = mf.transform; + foreach (Vector3 p in verts) + { + Vector3 wp = tra.TransformPoint(p); + if (worldVerticesSet.Contains(wp)) + { + worldVerticesSet.Remove(wp); + newSelected.Add(new EasyColliderVertex(tra, p)); + } + } + } + + Transform t = SelectedGameObject.transform; + + foreach (Vector3 v in worldVerticesSet) + { + EasyColliderVertex ecv = new EasyColliderVertex(t, t.InverseTransformPoint(v)); + newSelected.Add(ecv); + } + SelectVertices(newSelected, false); + } + + /// + /// Selects or deselects a vertex. Returns true if selected, false if deselected. + /// + /// Vertex to select + /// True if selected, false if deselected. + public bool SelectVertex(EasyColliderVertex ecv, bool isVertex) + { + Vector3 normal = Vector3.zero; + int count = 0; + // calculate smoothed normal. + foreach (MeshFilter mf in MeshFilters) + { + if (mf == null || mf.transform != ecv.T || mf.sharedMesh == null) continue; + Vector3[] vertices = mf.sharedMesh.vertices; + Vector3[] normals = mf.sharedMesh.normals; + for (int i = 0; i < vertices.Length; i++) + { + if (vertices[i] == ecv.LocalPosition) + { + normal += normals[i]; + count++; + } + } + } + ecv.Normal = normal.normalized; + + if (SelectedVerticesSet.Remove(ecv)) + { + // Debug.Log("Removed."); + if (!isVertex) + { + SelectedNonVerticesSet.Remove(ecv); + } + SelectedVertices.Remove(ecv); + return false; + } + else + { + // Debug.Log("Not removed."); + LastSelectedVertices = new List(); + if (!isVertex) + { + SelectedNonVerticesSet.Add(ecv); + } + SelectedVerticesSet.Add(ecv); + SelectedVertices.Add(ecv); + LastSelectedVertices.Add(ecv); + return true; + } + } + + /// + /// Sets the density values on gizmos and compute if needed. + /// + /// Should density scaling be used? + public void SetDensityOnDisplayers(bool useDensityScale) + { + if (Compute != null) + { + Compute.DensityScale = DensityScale; + } + if (Gizmos != null) + { + Gizmos.DensityScale = Gizmos.UseFixedGizmoScale ? DensityScale * 7.5f : DensityScale; + } + } + + /// + /// Sets up the required components from the parent to the children (if children are enabled) + /// This includes rigidbodies, colliders, and mesh colliders. + /// + /// + /// + void SetRequiredComponentsFrom(GameObject parent, List meshFilters) + { + if (parent == null) return; + Rigidbody[] rigidbodies; + + // get either parent + children or just parent rigidbodies & colliders. + if (IncludeChildMeshes) + { + rigidbodies = parent.GetComponentsInChildren(); + } + else + { + rigidbodies = parent.GetComponents(); + } + // make sure rigidbodies are set to kinematic for raycasting + foreach (Rigidbody rb in rigidbodies) + { + if (!rb.isKinematic && !NonKinematicRigidbodies.Contains(rb)) + { + Undo.RegisterCompleteObjectUndo(rb, "change isKinmatic"); + rb.isKinematic = true; + NonKinematicRigidbodies.Add(rb); + } + } + + + // Add a mesh collider for every mesh filter. + foreach (MeshFilter filter in meshFilters) + { + if (filter != null) + { + MeshCollider mc = filter.GetComponent(); + + if (mc != null && mc.enabled && mc.sharedMesh == filter.sharedMesh) + { + RaycastableColliders.Add(mc); + continue; + } // don't add a mesh collider if it exists and is the correct mesh. + MeshCollider collider = Undo.AddComponent(filter.gameObject); + AddedInstanceIDs.Add(GetIDFor(collider)); + RaycastableColliders.Add(collider); + } + } + } + + void BakeSkinnedMesh(SkinnedMeshRenderer skinnedMesh, Mesh m) + { + // neither of these work properly 100% of the time, so need to fix skinned mesh baking manually at some point. + // #if (UNITY_2020_2_OR_NEWER) + // skinnedMesh.BakeMesh(m, true); + // #else + skinnedMesh.BakeMesh(m); + // #endif + } + + + + /// + /// Creates a mesh filter and bakes a mesh for a skinned mesh renderer. + /// + /// Skinned mesh renderer to create the mesh filter for. + /// The mesh filter that was created annd baked. + MeshFilter SetupFilterForSkinnedMesh(SkinnedMeshRenderer smr) + { + // Add a mesh filter and collider to the skinned mesh renderer while we select vertices. + MeshFilter filter = smr.GetComponent(); + //prevents duplication of un-needed mesh filters. + if (smr.transform.childCount > 0) + { + for (int i = 0; i < smr.transform.childCount; i++) + { + if (smr.transform.GetChild(i).name == "Scaled Mesh Filter (Temporary)") + { + filter = smr.transform.GetChild(i).GetComponent(); + if (filter != null) + { + break; + } + } + } + } + if (filter == null) + { + // if (smr.transform.localScale != Vector3.one) + // { + GameObject filterHolder = new GameObject("Scaled Mesh Filter (Temporary)"); + filterHolder.transform.parent = smr.transform; + filterHolder.transform.localPosition = Vector3.zero; + filterHolder.transform.localRotation = Quaternion.identity; + AddedInstanceIDs.Add(GetIDFor(filterHolder)); + Undo.RegisterCreatedObjectUndo(filterHolder, "Create MeshFilter"); + filter = Undo.AddComponent(filterHolder); + // } + // else + // { + // filter = Undo.AddComponent(smr.gameObject); + // AddedInstanceIDs.Add(filter.GetInstanceID()); + // } + } + if (filter != null) + { + if (!_SkinnedMeshFilterPairs.ContainsKey(smr)) + { + _SkinnedMeshFilterPairs.Add(smr, filter); + AddSkinnedMeshBoneTransforms(smr); + } + AddedInstanceIDs.Add(GetIDFor(filter)); + + + // Create a new mesh, so we prevent null refs by setting either the collider or filter's shared mesh. + Mesh mesh = new Mesh(); + // Bake the skinned mesh to the mesh, otherwise you can have offset colliders/filters which aren't correctly located. + // smr.BakeMesh(mesh); + BakeSkinnedMesh(smr, mesh); + + // Set the shared mesh's to that mesh. + filter.sharedMesh = mesh; + + // filter.sharedMesh = smr.sharedMesh; + // AddedComponentIDs.Add(filter.GetInstanceID()); + // reset scale to what it was + } + return filter; + } + + + private void TryDestroyObject(GameObject go, bool undo = true) + { +#if (UNITY_2018_3_OR_NEWER) + if (!PrefabUtility.IsPartOfAnyPrefab(go)) + { + if (undo) + { + Undo.DestroyObjectImmediate(go); + } + else + { + DestroyImmediate(go); + } + } +#else + if (undo) + { + Undo.DestroyObjectImmediate(go); + } + else + { + DestroyImmediate(go); + } +#endif + } + + private void TryDestroyComponent(Component component) + { + if (component != null) + { + Undo.DestroyObjectImmediate(component); + } + } + + /// + /// Checks added instance IDs and checks if they are mesh filters, also checks if any meshfilters have been deleted + /// Re-adds lost meshfilters from Undo-Redos + /// + public void VerifyMeshFiltersOnUndoRedo() + { + // fixes bug on lost mesh filter with skinned mesh renderer + foreach (var id in AddedInstanceIDs) + { + Object o = GetObjectForID(id); + if (o == null) { continue; } + else + { + if (o is MeshFilter) + { + MeshFilters.Add(o as MeshFilter); + } + } + } + // fix for losing a mesh filter when child meshes are enabled + // one is deleted, an operation is done, and undo-redo is done to the point of pre-deletion. + // (making sure the count is the same prevents the mesh filter from being permanently lost) + bool hasNullMeshFilter = false; + foreach (MeshFilter mf in MeshFilters) + { + if (mf == null) + { + hasNullMeshFilter = true; + } + } + if (hasNullMeshFilter) + { + List mfs = GetMeshFilters(SelectedGameObject); + if (mfs != null && mfs.Count == MeshFilters.Count) + { + MeshFilters = mfs; + } + } + } + + public void MergeSelectedColliders(CREATE_COLLIDER_TYPE mergeTo, bool removeMergedColliders) + { + EasyColliderCreator ecc = new EasyColliderCreator(); + Collider createdCollider = ecc.MergeColliders(SelectedColliders, mergeTo, GetProperties()); + if (removeMergedColliders) + { + RemoveSelectedColliders(); + } + if (createdCollider != null) + { + DisableCreatedCollider(createdCollider); + } + SelectedColliders.Clear(); + } + + + public bool SetAttachToOnBoneChange = true; + public bool CleanVerticesOnBoneChange = false; + public float SelectedBoneWeight = 0.5f; + public Transform LastSelectedBone; + public List BoneTransforms = new List(); + + + [System.Serializable] + /// + /// lets us serialize the vertices selected per bone. + /// + private class SerializableBoneVertexList + { + public List SelectedVertices = new List(); + } + + + [SerializeField] List _selectedBones = new List(); + [SerializeField] List _selectedBoneVertices = new List(); + Dictionary _SkinnedMeshFilterPairs = new Dictionary(); + + void AddSkinnedMeshBoneTransforms(SkinnedMeshRenderer renderer) + { + // fix issue if renderer does not actually have a mesh assigned. + if (renderer.sharedMesh == null) return; + HashSet validBoneIndexes = new HashSet(); +#if UNITY_2019_1_OR_NEWER + Transform transform = renderer.transform; + Vector3[] vertices = renderer.sharedMesh.vertices; + Unity.Collections.NativeArray weights = renderer.sharedMesh.GetAllBoneWeights(); + Unity.Collections.NativeArray bonesPerVertex = renderer.sharedMesh.GetBonesPerVertex(); + int boneWeightIndex = 0; + for (int vertIndex = 0; vertIndex < vertices.Length; vertIndex++) + { + if (vertIndex <= bonesPerVertex.Length) + { + int numBonesForVertex = bonesPerVertex[vertIndex]; + for (int i = 0; i < numBonesForVertex; i++) + { + if (boneWeightIndex <= weights.Length) + { + BoneWeight1 item = weights[boneWeightIndex]; + if (item.boneIndex >= 0 && item.weight >= 0) + { + validBoneIndexes.Add(item.boneIndex); + } + boneWeightIndex++; + } + else { break; } + } + } + else { break; } + } +#else + BoneWeight[] weights = renderer.sharedMesh.boneWeights; + for (int i = 0; i < weights.Length; i++) + { + BoneWeight item = weights[i]; + if (item.boneIndex0 >= 0 && item.weight0 >= 0) + { + validBoneIndexes.Add(item.boneIndex0); + } + if (item.boneIndex1 >= 0 && item.weight1 >= 0) + { + validBoneIndexes.Add(item.boneIndex1); + } + if (item.boneIndex2 >= 0 && item.weight2 >= 0) + { + validBoneIndexes.Add(item.boneIndex2); + } + if (item.boneIndex3 >= 0 && item.weight3 >= 0) + { + validBoneIndexes.Add(item.boneIndex3); + } + } +#endif + List bonesWithAtLeastOneWeightedVertex = new List(); + // add them in order. + List validBoneIndexList = validBoneIndexes.ToList(); + validBoneIndexList.Sort(); + if (renderer.bones != null && renderer.bones.Length > 0) + { + foreach (var index in validBoneIndexList) + { + // occurs if a smr is optimized and bones are hidden. + if (index >= 0 && index < renderer.bones.Length) + { + bonesWithAtLeastOneWeightedVertex.Add(renderer.bones[index]); + } + } + } + BoneTransforms.AddRange(bonesWithAtLeastOneWeightedVertex); + } + + public void ClearVerticesForBone(Transform bone) + { + int boneIndex = _selectedBones.IndexOf(bone); + if (boneIndex >= 0) + { + var selectedVertsForBone = _selectedBoneVertices[boneIndex].SelectedVertices; + HashSet selectedVertSet = new HashSet(selectedVertsForBone); + // because they can technically be cleared already, so clearing and invert would break things. + selectedVertSet.IntersectWith(_SelectedVerticesSet); + SelectVertices(selectedVertSet, true); + _selectedBones.RemoveAt(boneIndex); + _selectedBoneVertices.RemoveAt(boneIndex); + } + } + + + public void SelectVerticesForBone(Transform bone, float weightCutoff) + { + LastSelectedBone = bone; + SkinnedMeshRenderer[] smrs = SelectedGameObject.GetComponentsInChildren(); + SkinnedMeshRenderer smr = null; + foreach (var item in smrs) + { + if (item.bones.Contains(bone)) + { + smr = item; + break; + } + } + if (smr == null) return; + Transform[] bones = smr.bones; + int boneIndex = -1; + for (int i = 0; i < bones.Length; i++) + { + if (bones[i] == bone) + { + boneIndex = i; + break; + } + } + if (_SkinnedMeshFilterPairs.ContainsKey(smr)) + { + Transform transform = _SkinnedMeshFilterPairs[smr].transform; + Vector3[] vertices = _SkinnedMeshFilterPairs[smr].sharedMesh.vertices; + + HashSet verts = new HashSet(); +#if UNITY_2019_1_OR_NEWER + Unity.Collections.NativeArray weights = smr.sharedMesh.GetAllBoneWeights(); + Unity.Collections.NativeArray bonesPerVertex = smr.sharedMesh.GetBonesPerVertex(); + int boneWeightIndex = 0; + for (int vertIndex = 0; vertIndex < vertices.Length; vertIndex++) + { + int numBonesForVertex = bonesPerVertex[vertIndex]; + for (int i = 0; i < numBonesForVertex; i++) + { + BoneWeight1 item = weights[boneWeightIndex]; + if (item.boneIndex == boneIndex && item.weight >= weightCutoff) + { + verts.Add(new EasyColliderVertex(transform, vertices[vertIndex])); + } + boneWeightIndex++; + } + } +#else + BoneWeight[] weights = smr.sharedMesh.boneWeights; + for (int i = 0; i < weights.Length; i++) + { + BoneWeight item = weights[i]; + if (item.boneIndex0 == boneIndex && item.weight0 >= weightCutoff) + { + verts.Add(new EasyColliderVertex(transform, vertices[i])); + } + if (item.boneIndex1 == boneIndex && item.weight1 >= weightCutoff) + { + verts.Add(new EasyColliderVertex(transform, vertices[i])); + } + if (item.boneIndex2 == boneIndex && item.weight2 >= weightCutoff) + { + verts.Add(new EasyColliderVertex(transform, vertices[i])); + } + if (item.boneIndex3 == boneIndex && item.weight3 >= weightCutoff) + { + verts.Add(new EasyColliderVertex(transform, vertices[i])); + } + } +#endif + // deselect any previously selected vertices for that bone, then reselect. + int selectedBoneIndex = _selectedBones.IndexOf(bone); + HashSet vertsAlreadySelectedForBone = new HashSet(); + if (selectedBoneIndex >= 0) + { + // we've selected this bone already, so we want the vertices we selected for it + // and then we need only the ones that are still selected with the intersect + // then we select them to deselect the currently selected ones, so that + // the new selection only selects the appropriate weight + vertsAlreadySelectedForBone.UnionWith(_selectedBoneVertices[selectedBoneIndex].SelectedVertices); + _selectedBoneVertices[selectedBoneIndex].SelectedVertices = verts.ToList(); + } + else + { + _selectedBones.Add(bone); + _selectedBoneVertices.Add(new SerializableBoneVertexList() { SelectedVertices = verts.ToList() }); + // we dont want to end up deselecting vertices that are already selected, but we do want to store them as verts for this bone + // so we will be selecting the ones already selected to deselect them, so they can then be selected + vertsAlreadySelectedForBone.UnionWith(verts); + } + if (vertsAlreadySelectedForBone.Count > 0) + { + vertsAlreadySelectedForBone.IntersectWith(_SelectedVerticesSet); + SelectVertices(vertsAlreadySelectedForBone, true); + } + SelectVertices(verts, true); + // Debug.Log("Verts count:" + verts.Count + " Selected now:" + SelectedVerticesSet.Count); + } + + } + + } +} +#endif \ No newline at end of file diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderEditor.cs.meta b/Assets/EasyColliderEditor/Scripts/EasyColliderEditor.cs.meta new file mode 100644 index 00000000..474c2816 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderEditor.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 8765d9c9fa7824d4da8e37f5cc1913fa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/EasyColliderEditor.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderEnums.cs b/Assets/EasyColliderEditor/Scripts/EasyColliderEnums.cs new file mode 100644 index 00000000..822f698a --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderEnums.cs @@ -0,0 +1,158 @@ +namespace ECE +{ + public enum NORMAL_OFFSET + { + Out, + In, + Both, + } + + /// + /// Capsule method to use when creating a capsule + /// + public enum CAPSULE_COLLIDER_METHOD + { + BestFit, + MinMax, + MinMaxPlusRadius, + MinMaxPlusDiameter, + } + + /// + /// Type of collider to create + /// + public enum CREATE_COLLIDER_TYPE + { + BOX, + ROTATED_BOX, + SPHERE, + CAPSULE, + ROTATED_CAPSULE, + CONVEX_MESH, + CYLINDER, + } + + /// + /// Orientation of collider + /// + public enum COLLIDER_ORIENTATION + { + NORMAL, + ROTATED, + } + + public enum CYLINDER_ORIENTATION + { + Automatic, + LocalX, + LocalY, + LocalZ + } + + /// + /// Enum for spheres ot cubes when drawing gizmos + /// + public enum GIZMO_TYPE + { + CUBE, + SPHERE, + } + + /// + /// enum for Shaders or Gizmos to draw vertices with. + /// + public enum RENDER_POINT_TYPE + { + SHADER, + GIZMOS, + } + + /// + /// Collider type to use when automatically generating colliders along a bone chain for a skinned mesh renderer. + /// + public enum SKINNED_MESH_COLLIDER_TYPE + { + Box, + Capsule, + Sphere, + Convex_Mesh, + } + + public enum SKINNED_MESH_DEPENETRATE_ORDER + { + InOrder, + Reverse, + InsideOut, + OutsideIn, + } + + /// + /// Sphere method to use when creating a sphere + /// + public enum SPHERE_COLLIDER_METHOD + { + BestFit, + Distance, + MinMax, + } + + public enum VHACD_CONVERSION + { + None, + Boxes, + Spheres, + Capsules, + } + + public enum MESH_COLLIDER_METHOD + { + QuickHull, + MessyHull + } + + + public enum VERTEX_SNAP_METHOD + { + Add, + Remove, + Both, + } + + /// + /// Method used to attach mesh colliders to the attach to object. + /// + public enum VHACD_RESULT_METHOD + { + AttachTo, + ChildObject, + IndividualChildObjects + } + + /// + /// Current window tab selected in the editor window. + /// + public enum ECE_WINDOW_TAB + { + None = -1, + Creation = 0, + Editing = 1, + VHACD = 2, + AutoSkinned = 3, + } + + public enum COLLIDER_HOLDER + { + Default, + Once, + Always + } + + public enum CONVEX_HULL_SAVE_METHOD + { + Prefab, + Mesh, + PrefabMesh, + MeshPrefab, + Folder, + } +} diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderEnums.cs.meta b/Assets/EasyColliderEditor/Scripts/EasyColliderEnums.cs.meta new file mode 100644 index 00000000..42af1b6b --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderEnums.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: d3013554ca157424db48970772ea240c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/EasyColliderEnums.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderGizmos.cs b/Assets/EasyColliderEditor/Scripts/EasyColliderGizmos.cs new file mode 100644 index 00000000..18bec7b2 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderGizmos.cs @@ -0,0 +1,185 @@ +#if (UNITY_EDITOR) +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +namespace ECE +{ + /// + /// Used to draw gizmos for selected / hovered vertices + /// Gizmos draw significantly faster than handles. + /// + + [System.Serializable] + public class EasyColliderGizmos : MonoBehaviour, ISerializationCallbackReceiver + { + + #region preference settings + private EasyColliderPreferences ECEPreferences + { + get { return EasyColliderPreferences.Preferences; } + } + public float CommonScale { get { return ECEPreferences.CommonScalingMultiplier; } } + public float DefaultScale { get { return ECEPreferences.DefaultScale; } } + public bool DisplayAllVertices { get { return ECEPreferences.DisplayAllVertices; } } + public Color DisplayVertexColor { get { return ECEPreferences.DisplayVerticesColour; } } + public GIZMO_TYPE GizmoType { get { return ECEPreferences.GizmoType; } } + public Color HoveredVertexColor { get { return ECEPreferences.HoverVertColour; } } + public Color OverlapVertexColor { get { return ECEPreferences.OverlapSelectedVertColour; } } + public Color SelectedVertexColor { get { return ECEPreferences.SelectedVertColour; } } + public bool UseFixedGizmoScale { get { return ECEPreferences.UseFixedGizmoScale; } } + #endregion + + /// + /// calculated density scale if use density scale is enabled. + /// + public float DensityScale = 0.0f; + + /// + /// List of all valid vertex positions in world space + /// + public HashSet DisplayVertexPositions = new HashSet(); + + /// + /// Should gizmos be drawn + /// + public bool DrawGizmos = true; + + /// + /// Set of hovered vertices in world space + /// + public HashSet HoveredVertexPositions = new HashSet(); + + /// + /// Set of selected vertices in world space + /// + public HashSet SelectedVertexPositions = new HashSet(); + + float GetScale() + { + // *10 makes it more equivalent to how the shader is drawn. + float scale = DefaultScale * CommonScale * 10; + if (scale <= 0.0f) + { + scale = ECEPreferences.DefaultScale * 10; + } + return scale; + } + + void OnDrawGizmos() + { + if (DrawGizmos) + { + // Keep track of gizmos color to reset at end + Color original = Gizmos.color; + // Selected vertices. + // scale for spheres. + float scale = GetScale(); + // size for cubes + Vector3 size = Vector3.one * scale; + // default scaling of 1.0f + float handleSize = 1.0f; + + // Display all vertices. + if (DisplayAllVertices) + { + Gizmos.color = DisplayVertexColor; + + foreach (Vector3 vert in DisplayVertexPositions) + { + if (UseFixedGizmoScale) + { + handleSize = HandleUtility.GetHandleSize(vert); + } + DrawAGizmo(vert, size * handleSize, scale * handleSize, GizmoType); + } + } + + // Selected vertices + Gizmos.color = SelectedVertexColor; + foreach (Vector3 vert in SelectedVertexPositions) + { + if (UseFixedGizmoScale) + { + handleSize = HandleUtility.GetHandleSize(vert); + } + DrawAGizmo(vert, size * handleSize, scale * handleSize, GizmoType); + } + + // Hover vertices. + Gizmos.color = HoveredVertexColor; + foreach (Vector3 vert in HoveredVertexPositions) + { + if (SelectedVertexPositions.Contains(vert)) + { + if (UseFixedGizmoScale) + { + handleSize = HandleUtility.GetHandleSize(vert); + } + Gizmos.color = OverlapVertexColor; + DrawAGizmo(vert, size * handleSize, scale * handleSize, GizmoType); + } + else + { + if (UseFixedGizmoScale) + { + handleSize = HandleUtility.GetHandleSize(vert); + } + Gizmos.color = HoveredVertexColor; + DrawAGizmo(vert, size * handleSize, scale * handleSize, GizmoType); + } + } + Gizmos.color = original; + } + } + + /// + /// Draws a gizmo of type at position at size or scale. + /// + /// World position to draw at + /// Size of cube to draw + /// Radius of sphere to draw + /// Sphere or Cubes? + private void DrawAGizmo(Vector3 position, Vector3 size, float scale, GIZMO_TYPE gizmoType) + { + switch (gizmoType) + { + case GIZMO_TYPE.SPHERE: + Gizmos.DrawSphere(position, scale / 2); + break; + case GIZMO_TYPE.CUBE: + Gizmos.DrawCube(position, size); + break; + } + } + + /// + /// Sets the set of selected vertices from a list of selected world vertices + /// + /// List of world vertex positions that are selected + public void SetSelectedVertices(List worldVertices) + { + SelectedVertexPositions.Clear(); + SelectedVertexPositions.UnionWith(worldVertices); + } + + + + public List SerializedDisplayVertexPositions = new List(); + public List SerializedHoveredVertexPositions = new List(); + public List SerializedSelectedVertexPositions = new List(); + public void OnBeforeSerialize() + { + SerializedDisplayVertexPositions.AddRange(DisplayVertexPositions); + SerializedHoveredVertexPositions.AddRange(HoveredVertexPositions); + SerializedSelectedVertexPositions.AddRange(SelectedVertexPositions); + } + + public void OnAfterDeserialize() + { + DisplayVertexPositions.UnionWith(SerializedDisplayVertexPositions); + HoveredVertexPositions.UnionWith(SerializedHoveredVertexPositions); + SelectedVertexPositions.UnionWith(SerializedSelectedVertexPositions); + } + } +} +#endif \ No newline at end of file diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderGizmos.cs.meta b/Assets/EasyColliderEditor/Scripts/EasyColliderGizmos.cs.meta new file mode 100644 index 00000000..1f8fbb34 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderGizmos.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 7302dd3862f934949a13c03008aba827 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/EasyColliderGizmos.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderPostProccessor.cs b/Assets/EasyColliderEditor/Scripts/EasyColliderPostProccessor.cs new file mode 100644 index 00000000..5ca91309 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderPostProccessor.cs @@ -0,0 +1,55 @@ +using UnityEngine; + +namespace ECE +{ + public class EasyColliderPostProccessor : IEasyColliderPostProcessor + { + // The default implementation does nothing. + // Can be used to add your own code if you want to do something specifically to each type of collider. + // If you're creating colliders at runtime with EasyColliderCreator, you can also set the post processor through code, + // if you don't it will by default use this implementation + + // NOTE: this happens for every single collider created in this asset / created by EasyColliderCreator + // Vertex selected created colliders, merged colliders, auto skinned colliders, vhacd colliders all go through these methods. + + /// + /// post processes a box collider, properties orientation indiciates if it's a rotated collider. + /// + /// + /// + public void PostProcessCollider(BoxCollider boxCollider, EasyColliderProperties properties) + { + // in here you'll get passed a completely generated box collider, and can do whatever you want with it. + } + + /// + /// post processes a capsule collider, properties orientation indiciates if it's a rotated collider. + /// + /// + /// + public void PostProcessCollider(CapsuleCollider capsuleCollider, EasyColliderProperties properties) + { + // in here you'll get passed a completely generated capsule collider, and can do whatever you want with it. + } + + /// + /// post processes a sphere collider + /// + /// + /// + public void PostProcessCollider(SphereCollider sphereCollider, EasyColliderProperties properties) + { + // in here you'll get passed a completely generated sphere collider, and can do whatever you want with it. + } + + /// + /// post processes a mesh collider. cylinder colliders are mesh colliders as well. + /// + /// + /// + public void PostProcessCollider(MeshCollider meshCollider, EasyColliderProperties properties) + { + // in here you'll get passed a completely generated mesh collider, and can do whatever you want with it. + } + } +} \ No newline at end of file diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderPostProccessor.cs.meta b/Assets/EasyColliderEditor/Scripts/EasyColliderPostProccessor.cs.meta new file mode 100644 index 00000000..cb1cdcee --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderPostProccessor.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: c4635610932a87342a4bf2e41e9ab050 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/EasyColliderPostProccessor.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderPreferences.asset b/Assets/EasyColliderEditor/Scripts/EasyColliderPreferences.asset new file mode 100644 index 00000000..8e694a7c --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderPreferences.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84cb3ff1b631aeaaf96486601277192328e9e99caec6686f10b68e218efd9af0 +size 3446 diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderPreferences.asset.meta b/Assets/EasyColliderEditor/Scripts/EasyColliderPreferences.asset.meta new file mode 100644 index 00000000..00d2d16c --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderPreferences.asset.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: 99d3a08e9dc280246aa61e99c233b0c0 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/EasyColliderPreferences.asset + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderPreferences.cs b/Assets/EasyColliderEditor/Scripts/EasyColliderPreferences.cs new file mode 100644 index 00000000..0b328672 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderPreferences.cs @@ -0,0 +1,615 @@ +#if (UNITY_EDITOR) +using UnityEngine; +using UnityEditor; +using System.IO; +namespace ECE +{ + [System.Serializable] + public class EasyColliderPreferences : ScriptableObject + { + /// + /// Currently set vhacd parameters. + /// + [SerializeField] private VHACDParameters _VHACDParameters; + public VHACDParameters VHACDParameters + { + get + { + if (_VHACDParameters == null) + { + _VHACDParameters = new VHACDParameters(); + } + return _VHACDParameters; + } + set { _VHACDParameters = value; } + } + + + /// + /// Float to convert to vhacd parameters resolution using 2^Value for UI Slider when advanced parameters is expanded + /// + [SerializeField] public float VHACDResFloat = 12.97f; + + /// + /// Resets vhacd parameters to default. + /// + public void VHACDSetDefaultParameters() + { + VHACDParameters = new VHACDParameters(); + } + /// + /// When enabled, selecting objects in the background of the scene view can be done with a single click even when an object is currently selected for collider creation. + /// + [SerializeField] public bool AllowBackgroundSelection; + + /// + /// when enabled, allows convex hulls used in mesh colliders to be saved in Packages folder. + /// THESE FILES ARE NOT SHARED WITH JUST THE PROJECT. MUST BE HANDLED BY YOUR TEAM. MUST ALSO SHARE SOURCE PACKAGES FOLDERS + /// + [SerializeField] public bool AllowSavingConvexHullsInPackages; + + /// + /// When entering prefab mode, should we automatically select the root object? + /// + [SerializeField] public bool AutoSelectOnPrefabOpen; + + /// + /// Auto include child skinned meshes + /// + [SerializeField] public bool AutoIncludeChildSkinnedMeshes; + + /// + /// Should we try to reduce the number of vertices in auto-skinned calculations for convex mesh colliders if the result ends up trying to create a collider that has >=256 triangles. + /// + [SerializeField] public bool AutoSkinnedForce256Triangles; + + /// + /// The minimum weight for a vertex to be included in a bone's calculations. + /// + [SerializeField] public float AutoSkinnedMinBoneWeight; + + /// + /// The angle of mis-alignedment above which we should create a better aligned child transform to hold colliders for skinned meshes. + /// + [SerializeField] public float AutoSkinnedMinRealignAngle; + + /// + /// Should we allow transforms to be created that would better align the collider with the mesh than a mis-aligned bone? + /// + [SerializeField] public bool AutoSkinnedAllowRealign; + + /// + /// Type of collider to use when auto generating skinned mesh colliders along a bone chain. + /// + [SerializeField] public SKINNED_MESH_COLLIDER_TYPE AutoSkinnedColliderType = SKINNED_MESH_COLLIDER_TYPE.Box; + + /// + /// Should we be attempting to compute penetration and depenetrate the colliders on the skinned mesh? + /// + [SerializeField] public bool AutoSkinnedDepenetrate; + + /// + /// Are we displaying with indentation? + /// + [SerializeField] public bool AutoSkinnedIndents = true; + + /// + /// number of times to run depenetration methods before stopping. + /// + [SerializeField] public int AutoSkinnedIterativeDepenetrationCount = 15; + + /// + /// Are we displaying with paired bones? + /// + [SerializeField] public bool AutoSkinnedPairing = true; + + /// + /// Are we using per-bone settings + /// + [SerializeField] public bool AutoSkinnedPerBoneSettings; + + /// + /// Amount of collider shrinking (along the various shift's) to do before trying to shift depenetrate. + /// + [SerializeField] public float AutoSkinnedShrinkAmount = 0.5f; + + [Tooltip("Enables using bone position distances during pairing of bones. (AutoSkinnedPairedDistanceDelta controls the distance when enabled.")] + [SerializeField] public bool AutoSkinnedUseDistanceDeltaPairing = true; + + [Tooltip("Max distance difference between possible bone pairs to still be considered as a pair. Checked along with length of child bone-chain.")] + [SerializeField] public float AutoSkinnedPairedDistanceDelta = 0.01f; + + + /// + /// The way we should sort the bone list in auto-skinned. + /// + [SerializeField] public SKINNED_MESH_DEPENETRATE_ORDER AutoSkinnedDepenetrateOrder; + + /// + /// Key to hold before box selection to only add vertices in the box. + /// + [SerializeField] public KeyCode BoxSelectPlusKey; + + /// + /// Key to hold before box selection to only remove vertices in the box. + /// + [SerializeField] public KeyCode BoxSelectMinusKey; + + /// + /// Capsule collider generation method to use when creating a capsule collider. + /// + [SerializeField] public CAPSULE_COLLIDER_METHOD CapsuleColliderMethod; + + /// + /// when enabled, when generating multiple convex mesh colliders with vhacd, they are all combined and stored in a single asset file. + /// + [SerializeField] public bool CombinedVHACDColliders; + + /// + /// A helpful common multiplier for all scales when using any scaling method. + /// + [SerializeField] public float CommonScalingMultiplier = 1.0f; + + /// + /// should created meshes used in convex mesh colliders be read/write enabled + /// + [SerializeField] public bool ConvexMeshReadWriteEnabled = true; + + [Tooltip("Default scale of the displayed boxes / gizmos around vertices. Combined with common scaling multiplier.")] + /// + /// Default scale of displayed vertices used along with the common scaling multiplier. + /// + [SerializeField] public float DefaultScale = 0.01f; + /// + /// scale for vertices that are hovered / will be added to the selected vertices + /// + [SerializeField] public float HoveredScaleMult = 1.0f; + /// + /// scale multiplier for vertices when using display all vertices + /// + [SerializeField] public float DisplayAllScaleMult = 1.0f; + /// + /// scale multiplier for vertices that will be removed from the selected vertices + /// + [SerializeField] public float OverlapScaleMult = 1.0f; + /// + /// scale multiplier for vertices that are currently selected. + /// + [SerializeField] public float SelectedScaleMult = 1.0f; + + /// + /// The method to use when creating a collider: if and how a gameobject should be made to hold the collider. + /// + [SerializeField] public COLLIDER_HOLDER ColliderHolder = COLLIDER_HOLDER.Default; + + /// + /// If true, puts rotated colliders on the same layer as the selected gameobject. + /// + [SerializeField] public bool CopyParentObjectLayer; + + /// + /// key to press to create from the current preview. + /// + [SerializeField] public KeyCode CreateFromPreviewKey; + + /// + /// For editor window tab tracking, maintains previously open tab. + /// + [SerializeField] public ECE_WINDOW_TAB CurrentWindowTab; + + /// + /// Should cylinder orientation field apply to capsules as well. + /// + [SerializeField] public bool CylinderAsCapsuleOrientation = false; + + /// + /// number of sides when creating a cylinder collider. + /// + [SerializeField] public int CylinderNumberOfSides = 16; + + /// + /// Method to use to decide which axis a cylinder should be oriented on. + /// + [SerializeField] public CYLINDER_ORIENTATION CylinderOrientation; + + /// + /// Offset in degrees when creating a cylinder. + /// + [SerializeField] public float CylinderRotationOffset = 0.0f; + + /// + /// Should tips be displayed? + /// + [SerializeField] public bool DisplayTips; + + /// + /// Should we display compute shader / gizmos on all the vertices? + /// + [SerializeField] public bool DisplayAllVertices; + + /// + /// Display vertices colour + /// + [SerializeField] public Color DisplayVerticesColour; + + [SerializeField] public bool EnableVertexToolsShortcuts = true; + + /// + /// Type of gizmos to use when drawing gizmos for vertices + /// + public GIZMO_TYPE GizmoType; + + /// + /// Hover vertices scaling colour + /// + [SerializeField] public Color HoverVertColour; + + /// + /// Number of points to generate around a rounded portion of a collider like sphere or capsules + /// + [SerializeField] public int MergeCollidersRoundnessAccuracy = 10; + + + /// + /// Method to use when generating mesh colliders + /// + [SerializeField] public MESH_COLLIDER_METHOD MeshColliderMethod; + + /// + /// Overlapped vertice scaling colour + /// + [SerializeField] public Color OverlapSelectedVertColour; + + /// + /// Key used to select points (any point on a mesh that isn't a vertex) + /// + [SerializeField] public KeyCode PointSelectKeyCode; + + + [SerializeField] public bool PopupDialogOnFinish; + + /// + /// Collider type we want to preview + /// + [SerializeField] public CREATE_COLLIDER_TYPE PreviewColliderType; + + /// + /// Color of lines to draw previewed colliders with. + /// + [SerializeField] public Color PreviewDrawColor; + + /// + /// Are previews enabled? + /// + [SerializeField] public bool PreviewEnabled; + + /// + /// Raycast delay time, ie only check / select at increments of this time. + /// + [SerializeField] public float RaycastDelayTime; + + /// + /// Render point method + /// + [SerializeField] public RENDER_POINT_TYPE RenderPointType; + + /// + /// Should colliders that are merged together be removed after merging is completed? + /// + [SerializeField] public bool RemoveMergedColliders; + + /// + /// Float that controls the amount an output collider's size should be increased or decreased, essentially scaling the output. + /// + [SerializeField] public float ShrinkGrow; + + /// + /// Should the rotated colliders pivot be created at the center of the points, or at 0? + /// + [SerializeField] public bool RotatedColliderPivotAtCenter; + + /// + /// Settings of the current rotate and duplicate section in collider creation. + /// + [SerializeField] public EasyColliderRotateDuplicate rotatedDupeSettings; + + /// + /// When true, meshes created from creating convex hulls are saved as assets. + /// + [SerializeField] public bool SaveConvexHullAsAsset; + + /// + /// Specifies how to search for a location to save the convex hull .asset files. + /// + [SerializeField] public CONVEX_HULL_SAVE_METHOD ConvexHullSaveMethod = CONVEX_HULL_SAVE_METHOD.PrefabMesh; + + /// + /// if SaveConvexHullMeshAtSelected is false, saves at the path specified. + /// + [SerializeField] public string SaveConvexHullPath; + + /// + /// Provides a subfolder to save colliders in an asset path when saving near the mesh + /// IE: if the mesh/prefab is found in an Environments/Meshes folder, an Environments/Meshes/ConvexHulls folder would be created to store the collision meshes + /// if using anything other than CONVEX_HULL_SAVE_METHOD.Folder + /// + [SerializeField] public string SaveConvexHullSubFolder; + + /// + /// Suffix with which to save convex hulls. + /// + [SerializeField] public string SaveConvexHullSuffix; + + /// + /// Selected vertice scaling colour + /// + [SerializeField] public Color SelectedVertColour; + + [SerializeField] public KeyCode ShortcutInvert = KeyCode.I; + [SerializeField] public KeyCode ShortcutGrow = KeyCode.G; + [SerializeField] public KeyCode ShortcutGrowLast = KeyCode.L; + [SerializeField] public KeyCode ShortcutClear = KeyCode.C; + [SerializeField] public KeyCode ShortcutRing = KeyCode.R; + + /// + /// Should the number of selected vertices be displayed in the ui? + /// + [SerializeField] public bool ShowSelectedVertexCount; + + /// + /// Sphere method to use when creating a sphere collider. + /// + public SPHERE_COLLIDER_METHOD SphereColliderMethod; + + /// + /// Should HandleUtility.GetHandleSize be used when using gizmos to draw to keep gizmo size constant regardless of distance to camera? + /// + [SerializeField] public bool UseFixedGizmoScale; + + /// + /// Enables using left click to select vertices, and right click to select points. + /// + [SerializeField] public bool UseMouseClickSelection = true; + + [SerializeField] public NORMAL_OFFSET VertexNormalOffsetType = NORMAL_OFFSET.Both; + + /// + /// Amount to offset the vertex (in direction of it's averaged normal) + /// + [SerializeField] public float VertexNormalOffset = 0f; + + /// + /// Amount to inset the vertex (in opposite direction of it's averaged normal) + /// + [SerializeField] public float VertexNormalInset = 0f; + + /// + /// Method used when raycasting for closest vertices, add (only snap to unselected verts), remove (only snap to selected verts), both (default) + /// + [SerializeField] public VERTEX_SNAP_METHOD VertexSnapMethod; + + /// + /// Key used to select vertices. + /// + [SerializeField] public KeyCode VertSelectKeyCode; + + /// + /// Should we update the VHACD calculation and preview as parameters change? + /// + [SerializeField] public bool VHACDPreview; + + /// + /// Sets all values to default values. + /// + public void SetDefaultValues() + { + #region VHACD settings + VHACDParameters = new VHACDParameters(); + VHACDPreview = true; + #endregion + + #region other settings + AutoIncludeChildSkinnedMeshes = true; + AutoSelectOnPrefabOpen = false; + DisplayTips = true; + DisplayAllVertices = false; + PopupDialogOnFinish = false; + PreviewEnabled = true; + RaycastDelayTime = 0.1f; + ShowSelectedVertexCount = false; + rotatedDupeSettings = new EasyColliderRotateDuplicate(); + RotatedColliderPivotAtCenter = false; + CylinderAsCapsuleOrientation = false; + #endregion + + #region autoskinned preferences + AutoSkinnedMinBoneWeight = 0.5f; + AutoSkinnedDepenetrate = false; + AutoSkinnedIterativeDepenetrationCount = 15; + AutoSkinnedPairing = true; + AutoSkinnedIndents = true; + AutoSkinnedColliderType = SKINNED_MESH_COLLIDER_TYPE.Box; + AutoSkinnedShrinkAmount = 0.5f; + // I find outside in to give the best results generally. + AutoSkinnedDepenetrateOrder = SKINNED_MESH_DEPENETRATE_ORDER.OutsideIn; + AutoSkinnedForce256Triangles = true; + AutoSkinnedUseDistanceDeltaPairing = true; + AutoSkinnedPairedDistanceDelta = 0.01f; + #endregion + + #region inputs + // shifts do not work. + BoxSelectMinusKey = KeyCode.S; + BoxSelectPlusKey = KeyCode.A; + CreateFromPreviewKey = KeyCode.BackQuote; + PointSelectKeyCode = KeyCode.B; + UseMouseClickSelection = true; + VertSelectKeyCode = KeyCode.V; + + EnableVertexToolsShortcuts = true; + ShortcutInvert = KeyCode.I; + ShortcutGrow = KeyCode.G; + ShortcutGrowLast = KeyCode.L; + ShortcutClear = KeyCode.C; + ShortcutRing = KeyCode.R; + #endregion + + #region Collider Methods + CapsuleColliderMethod = CAPSULE_COLLIDER_METHOD.MinMax; + MeshColliderMethod = MESH_COLLIDER_METHOD.QuickHull; + PreviewColliderType = CREATE_COLLIDER_TYPE.BOX; + SphereColliderMethod = SPHERE_COLLIDER_METHOD.MinMax; + #endregion + + #region Collider Settings / Paramaters + ShrinkGrow = 1f; + MergeCollidersRoundnessAccuracy = 10; + VertexNormalOffset = 0f; + VertexNormalInset = 0f; + VertexNormalOffsetType = NORMAL_OFFSET.Both; + ColliderHolder = COLLIDER_HOLDER.Default; + CylinderNumberOfSides = 16; + CylinderOrientation = CYLINDER_ORIENTATION.Automatic; + CylinderRotationOffset = 0.0f; + MeshColliderMethod = MESH_COLLIDER_METHOD.QuickHull; + RemoveMergedColliders = true; + CopyParentObjectLayer = true; + #endregion + + #region drawing + CommonScalingMultiplier = 1.0f; + DefaultScale = 0.01f; + HoveredScaleMult = 1.0f; + DisplayAllScaleMult = 1.0f; + OverlapScaleMult = 1.0f; + SelectedScaleMult = 1.0f; + + DisplayVerticesColour = Color.blue; + GizmoType = GIZMO_TYPE.SPHERE; + HoverVertColour = Color.cyan; + OverlapSelectedVertColour = Color.red; + PreviewDrawColor = Color.cyan; + if (SystemInfo.graphicsShaderLevel < 45) + { + RenderPointType = RENDER_POINT_TYPE.GIZMOS; + } + else + { + RenderPointType = RENDER_POINT_TYPE.SHADER; + } + SelectedVertColour = Color.green; + UseFixedGizmoScale = true; + #endregion + + #region Save Convex Hull Settings + ConvexHullSaveMethod = CONVEX_HULL_SAVE_METHOD.PrefabMesh; + ResetDefaultSavePath(); + SaveConvexHullAsAsset = true; + SaveConvexHullSuffix = "_ConvexHull_"; + ConvexMeshReadWriteEnabled = true; + SaveConvexHullSubFolder = ""; + #endregion + } + + private static EasyColliderPreferences _Prefereneces; + public static EasyColliderPreferences Preferences + { + get + { + if (_Prefereneces == null) + { + // lazy load preferences when needed. + _Prefereneces = FindOrCreatePreferences(); + } + return _Prefereneces; + } + } + + private static EasyColliderPreferences FindOrCreatePreferences() + { + EasyColliderPreferences preferences; + string[] ecp = AssetDatabase.FindAssets("EasyColliderPreferences t:ScriptableObject"); + string assetPath = ""; + if (ecp.Length > 0) + { + assetPath = AssetDatabase.GUIDToAssetPath(ecp[0]); + if (ecp.Length > 1) + { + Debug.LogWarning("Easy Collider Editor has found multiple preferences files. Using the one located at " + assetPath); + } + preferences = AssetDatabase.LoadAssetAtPath(assetPath, typeof(EasyColliderPreferences)) as EasyColliderPreferences; + } + else + { + ecp = AssetDatabase.FindAssets("EasyColliderWindow t:script"); + if (ecp.Length > 0) + { + assetPath = AssetDatabase.GUIDToAssetPath(ecp[0]); + if (ecp.Length > 1) + { + Debug.LogWarning("Easy Collider Editor has found multiple preferences files. Using the one located at " + assetPath); + } + } + // preferences = AssetDatabase.LoadAssetAtPath(assetPath, typeof(EasyColliderPreferences)) as EasyColliderPreferences; + // Create a new preferences file. + + string prefPath = assetPath.Remove(assetPath.Length - 21) + "EasyColliderPreferences.asset"; + preferences = CreateInstance(); + preferences.SetDefaultValues(); + AssetDatabase.CreateAsset(preferences, prefPath); + AssetDatabase.SaveAssets(); + Debug.LogWarning("Easy Collider Editor did not find a preferences file, new preferences file created at " + prefPath); + } + return preferences; + } + + + static string defaultFolderName = "Convex Hulls"; + /// + /// Resets the default save path to the location of the preferences scriptable object. + /// + public bool ResetDefaultSavePath() + { + // path to this preferences object. + SaveConvexHullPath = AssetDatabase.GetAssetPath(MonoScript.FromScriptableObject(this)); + SaveConvexHullPath = SaveConvexHullPath.Remove(SaveConvexHullPath.LastIndexOf("/")) + "/"; + string rootPath = SaveConvexHullPath.Remove(SaveConvexHullPath.LastIndexOf("/Scripts/"), 9); + // if the preferences is in soemthing other than Assets (like packages, create a new folder at the root to hold convex hulls) + if (!SaveConvexHullPath.StartsWith("Assets")) + { + if (!AssetDatabase.IsValidFolder("Assets/Convex Hulls")) + { + AssetDatabase.CreateFolder("Assets", "Convex Hulls"); + AssetDatabase.Refresh(); + Debug.LogWarning("A folder has been created at: Assets/Convex Hulls to save convex mesh collider assets because Easy Collider exists in the packages folder. Folder to save assets in can be changed in Easy Collider Editor's preferences foldout."); + } + SaveConvexHullPath = "Assets/Convex Hulls/"; + return true; + } + // remove invalid characters. + SaveConvexHullPath = string.Join("", SaveConvexHullPath.Split(Path.GetInvalidPathChars())); + // try creating and saving a Convex Hull specific folder if it doesn't exist. + if (!AssetDatabase.IsValidFolder(rootPath + "/" + defaultFolderName)) + { + AssetDatabase.CreateFolder(rootPath, defaultFolderName); + AssetDatabase.Refresh(); + if (AssetDatabase.IsValidFolder(rootPath + "/" + defaultFolderName)) + { + SaveConvexHullPath = rootPath + "/" + defaultFolderName + "/"; + return true; + } + } + else + { + SaveConvexHullPath = rootPath + "/" + defaultFolderName + "/"; + } + return false; + } + + public int GetCreationValuesHashCode() + { + return 0; + } + } +} +#endif \ No newline at end of file diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderPreferences.cs.meta b/Assets/EasyColliderEditor/Scripts/EasyColliderPreferences.cs.meta new file mode 100644 index 00000000..79990234 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderPreferences.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: fb5e4193615d8464a82ea935f90494a0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/EasyColliderPreferences.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderPreviewer.cs b/Assets/EasyColliderEditor/Scripts/EasyColliderPreviewer.cs new file mode 100644 index 00000000..46aa590b --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderPreviewer.cs @@ -0,0 +1,714 @@ +#if (UNITY_EDITOR) +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +namespace ECE +{ + + /// + /// Scriptable object to preview colliders that would be created from the given points + /// + [System.Serializable] + public class EasyColliderPreviewer : ScriptableObject + { + /// + /// Current calculation method of the collider type we are using as an int + /// + public int CurrentMethod = 0; + + /// + /// Current attach to transform used in the calculation + /// + public Transform CurrentAttachTo; + + /// + /// Current vertex count for the last calculation we did for a preview + /// + public int CurrentVertexCount = 0; + + public float CurrentShrinkGrow = 0; + + /// + /// Current count of the nubmer of colliders selected for the last preview calculation. + /// + public int CurrentColliderCount = 0; + + public float CurrentVertexNormalOffset = 0; + public float CurrentVertexNormalInset = 0; + + public NORMAL_OFFSET CurrentNormalOffset; + + /// + /// Color to draw the preview lines with + /// + public Color DrawColor = Color.cyan; + + private EasyColliderCreator _ECC; + /// + /// Creator that calculates the colliders we use to preview. + /// + /// + public EasyColliderCreator ECC + { + get + { + if (_ECC == null) + { + _ECC = new EasyColliderCreator(); + } + return _ECC; + } + set { _ECC = value; } + } + + /// + /// Shader used to draw mesh colliders. + /// + private Shader MeshColliderShader; + + /// + /// All the data from the current preview calculation + /// + public EasyColliderData PreviewData; + + + public List RotateAndDuplicateData = new List(); + public bool HasValidRotateAndDuplicateData() + { + if (RotateAndDuplicateData == null || RotateAndDuplicateData.Count == 0) return false; + foreach (EasyColliderData d in RotateAndDuplicateData) + { + if (!d.IsValid) + { + return false; + } + } + return true; + } + + + //TODO: integrate calculation of auto skinned into normal update-preview stuff. + public List AutoSkinnedData = new List(); + + /// + /// Preview data hides that rotated colliders are different "collider types" so this keeps track of that for updating the preview. + /// + public CREATE_COLLIDER_TYPE ActualColliderType; + + public ECE_WINDOW_TAB CurrentPreviewTab; + + #region DrawPreviews + + /// + /// Draws a preview from the current preview data. + /// + private void DrawPreview() + { + if (RotateAndDuplicateData != null && RotateAndDuplicateData.Count > 0) + { + // Debug.Log("Rotate and duplicate preview"); + DrawList(RotateAndDuplicateData); + } + else if (AutoSkinnedData != null && AutoSkinnedData.Count > 0) + { + // Debug.Log("Auto skinned preivew"); + DrawList(AutoSkinnedData); + } + else + { + // make sure we have data, and it's valid. + if (PreviewData != null && PreviewData.IsValid) + { + // Draw based on the preview type. + switch (PreviewData.ColliderType) + { + case CREATE_COLLIDER_TYPE.BOX: + case CREATE_COLLIDER_TYPE.ROTATED_BOX: + DrawPreviewBox(PreviewData as BoxColliderData, DrawColor); + break; + case CREATE_COLLIDER_TYPE.SPHERE: + DrawPreviewSphere(PreviewData as SphereColliderData, DrawColor); + break; + case CREATE_COLLIDER_TYPE.CAPSULE: + case CREATE_COLLIDER_TYPE.ROTATED_CAPSULE: + DrawPreviewCapsule(PreviewData as CapsuleColliderData, DrawColor); + break; + case CREATE_COLLIDER_TYPE.CONVEX_MESH: + case CREATE_COLLIDER_TYPE.CYLINDER: + DrawPreviewConvexMesh(PreviewData as MeshColliderData, DrawColor); + break; + } + } + } + } + + public void DrawList(List data) + { + foreach (EasyColliderData PreviewData in data) + { + if (!PreviewData.IsValid) { continue; } + // Draw based on the preview type. + switch (PreviewData.ColliderType) + { + case CREATE_COLLIDER_TYPE.BOX: + case CREATE_COLLIDER_TYPE.ROTATED_BOX: + DrawPreviewBox(PreviewData as BoxColliderData, DrawColor); + break; + case CREATE_COLLIDER_TYPE.SPHERE: + DrawPreviewSphere(PreviewData as SphereColliderData, DrawColor); + break; + case CREATE_COLLIDER_TYPE.CAPSULE: + case CREATE_COLLIDER_TYPE.ROTATED_CAPSULE: + DrawPreviewCapsule(PreviewData as CapsuleColliderData, DrawColor); + break; + case CREATE_COLLIDER_TYPE.CONVEX_MESH: + case CREATE_COLLIDER_TYPE.CYLINDER: + DrawPreviewConvexMesh(PreviewData as MeshColliderData, DrawColor); + break; + } + } + } + + /// + /// Draws a mesh collider preview + /// + /// Data from quickhull calculation + private void DrawPreviewConvexMesh(MeshColliderData data, Color color) + { + // try to find mesh shader + if (MeshColliderShader == null) + { + MeshColliderShader = Shader.Find("Custom/EasyColliderMeshColliderPreview"); + } + // if we have the shader, draw it using the wireframe and the color + if (MeshColliderShader != null && data.ConvexMesh != null) + { + Material wireMat = new Material(MeshColliderShader); + wireMat.SetColor("_Color", color); + wireMat.SetPass(0); + GL.wireframe = true; + Graphics.DrawMeshNow(data.ConvexMesh, data.Matrix); + GL.wireframe = false; + Graphics.DrawMeshNow(data.ConvexMesh, data.Matrix); + } + } + + /// + /// Draws a box collider + /// + /// Data from box calculation + private void DrawPreviewBox(BoxColliderData data, Color color) + { + // half size and center + Vector3 hs = data.Size / 2; + Vector3 c = data.Center; + // transform each point of the cube to world space with the transformation matrix + Vector3[] points = new Vector3[8]{ + data.Matrix.MultiplyPoint3x4(c + hs), + data.Matrix.MultiplyPoint3x4(c + new Vector3(hs.x, hs.y, -hs.z)), + data.Matrix.MultiplyPoint3x4(c + new Vector3(hs.x, -hs.y, hs.z)), + data.Matrix.MultiplyPoint3x4(c + new Vector3(hs.x, -hs.y, -hs.z)), + data.Matrix.MultiplyPoint3x4(c + new Vector3(-hs.x, hs.y, hs.z)), + data.Matrix.MultiplyPoint3x4(c + new Vector3(-hs.x, hs.y, -hs.z)), + data.Matrix.MultiplyPoint3x4(c + new Vector3(-hs.x, -hs.y, hs.z)), + data.Matrix.MultiplyPoint3x4(c - hs) + }; + // draw the lines connecting corners of the cube. + Handles.color = color; + Handles.DrawLine(points[0], points[1]); + Handles.DrawLine(points[0], points[2]); + Handles.DrawLine(points[0], points[4]); + Handles.DrawLine(points[7], points[5]); + Handles.DrawLine(points[7], points[6]); + Handles.DrawLine(points[7], points[3]); + Handles.DrawLine(points[1], points[5]); + Handles.DrawLine(points[1], points[3]); + Handles.DrawLine(points[2], points[6]); + Handles.DrawLine(points[2], points[3]); + Handles.DrawLine(points[4], points[5]); + Handles.DrawLine(points[4], points[6]); + } + + public void DrawCapsuleCollider(CapsuleColliderData data, Color color) + { + DrawPreviewCapsule(data, color); + } + + private void DrawPreviewCapsule(CapsuleColliderData data, Color color) + { + Handles.color = color; + // calculate top and bottom center sphere locations. + float offset = data.Height / 2; + Vector3 top, bottom = top = data.Center; + float radius = data.Radius; +#if (UNITY_2017_2_OR_NEWER) + Vector3 scale = data.Matrix.lossyScale; +#else + Vector3 scale = CurrentAttachTo.transform.lossyScale; +#endif + switch (data.Direction) + { + case 0: //x axis + //adjust radius by the bigger scale. + radius *= scale.y > scale.z ? scale.y : scale.z; + // adjust the offset to top and bottom mid points for spheres based on radius / scale in that direction + offset -= radius / scale.x; + // offset top and bottom points. + top.x += offset; + bottom.x -= offset; + break; + case 1: + radius *= scale.x > scale.z ? scale.x : scale.z; + offset -= radius / scale.y; + top.y += offset; + bottom.y -= offset; + break; + case 2: + radius *= scale.x > scale.y ? scale.x : scale.y; + offset -= radius / scale.z; + top.z += offset; + bottom.z -= offset; + break; + } + // dont know why mathf.approx wasn't working here, but the others don't work in rare cases. (top==bottom in this case works) + // which fixes issue where a preview would not be drawn in some extremely weird rare case. + if (data.Height < data.Radius * 2 || Mathf.Approximately(data.Height, data.Radius * 2) || top == bottom) + { + // draw just the sphere if the radius and the height will make a sphere. + Vector3 worldCenter = data.Matrix.MultiplyPoint(data.Center); + Handles.DrawWireDisc(worldCenter, Vector3.forward, radius); + Handles.DrawWireDisc(worldCenter, Vector3.right, radius); + Handles.DrawWireDisc(worldCenter, Vector3.up, radius); + return; + } + Vector3 worldTop = data.Matrix.MultiplyPoint3x4(top); + Vector3 worldBottom = data.Matrix.MultiplyPoint3x4(bottom); + Vector3 up = worldTop - worldBottom; + Vector3 cross1 = Vector3.up; + // dont want to cross if in same direction, forward works in this case as the first cross + if (up.normalized == cross1 || up.normalized == -cross1) + { + cross1 = Vector3.forward; + } + Vector3 right = Vector3.Cross(up, -cross1).normalized; + Vector3 forward = Vector3.Cross(up, -right).normalized; + // full circles at top and bottom + Handles.DrawWireDisc(worldTop, up, radius); + Handles.DrawWireDisc(worldBottom, up, radius); + // half arcs at top and bottom + Handles.DrawWireArc(worldTop, forward, right, 180f, radius); + Handles.DrawWireArc(worldTop, -right, forward, 180f, radius); + Handles.DrawWireArc(worldBottom, -forward, right, 180f, radius); + Handles.DrawWireArc(worldBottom, right, forward, 180f, radius); + // connect bottom and top side points + Handles.DrawLine(worldTop + right * radius, worldBottom + right * radius); + Handles.DrawLine(worldTop - right * radius, worldBottom - right * radius); + Handles.DrawLine(worldTop + forward * radius, worldBottom + forward * radius); + Handles.DrawLine(worldTop - forward * radius, worldBottom - forward * radius); + } + + public void DrawSphereCollider(SphereColliderData data, Color color) + { + DrawPreviewSphere(data, color); + } + + /// + /// Draws a sphere collider + /// + /// Data from sphere calculation + private void DrawPreviewSphere(SphereColliderData data, Color color) + { + Handles.color = color; + Vector3 worldCenter = data.Matrix.MultiplyPoint3x4(data.Center); + // Draw all normal axis' rings at the world center location for both perspective and isometric/orthographic + float radius = data.Radius; +#if (UNITY_2017_2_OR_NEWER) + Vector3 scale = data.Matrix.lossyScale; +#else + Vector3 scale = CurrentAttachTo.transform.lossyScale; +#endif + float largestScale = Mathf.Max(Mathf.Max(scale.x, scale.y), scale.z); + radius *= largestScale; + Handles.DrawWireDisc(worldCenter, Vector3.forward, radius); + Handles.DrawWireDisc(worldCenter, Vector3.right, radius); + Handles.DrawWireDisc(worldCenter, Vector3.up, radius); + // orthographic camera + if (Camera.current != null) + { + if (Camera.current.orthographic) + { + // simple, use cameras forward in orthographic + Handles.DrawWireDisc(worldCenter, Camera.current.transform.forward, radius); + } + else + { + // draw a circle facing the camera covering all the radius in prespective mode + Vector3 normal = worldCenter - Handles.inverseMatrix.MultiplyPoint(Camera.current.transform.position); + float sqrMagnitude = normal.sqrMagnitude; + float r2 = radius * radius; + float r4m = r2 * r2 / sqrMagnitude; + float newRadius = Mathf.Sqrt(r2 - r4m); + Handles.DrawWireDisc(worldCenter - r2 * normal / sqrMagnitude, normal, newRadius); + } + } + } + + #endregion + + + public void ClearPreview() + { + // PreviewData = new EasyColliderData(); + PreviewData = null; + RotateAndDuplicateData = null; + AutoSkinnedData = null; + } + + /// + /// Gets the method for the current preview collider type + /// + /// type of collider we are previewing + /// enum of method selected for collider type as an int + private int GetMethodForColliderPreviewType(EasyColliderPreferences preferences) + { + if (preferences.PreviewColliderType == CREATE_COLLIDER_TYPE.CAPSULE || preferences.PreviewColliderType == CREATE_COLLIDER_TYPE.ROTATED_CAPSULE) + { + return (int)preferences.CapsuleColliderMethod; + } + else if (preferences.PreviewColliderType == CREATE_COLLIDER_TYPE.SPHERE) + { + return (int)preferences.SphereColliderMethod; + } + else if (preferences.PreviewColliderType == CREATE_COLLIDER_TYPE.CONVEX_MESH) + { + return (int)preferences.MeshColliderMethod; + } + else if (preferences.PreviewColliderType == CREATE_COLLIDER_TYPE.CYLINDER) + { + return (int)preferences.CylinderNumberOfSides + (int)preferences.CylinderOrientation + ((int)preferences.CylinderRotationOffset * 1000); + } + else + { + return 0; + } + } + + #region CalculatePreviews + + /// + /// Calculates the preview data to be used to display. + /// + /// current preferences + /// current editor + private void CalculatePreviewCollider(EasyColliderPreferences preferences, EasyColliderEditor ece) + { + // private void CalculatePreviewCollider(CREATE_COLLIDER_TYPE type, List worldVertices, List normals, GameObject attachTo, int method) + // CalculatePreviewCollider(preferences.PreviewColliderType, editor.GetWorldVertices(), editor.GetNormals(), editor.AttachToObject, GetMethodForColliderPreviewType(preferences)); + List worldVertices = ece.GetWorldVertices(true); + switch (ActualColliderType) + { + case CREATE_COLLIDER_TYPE.BOX: + case CREATE_COLLIDER_TYPE.ROTATED_BOX: + PreviewData = ECC.CalculateBox(worldVertices, CurrentAttachTo.transform, ActualColliderType == CREATE_COLLIDER_TYPE.ROTATED_BOX, true); + break; + case CREATE_COLLIDER_TYPE.SPHERE: + PreviewData = CalculatePreviewSphere(worldVertices, CurrentAttachTo.transform, preferences.SphereColliderMethod); + break; + case CREATE_COLLIDER_TYPE.ROTATED_CAPSULE: + case CREATE_COLLIDER_TYPE.CAPSULE: + PreviewData = CalculatePreviewCapsule(worldVertices, CurrentAttachTo.transform, preferences.CapsuleColliderMethod, ActualColliderType == CREATE_COLLIDER_TYPE.ROTATED_CAPSULE); + break; + case CREATE_COLLIDER_TYPE.CONVEX_MESH: + PreviewData = CalculatePreviewMesh(worldVertices, ece.GetNormals(), CurrentAttachTo.transform, preferences.MeshColliderMethod); + break; + case CREATE_COLLIDER_TYPE.CYLINDER: + PreviewData = CalculatePreviewCylinder(worldVertices, CurrentAttachTo.transform); + break; + } + } + + /// + /// Calculates the merge colliders preview data + /// + /// current preferences + /// current editor + private void CalculatePreviewMerge(EasyColliderPreferences preferences, EasyColliderEditor ece) + { + // private void CalculatePreviewMerge(CREATE_COLLIDER_TYPE mergeTo, List selectedColliders, GameObject attachTo, int method) + // CalculatePreviewMerge(preferences.PreviewColliderType, editor.SelectedColliders, editor.AttachToObject, GetMethodForColliderPreviewType(preferences)); + if (CurrentColliderCount > 0) + { + PreviewData = ECC.MergeCollidersPreview(ece.SelectedColliders, preferences.PreviewColliderType, CurrentAttachTo); + } + else + { + PreviewData = new EasyColliderData(); + } + } + + + /// + /// Calculates the rotate and duplicate preview data. + /// + /// current preferences + /// current editor + private void CalculatePreviewRotateAndDuplicate(EasyColliderPreferences preferences, EasyColliderEditor ece) + { + if (ece.AttachToObject == null || ece.SelectedGameObject == null || preferences.rotatedDupeSettings.pivot == null) return; + RotateAndDuplicateData = ECC.CalculateRotateAndDuplicate(preferences, ece); + } + + + /// + /// Calculates a preview capsule + /// + /// world vertices used to calculate the collider + /// gameobject the collider will be attached to + /// calculation method used to calculate the collider type as an int + /// will the collider be a rotated collider? + /// Calculated capsule collider data + private EasyColliderData CalculatePreviewCapsule(List worldVertices, Transform attachTo, CAPSULE_COLLIDER_METHOD method, bool isRotated) + { + switch (method) + { + case CAPSULE_COLLIDER_METHOD.BestFit: + return ECC.CalculateCapsuleBestFit(worldVertices, attachTo, isRotated, true); + default: + return ECC.CalculateCapsuleMinMax(worldVertices, attachTo, method, isRotated, true); + } + } + + + /// + /// Calculates a preview cylinder + /// + /// world vertices used to calculate the collider + /// transform the collider will be attached to + /// Calculated cylinder shaped mesh collider data + private MeshColliderData CalculatePreviewCylinder(List worldVertices, Transform attachTo) + { + MeshColliderData data = ECC.CalculateCylinderCollider(worldVertices, attachTo); + return data; + } + + /// + /// Calculates the preview data for a convex mesh collider. (uses quickhull calculation for both messy and quickhull method) + /// + /// World-Space vertices that are selected + /// Current AttachTo object + /// Mesh Collider generation method + /// EasyCollider data with a mesh value set. + private MeshColliderData CalculatePreviewMesh(List worldVertices, List normals, Transform attachTo, MESH_COLLIDER_METHOD method) + { + // Messy-hull not suited for preview as the intermediate mesh is essentially useless + // and the actual important result is from the internal calculation by the mesh collider, so it would have to be added. + // but since internall it uses a similar hull method, the result should be similar as well. + // previewer now goes through ECC for meshes as well, as it should. + return ECC.CalculateHullData(worldVertices, attachTo); + } + + /// + /// Calculates a preview sphere + /// + /// world vertices used to calculate the collider + /// gameobject the collider will be attached to + /// calculation method used to calculate the collider type as an int + /// Calculated sphere collider data + private EasyColliderData CalculatePreviewSphere(List worldVertices, Transform attachTo, SPHERE_COLLIDER_METHOD method) + { + switch (method) + { + case SPHERE_COLLIDER_METHOD.Distance: + return ECC.CalculateSphereDistance(worldVertices, attachTo); + case SPHERE_COLLIDER_METHOD.BestFit: + return ECC.CalculateSphereBestFit(worldVertices, attachTo); + default: + return ECC.CalculateSphereMinMax(worldVertices, attachTo); + } + } + + #endregion + + /// + /// Checks to see if the collider needs to be calculated, so the preview is updated. + /// + /// + /// + /// + /// true if the preview needs to be updated. + private bool NeedsUpdate(EasyColliderPreferences preferences, EasyColliderEditor editor) + { + // private bool NeedsUpdate(EasyColliderEditor editor, CREATE_COLLIDER_TYPE colliderType, int method) + //(NeedsUpdate(editor, preferences.PreviewColliderType, GetMethodForColliderPreviewType(preferences)) || forceUpdate) + if (preferences.CurrentWindowTab == ECE_WINDOW_TAB.Creation) + { + if (CurrentVertexCount != editor.SelectedVertices.Count + || (PreviewData == null && RotateAndDuplicateData == null && preferences.rotatedDupeSettings.enabled) + || (PreviewData == null && preferences.rotatedDupeSettings.enabled == false) + || (PreviewData != null && PreviewData.ColliderType != preferences.PreviewColliderType) + || CurrentMethod != GetMethodForColliderPreviewType(preferences) + || CurrentAttachTo != editor.AttachToObject.transform + || ActualColliderType != preferences.PreviewColliderType + || editor.HasTransformMoved() + || CurrentVertexNormalOffset != preferences.VertexNormalOffset + || CurrentVertexNormalInset != preferences.VertexNormalInset + || CurrentShrinkGrow != preferences.ShrinkGrow + || CurrentNormalOffset != preferences.VertexNormalOffsetType + ) + { + return true; + } + } + else if (preferences.CurrentWindowTab == ECE_WINDOW_TAB.Editing) + { + if (CurrentColliderCount != editor.SelectedColliders.Count + || PreviewData == null + || CurrentMethod != GetMethodForColliderPreviewType(preferences) + || CurrentAttachTo != editor.AttachToObject.transform + || editor.HasTransformMoved() + || ActualColliderType != preferences.PreviewColliderType) + { + return true; + } + return false; + } + return false; + } + + /// + /// Updates the current preview if needed. + /// + /// + /// + public void UpdatePreview(EasyColliderEditor editor, EasyColliderPreferences preferences, bool forceUpdate = false) + { + if (editor != null && preferences != null && editor.SelectedGameObject != null && editor.AttachToObject != null) + { + DrawColor = preferences.PreviewDrawColor; + if (NeedsUpdate(preferences, editor) || forceUpdate) + { + ActualColliderType = preferences.PreviewColliderType; + CurrentAttachTo = editor.AttachToObject.transform; + CurrentColliderCount = editor.SelectedColliders.Count; + CurrentMethod = GetMethodForColliderPreviewType(preferences); + CurrentPreviewTab = preferences.CurrentWindowTab; + CurrentVertexCount = editor.SelectedVertices.Count; + CurrentVertexNormalOffset = preferences.VertexNormalOffset; + CurrentVertexNormalInset = preferences.VertexNormalInset; + CurrentShrinkGrow = preferences.ShrinkGrow; + CurrentNormalOffset = preferences.VertexNormalOffsetType; + if (PreviewData == null) + { + PreviewData = new EasyColliderData(); + } + if (preferences.CurrentWindowTab == ECE_WINDOW_TAB.Creation) + { + if (preferences.rotatedDupeSettings.enabled == true) + { + RotateAndDuplicateData = new List(); + CurrentAttachTo = preferences.rotatedDupeSettings.pivot.transform; + CalculatePreviewRotateAndDuplicate(preferences, editor); + } + else + { + CalculatePreviewCollider(preferences, editor); + } + PreviewData.ColliderType = ActualColliderType; + } + else if (preferences.CurrentWindowTab == ECE_WINDOW_TAB.Editing) + { + CalculatePreviewMerge(preferences, editor); + } + } + DrawPreview(); + } + } + + + #region VHACDPREVIEW + + /// + /// Array of triangles for use in VHACDResultPreview + /// + private int[] triangles; + /// + /// Array of vertices for use in VHACDResultPreview + /// + private Vector3[] vertices; + /// + /// List of randomized colors for use in VHACDResultPreview + /// + private List colors = new List(); + + + public void DrawVHACDConversionPreview(List data, VHACD_CONVERSION convertTo) + { + foreach (EasyColliderData d in data) + { + if (d == null) continue; + if (convertTo == VHACD_CONVERSION.Boxes) + { + BoxColliderData bcd = d as BoxColliderData; + if (bcd == null) continue; + DrawPreviewBox(bcd, DrawColor); + } + else if (convertTo == VHACD_CONVERSION.Capsules) + { + CapsuleColliderData ccd = d as CapsuleColliderData; + if (ccd == null) continue; + DrawPreviewCapsule(ccd, DrawColor); + } + else if (convertTo == VHACD_CONVERSION.Spheres) + { + SphereColliderData scd = d as SphereColliderData; + if (scd == null) continue; + DrawPreviewSphere(scd, DrawColor); + } + } + } + + + /// + /// Draws each mesh in the dictionary with a black wireframe, and a random color semi-transparent mesh. + /// + /// Dictionary of transforms and meshes from the result of VHACD + public void DrawVHACDResultPreview(Dictionary previewResult) + { + Shader s = Shader.Find("Custom/EasyColliderMeshColliderPreview"); + if (s != null) + { + Material wireMat = new Material(s); + wireMat.SetColor("_Color", Color.black); + Material flatMat = new Material(s); + foreach (KeyValuePair kvp in previewResult) + { + if (kvp.Key == null) continue; // transform can be null if it's deleted. + if (colors.Count < kvp.Value.Length) + { + int addColors = kvp.Value.Length - colors.Count; + for (int i = 0; i < addColors; i++) + { + colors.Add(new Color(Random.Range(0f, 1f), Random.Range(0f, 1f), Random.Range(0f, 1f))); + } + } + for (int i = 0; i < kvp.Value.Length; i++) + { + flatMat.SetColor("_Color", colors[i]); + flatMat.SetPass(1); + Graphics.DrawMeshNow(kvp.Value[i], kvp.Key.localToWorldMatrix); + wireMat.SetPass(1); + GL.wireframe = true; + Graphics.DrawMeshNow(kvp.Value[i], kvp.Key.localToWorldMatrix); + GL.wireframe = false; + } + } + } + else + { + Debug.LogWarning("EasyColliderEditor: Unable to find shader for preview."); + } + } + #endregion + } +} +#endif diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderPreviewer.cs.meta b/Assets/EasyColliderEditor/Scripts/EasyColliderPreviewer.cs.meta new file mode 100644 index 00000000..c33564ae --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderPreviewer.cs.meta @@ -0,0 +1,20 @@ +fileFormatVersion: 2 +guid: 0975e5f3c5571d940891fd8dc17e7587 +timeCreated: 1592761340 +licenseType: Store +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/EasyColliderPreviewer.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderProperties.cs b/Assets/EasyColliderEditor/Scripts/EasyColliderProperties.cs new file mode 100644 index 00000000..a2604317 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderProperties.cs @@ -0,0 +1,100 @@ +using UnityEngine; +namespace ECE +{ + /// + /// Properties to use when creating a collider. + /// + public struct EasyColliderProperties + { + /// + /// Marks the collider's isTrigger property + /// + public bool IsTrigger; + + /// + /// Layer of gameobject when creating a rotated collider. + /// + public int Layer; + +#if (UNITY_6000_0_OR_NEWER) + /// + /// Physic material to set on collider. + /// + public PhysicsMaterial PhysicMaterial; +#else + /// + /// Physic material to set on collider. + /// + public PhysicMaterial PhysicMaterial; + public void SetPhysicMat(PhysicMaterial physmat) + { + PhysicMaterial = physmat; + } +#endif + + +#if (UNITY_2022_2_OR_NEWER) + /// + /// whether or not the collidere generates contacts for physics.contact event. + /// + public bool ProvidesContacts; + /// + /// layer overrides - layer override priority property on a collider. + /// + public int LayerOverridePriority; + + /// + /// layer overrides exclude layer mask property on colliders. + /// + + public LayerMask ExcludeLayers; + + /// + /// layer overrides include layer mask property on colliders. + /// + public LayerMask IncludeLayers; +#endif + + /// + /// Orientation of created collider. + /// + public COLLIDER_ORIENTATION Orientation; + + /// + /// Gameobject collider gets added to. + /// + public GameObject AttachTo; + + + /// + /// Properties with which to create a collider + /// + /// Should the collider's isTrigger property be true? + /// Layer of gameobject when creating a rotated collider + /// Physic Material to apply to a collider + /// GameObject to attach the collider to + /// Orientation of the collider for generation + public EasyColliderProperties(bool isTrigger, int layer, +#if (UNITY_6000_0_OR_NEWER) + PhysicsMaterial physicMaterial, +#else + PhysicMaterial physicMaterial, +#endif + + GameObject attachTo, COLLIDER_ORIENTATION orientation = COLLIDER_ORIENTATION.NORMAL) + { + IsTrigger = isTrigger; + Layer = layer; + PhysicMaterial = physicMaterial; + AttachTo = attachTo; + Orientation = orientation; +#if (UNITY_2022_2_OR_NEWER) + ProvidesContacts = false; + LayerOverridePriority = 0; + IncludeLayers = 0; + ExcludeLayers = 0; +#endif + } + + } +} diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderProperties.cs.meta b/Assets/EasyColliderEditor/Scripts/EasyColliderProperties.cs.meta new file mode 100644 index 00000000..41d6e40f --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderProperties.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 6beb8fffd8449374b81cf9857e315469 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/EasyColliderProperties.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderQuickHull.cs b/Assets/EasyColliderEditor/Scripts/EasyColliderQuickHull.cs new file mode 100644 index 00000000..737ceb5f --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderQuickHull.cs @@ -0,0 +1,1238 @@ + +using System.Collections.Generic; +using UnityEngine; +using System.Linq; +namespace ECE +{ + // TODO: re-implement coroutine for runtime quickhull? improve find initial points? + // Implemented with the help of the explanation of the algorithm found here: http://algolist.ru/maths/geom/convhull/qhull3d.php + public class EasyColliderQuickHull + { + + /// + /// Calculates a convex hull for a list of local-space points. + /// + /// Local-Space points to generate a hull on. + /// the class with the result already calculated + public static EasyColliderQuickHull CalculateHull(List points) + { + EasyColliderQuickHull qh = new EasyColliderQuickHull(); + // Calculate and return the quickhull. + qh.GenerateHull(points); + return qh; + } + + /// + /// Calculates a convex hull for a list of world-space points. + /// + /// World-Space points to generate a hull on. + /// Transform the result will be attached to. + /// class with result calculated + public static EasyColliderQuickHull CalculateHullWorld(List points, Transform attachTo) + { + List localPoints = new List(); + foreach (Vector3 point in points) + { + localPoints.Add(attachTo.InverseTransformPoint(point)); + } + EasyColliderQuickHull qh = new EasyColliderQuickHull(); + qh.GenerateHull(localPoints); + return qh; + } + + /// + /// Calculates a convex hull for a list of world-space points for use by the previwer. + /// + /// world-space points to generate a hull on + /// + /// EasyColliderData with mesh, matrix, and validity + public static MeshColliderData CalculateHullData(List points, Transform attachTo) + { + if (points == null || points.Count < 4) + { + // can't calculate yet. + return new MeshColliderData(); + } + EasyColliderQuickHull qh = CalculateHullWorld(points, attachTo); + MeshColliderData data = new MeshColliderData(); + data.ConvexMesh = qh.Result; + data.IsValid = true; + data.Matrix = attachTo.localToWorldMatrix; + data.ColliderType = CREATE_COLLIDER_TYPE.CONVEX_MESH; + return data; + } + + /// + /// Calculates a convex hull for a list of local-space points for use by the previewer + /// + /// local space points + /// EasyColliderData with mesh + public static MeshColliderData CalculateHullData(List points) + { + EasyColliderQuickHull qh = CalculateHull(points); + MeshColliderData data = new MeshColliderData(); + data.ConvexMesh = qh.Result; + data.IsValid = true; + data.ColliderType = CREATE_COLLIDER_TYPE.CONVEX_MESH; + return data; + } + + + + /// + /// class representing a triangle / face + /// + private class Face + { + /// + /// v0 to v1 face + /// + public int F0; + + /// + /// v1 to v2 face + /// + public int F1; + + /// + /// v2 to v0 face + /// + public int F2; + + /// + /// Normal of the face + /// + public Vector3 Normal; + + /// + /// is the face on the convex hull? + /// + public bool OnConvexHull; + + /// + /// List of vertices on the outside of the triangle (signed distance from plane is positive) + /// + public List OutsideVertices; + + // vertex index on points list. + public int V0; + public int V1; + public int V2; + + /// + /// Creates a face + /// + /// vertex 0 + /// vertex 1 + /// vertex 2 + /// normal of the face + /// face connected to v0-v1 edge + /// face connected to v1-v2edge + /// face connected to v2-v0 edge + public Face(int v0, int v1, int v2, Vector3 normal, int f0, int f1, int f2) + { + V0 = v0; + V1 = v1; + V2 = v2; + Normal = normal; + OutsideVertices = new List(); + F0 = f0; + F1 = f1; + F2 = f2; + OnConvexHull = true; + } + } + + /// + /// Class to hold vertex and face data of the current horizon edge + /// + private class Horizon + { + /// + /// Index of Face crossed over to + /// + public int Face; + + /// + /// Index of Face crossed over from + /// + public int From; + + /// + /// Is the edge on the convex hull? + /// + public bool OnConvexHull; + + /// + /// Index of vertex 0 of edge + /// + public int V0; + + /// + /// Index of vertex 1 of edge + /// + public int V1; + + /// + /// Create a new horizon edge, automatically marked on convex hull + /// + /// Index of vertex 0 of edge + /// Index of vertex 1 of edge + /// Face we cross edge to + /// Face we cross edge from + public Horizon(int v0, int v1, int face, int from) + { + V0 = v0; + V1 = v1; + Face = face; + From = from; + OnConvexHull = true; + } + } + + //Debug variables left in for future use. + protected bool DebugHorizon; + protected Color DebugHorizonColor = new Color(1, 0.5f, 0, 1); + protected int DebugLoopNumber = 0; + protected int DebugMaxLoopNumber; + protected bool DebugNewFaces; + protected bool DebugNormals; + protected bool DebugOutsideSet; + protected Color DebugNormalColor = new Color(0.5f, 0, 0.5f, 1); + protected float DrawTime = 2f; + + /// + /// list of assigned vertices from add to outside set. + /// + /// + /// + private HashSet AssignedVertices = new HashSet(); + + /// + /// List of vertices that area already done (in/on the convex hull) + /// + private HashSet ClosedVertices = new HashSet(); + + /// + /// List of current horizon edges + /// + /// + /// + private List CurrentHorizon = new List(); + + /// + /// Just a small value for float comparisons + /// + private float Epsilon = 0.000001f; + + + /// + /// List of faces in the convex hull. + /// + private List Faces = new List(); + + /// + /// List of new faces created after finding the horizon edge. + /// + private List NewFaces = new List(); + + /// + /// result mesh of quick hull calculation + /// + public Mesh Result = null; + + /// + /// list of unasigned vertices for add to outside set. + /// + private HashSet UnAssignedVertices = new HashSet(); + + /// + /// List of all original vertices. + /// + /// + /// + private List VerticesList = new List(); + + /// + /// Adds vertices to a faces outside set and adds them to the assigned vertices set. + /// Also closes / merges vertices + /// + /// Face to assign vertices to + /// Set of unassigned vertices + private void AddToOutsideSet(Face face, HashSet vertices) + { + float d = 0; + foreach (int i in vertices) + { + // skip already assigned vertices. + if (AssignedVertices.Contains(i) || ClosedVertices.Contains(i)) continue; + // vertex is not assigned + d = DistanceFromPlane(VerticesList[i], face.Normal, VerticesList[face.V0]); + if (IsApproxZero(d)) + { + if (IsVertOnFace(i, face)) + { + ClosedVertices.Add(i); + } + } + else if (d > 0) + { + // claim vertex by removing it from vertices list and adding to the face's set of vertices. + AssignedVertices.Add(i); + face.OutsideVertices.Add(i); + } + } + } + + /// + /// Checks if vertices a, and b, are coincident using an epsilon value. + /// + /// + /// + /// true if coincident, false otherwise + private bool AreVertsCoincident(Vector3 a, Vector3 b) + { + // if one of them is greater than epislon, they aren't coincident. + // simpler than checking they are all < Epsilon, as they aren't coincident if any single one fails. + if (Mathf.Abs(a.x - b.x) > Epsilon || Mathf.Abs(a.y - b.y) > Epsilon || Mathf.Abs(a.z - b.z) > Epsilon) + { + return false; + } + return true; + } + + /// + /// Checks if vertices a and b are approximately coincident (x, y, and z differences are all < epsilon) + /// + /// + /// + /// true if coincident, false otherwise. + private bool AreVertsCoincident(int a, int b) + { + if (Mathf.Abs(VerticesList[a].x - VerticesList[b].x) > Epsilon + || Mathf.Abs(VerticesList[a].y - VerticesList[b].y) > Epsilon + || Mathf.Abs(VerticesList[a].z - VerticesList[b].z) > Epsilon) + { + return false; + } + return true; + } + + /// + /// Updates closed vertices list by checking if unassigned vertices lie on a face on the convex hull. + /// Updates the unassigned list by removing the newly closed vertices. + /// + private void CloseUnAssignedVertsOnFaces() + { + HashSet newClosedVertices = new HashSet(); + foreach (Face f in Faces) + { + if (!f.OnConvexHull) { continue; } + foreach (int i in UnAssignedVertices) + { + if (ClosedVertices.Contains(i)) { continue; } + if (IsVertOnFace(i, f)) + { + newClosedVertices.Add(i); + ClosedVertices.Add(i); + } + } + } + UnAssignedVertices.ExceptWith(newClosedVertices); + } + + + /// + /// Checks to see if vertex at index i is on a face. + /// + /// index + /// face + /// true if vertex at index i is on the face + private bool IsVertOnFace(int i, Face face) + { + // same approximate position as one of the corners + if (AreVertsCoincident(i, face.V0) || AreVertsCoincident(i, face.V1) || AreVertsCoincident(i, face.V2)) + { + return true; + } + // areas of full triangle, and point and edge. + float a = CalcTriangleArea(face.V0, face.V1, face.V2); + float a1 = CalcTriangleArea(i, face.V0, face.V1); + float a2 = CalcTriangleArea(i, face.V1, face.V2); + float a3 = CalcTriangleArea(i, face.V2, face.V0); + if (isApproxEqual(a, (a1 + a2 + a3))) + { + return true; + } + return false; + } + + /// + /// Calculates the normal of a face with points a, b, and c. + /// + /// + /// + /// + /// Normal of the face formed by points a, b, and c. + private Vector3 CalcNormal(Vector3 a, Vector3 b, Vector3 c) + { + return Vector3.Cross(b - a, c - a).normalized; + } + + /// + /// Calculates a normal given vertex index's a, b, and c. + /// + /// + /// + /// + /// Normal of the face formed by the vertices at indexs a, b, and c + private Vector3 CalcNormal(int a, int b, int c) + { + return Vector3.Cross(VerticesList[b] - VerticesList[a], VerticesList[c] - VerticesList[a]).normalized; + } + + /// + /// Calculates the area of a tringle with points v0, v1, and v1. + /// + /// + /// + /// + /// Area of the triangle + private float CalcTriangleArea(int v0, int v1, int v2) + { + return (0.5f) * Vector3.Cross(VerticesList[v1] - VerticesList[v0], VerticesList[v2] - VerticesList[v1]).magnitude; + } + + /// + /// Calculate the horizon edge recursively + /// + /// index in vertices list of the current eyepoint + /// last edge that was crossed to get to currFace + /// index in list of faces to check horizon on + /// is this the first face? + private void CalculateHorizon(int eyePoint, Horizon crossedEdge, int currFace, bool firstFace = true) + { + // if curr face is not on the convex hull (negative distance) + float d = DistanceFromPlane(VerticesList[eyePoint], Faces[currFace].Normal, VerticesList[Faces[currFace].V0]); + // if the currFace is not on the convex hull + if (!Faces[currFace].OnConvexHull) + { + // mark the crossed edge as not on the convex hull and return + crossedEdge.OnConvexHull = false; + return; + } + // if the curr face is visible from the eyepoint (signed distance from plane will be positive) + else if (d > 0) + { + // 1. mark current face as not on the convex hull. + Faces[currFace].OnConvexHull = false; + // 2. remove all vertices from the currFace's outside set and add them to the list unclaimed vertices. + UnAssignedVertices.UnionWith(Faces[currFace].OutsideVertices); + Faces[currFace].OutsideVertices.Clear(); + // if the crossed edge != null (only null for the first face) then mark the crossed edge as not on the convex hull + if (!firstFace) + { + crossedEdge.OnConvexHull = false; + } + // cross each of the edges of currface which are still on the convex hull. in counterclockwise order + // starting from the edge after the crossed edge (in the case of the first face, pick any edge to start with. for each curr edge recurse with the call.) + if (firstFace) + { + // first face -> we can start with any edge. + // add v0 - v1 edge + CurrentHorizon.Add(new Horizon(Faces[currFace].V0, Faces[currFace].V1, Faces[currFace].F0, currFace)); + // recursive call from that edge. + CalculateHorizon(eyePoint, CurrentHorizon[CurrentHorizon.Count - 1], Faces[currFace].F0, false); + // add v1 - v2 edge + CurrentHorizon.Add(new Horizon(Faces[currFace].V1, Faces[currFace].V2, Faces[currFace].F1, currFace)); + CalculateHorizon(eyePoint, CurrentHorizon[CurrentHorizon.Count - 1], Faces[currFace].F1, false); + // add v2 - v0 edge. + CurrentHorizon.Add(new Horizon(Faces[currFace].V2, Faces[currFace].V0, Faces[currFace].F2, currFace)); + CalculateHorizon(eyePoint, CurrentHorizon[CurrentHorizon.Count - 1], Faces[currFace].F2, false); + } + else + { + // not the first face, but still visible. + if (Faces[currFace].F0 == crossedEdge.From) // crossed edge was v0-v1 edge. + { + // add v1-v2 edge to horizon + CurrentHorizon.Add(new Horizon(Faces[currFace].V1, Faces[currFace].V2, Faces[currFace].F1, currFace)); + CalculateHorizon(eyePoint, CurrentHorizon[CurrentHorizon.Count - 1], Faces[currFace].F1, false); + // add v2-v0 edge to horizon + CurrentHorizon.Add(new Horizon(Faces[currFace].V2, Faces[currFace].V0, Faces[currFace].F2, currFace)); + CalculateHorizon(eyePoint, CurrentHorizon[CurrentHorizon.Count - 1], Faces[currFace].F2, false); + } + else if (Faces[currFace].F1 == crossedEdge.From) // crossed edge was v1-v2 edge + { + // So much time spent looking for a bug in this method, and it was simply just forgetting + // that we need to go in a certain order. + // we NEED to go v2-v0 then v0-v1....................... Oops. + // add v2-v0 edge to horizon + CurrentHorizon.Add(new Horizon(Faces[currFace].V2, Faces[currFace].V0, Faces[currFace].F2, currFace)); + CalculateHorizon(eyePoint, CurrentHorizon[CurrentHorizon.Count - 1], Faces[currFace].F2, false); + // add v0-v1 edge to horizon + CurrentHorizon.Add(new Horizon(Faces[currFace].V0, Faces[currFace].V1, Faces[currFace].F0, currFace)); + CalculateHorizon(eyePoint, CurrentHorizon[CurrentHorizon.Count - 1], Faces[currFace].F0, false); + } + else if (Faces[currFace].F2 == crossedEdge.From) // crossed edge was v2-v0 edge. + { + // add v0-v1 edge to horizon + CurrentHorizon.Add(new Horizon(Faces[currFace].V0, Faces[currFace].V1, Faces[currFace].F0, currFace)); + CalculateHorizon(eyePoint, CurrentHorizon[CurrentHorizon.Count - 1], Faces[currFace].F0, false); + // add v1-v2 edge to horizon + CurrentHorizon.Add(new Horizon(Faces[currFace].V1, Faces[currFace].V2, Faces[currFace].F1, currFace)); + CalculateHorizon(eyePoint, CurrentHorizon[CurrentHorizon.Count - 1], Faces[currFace].F1, false); + } + } + } + } + + /// + /// Creates a mesh from the face data of the convex hull. + /// + /// + /// A mesh of the convex hull + private Mesh CreateMesh(List allFaces) + { + Mesh m = new Mesh(); + List vertices = new List(); + // filter faces based on which are still on the convex hull. + List faces = allFaces.Where(face => face.OnConvexHull).ToList(); + List normals = new List(); + int[] triangles = new int[faces.Count * 3]; + int t0, t1, t2 = t1 = t0 = 0; + for (int i = 0; i < faces.Count; i++) + { + // QH-SMOOTHED + // shared vertices smooth + // since we just add normals together and normalize them, and verts can be shared by n faces + // it's not really proper smoothing. + t0 = vertices.IndexOf(VerticesList[faces[i].V0]); + t1 = vertices.IndexOf(VerticesList[faces[i].V1]); + t2 = vertices.IndexOf(VerticesList[faces[i].V2]); + if (t0 < 0) + { + normals.Add(faces[i].Normal); + vertices.Add(VerticesList[faces[i].V0]); + t0 = vertices.Count - 1; + } + else + { + normals[t0] = (normals[t0] + faces[i].Normal).normalized; + } + if (t1 < 0) + { + normals.Add(faces[i].Normal); + vertices.Add(VerticesList[faces[i].V1]); + t1 = vertices.Count - 1; + } + else + { + normals[t1] = (normals[t1] + faces[i].Normal).normalized; + } + if (t2 < 0) + { + normals.Add(faces[i].Normal); + vertices.Add(VerticesList[faces[i].V2]); + t2 = vertices.Count - 1; + } + else + { + normals[t2] = (normals[t2] + faces[i].Normal).normalized; + } + //QH_SMOOTHED + // all vertices are added and arent shared so they aren't smoothed at all. + //QH-UNSMOOTHED + // vertices.Add(VerticesList[faces[i].V0]); + // t0 = vertices.Count - 1; + // vertices.Add(VerticesList[faces[i].V1]); + // t1 = vertices.Count - 1; + // vertices.Add(VerticesList[faces[i].V2]); + // t2 = vertices.Count - 1; + // normals.Add(faces[i].Normal); + // normals.Add(faces[i].Normal); + // normals.Add(faces[i].Normal); + //QH_UNSMOOTHED + triangles[i * 3] = t0; + triangles[i * 3 + 1] = t1; + triangles[i * 3 + 2] = t2; + } + m.SetVertices(vertices); + m.SetTriangles(triangles, 0); + m.SetNormals(normals); + // m.RecalculateNormals(0); + return m; + } + + /// + /// Calculates the distance a point is from a line. + /// + /// how far away is this point + /// direction of line + /// a point on the line + /// + float DistanceFromLine(Vector3 point, Vector3 line, Vector3 pointOnLine) + { + Vector3 v = point - pointOnLine; + float dV = Vector3.Dot(v, line); + v = pointOnLine + dV * line; + return Vector3.Distance(v, point); + } + + /// + /// Calculates the signed distance of a point from a plane + /// + /// how far is this point + /// from this plane + /// Distance point is from plane. + float DistanceFromPlane(Vector3 point, Plane p) + { + return p.GetDistanceToPoint(point); + } + + /// + /// Calculate the signed distance of a point from a plane + /// + /// point + /// normal of the plane + /// a point on the plane. + /// + float DistanceFromPlane(Vector3 point, Vector3 normal, Vector3 pointOnPlane) + { + return Vector3.Dot(normal, point - pointOnPlane); + } + + /// + /// Finds the initial max points from which to build a hull from. Creates faces from these points. + /// + /// list of points + /// true if it found and made the faces, false otherwise. + bool FindInitialHull(List points) + { + List initialPoints; + bool initialPointsFound = false; + // Brain isn't working great right now, so two methods of finding initial points. + if (FindInitialPoints(points, out initialPoints)) + { + initialPointsFound = true; + } + else if (FindInitialPointsFallBack(points, out initialPoints)) + { + initialPointsFound = true; + } + if (initialPointsFound) + { + // we've found 6 valid points xMin,xMax & same for y, z. that are in a 3d point cloud. + // find the point which is furthest distance from the line defined by the first two points. + float maxDistance = -Mathf.Infinity; + int furthestLinePoint = 0; + Vector3 line = (points[initialPoints[1]] - points[initialPoints[0]]).normalized; + int furthestIndex = 0; + for (int i = 2; i < 6; i++) + { + float d = DistanceFromLine(points[initialPoints[i]], line, points[initialPoints[0]]); + if (isAGreaterThanB(d, maxDistance)) + { + maxDistance = d; + furthestLinePoint = initialPoints[i]; + furthestIndex = i; + } + } + // swap the points at the furthest index and the 3rd point. + initialPoints[furthestIndex] = initialPoints[2]; + initialPoints[2] = furthestLinePoint; + + // find the point which has the largest absolute distance from the plane defined by the first three points. + maxDistance = -Mathf.Infinity; + Plane p = new Plane(points[initialPoints[0]], points[initialPoints[1]], points[furthestLinePoint]); + int furthestPlanePoint = -1; + for (int i = 2; i < 6; i++) + { + if (initialPoints[i] == furthestLinePoint) continue; + float d = DistanceFromPlane(points[initialPoints[i]], p); + if (!IsApproxZero(d) && isAGreaterThanB(Mathf.Abs(d), maxDistance)) + { + furthestPlanePoint = initialPoints[i]; + maxDistance = d; + furthestIndex = i; + } + } + // if the furest plane point is still -1, all points are coplanar. + if (furthestPlanePoint == -1) + { + return false; + } + // swap the points + initialPoints[furthestIndex] = initialPoints[3]; + initialPoints[3] = furthestPlanePoint; + + // remember that if the distance from the fourth point was negative, the order of the first three vertices must be reversed. + if (DistanceFromPlane(points[furthestPlanePoint], p) < 0.0f) + { + int i1 = initialPoints[2]; + initialPoints[2] = initialPoints[0]; + initialPoints[0] = i1; + } + + // add the faces. (Creating a tetrahedron to start.) + Faces.Add(new Face(initialPoints[0], initialPoints[2], initialPoints[1], CalcNormal(points[initialPoints[0]], points[initialPoints[2]], points[initialPoints[1]]), 2, 3, 1)); + Faces.Add(new Face(initialPoints[0], initialPoints[1], initialPoints[3], CalcNormal(points[initialPoints[0]], points[initialPoints[1]], points[initialPoints[3]]), 0, 3, 2)); + Faces.Add(new Face(initialPoints[0], initialPoints[3], initialPoints[2], CalcNormal(points[initialPoints[0]], points[initialPoints[3]], points[initialPoints[2]]), 1, 3, 0)); + Faces.Add(new Face(initialPoints[1], initialPoints[2], initialPoints[3], CalcNormal(points[initialPoints[1]], points[initialPoints[2]], points[initialPoints[3]]), 0, 2, 1)); + + UnAssignedVertices.UnionWith(Enumerable.Range(0, points.Count)); + // keep track of all vertices that were assigned. + AssignedVertices = new HashSet(); + foreach (Face f in Faces) + { + AddToOutsideSet(f, UnAssignedVertices); + } + // remove all vertices that weren't assigned at all, as they are inside or merged, so not part of the convex hull + // ClosedVertices = new HashSet(); + ClosedVertices.UnionWith(UnAssignedVertices); + ClosedVertices.ExceptWith(AssignedVertices); + return true; + } + return false; + } + + /// + /// Fallback old method of finding initial points. + /// + /// + /// + /// + bool FindInitialPointsFallBack(List points, out List initialPoints) + { + List ips = new List(6) { -1, -1, -1, -1, -1, -1 }; + initialPoints = new List(6) { -1, -1, -1, -1, -1, -1 }; + // keep track of points of x,y,z min and max. + // could just be floats since we're only using them in comparisons and tracking the actual indexs. + Vector3 xMin, yMin, zMin = yMin = xMin = new Vector3(Mathf.Infinity, Mathf.Infinity, Mathf.Infinity); + Vector3 xMax, yMax, zMax = yMax = xMax = new Vector3(-Mathf.Infinity, -Mathf.Infinity, -Mathf.Infinity); + for (int i = 0; i < points.Count; i++) + { + // using epislon make sure the new point is less than the min + // if they are the same as the current, and they are used in multiple places, replace it with the new one. + // (otherwise the same point can be (example) both xMin and zMin, and result in the initial faces being coplanar) when there are other points to use. + if (isALessThanB(points[i].x, xMin.x) || (isApproxEqual(points[i].x, xMin.x) && initialPoints.FindAll(element => element == ips[0]).Count > 1)) + { + // keep track of the index of the point + initialPoints[0] = i; + ips[0] = i; + // set the minimum point. + xMin = points[i]; + } + if (isAGreaterThanB(points[i].x, xMax.x) || (isApproxEqual(points[i].x, xMax.x) && initialPoints.FindAll(element => element == ips[1]).Count > 1)) + { + initialPoints[1] = i; + ips[1] = i; + xMax = points[i]; + } + if (isALessThanB(points[i].y, yMin.y) || (isApproxEqual(points[i].y, yMin.y) && initialPoints.FindAll(element => element == ips[2]).Count > 1)) + { + initialPoints[2] = i; + ips[2] = i; + yMin = points[i]; + } + if (isAGreaterThanB(points[i].y, yMax.y) || (isApproxEqual(points[i].y, yMax.y) && initialPoints.FindAll(element => element == ips[3]).Count > 1)) + { + initialPoints[3] = i; + ips[3] = i; + yMax = points[i]; + } + if (isALessThanB(points[i].z, zMin.z) || (isApproxEqual(points[i].z, zMin.z) && initialPoints.FindAll(element => element == ips[4]).Count > 1)) + { + initialPoints[4] = i; + ips[4] = i; + zMin = points[i]; + } + if (isAGreaterThanB(points[i].z, zMax.z) || (isApproxEqual(points[i].z, zMax.z) && initialPoints.FindAll(element => element == ips[5]).Count > 1)) + { + initialPoints[5] = i; + ips[5] = i; + zMax = points[i]; + } + } + if (!isApproxEqual(xMin.x, xMax.x) && !isApproxEqual(yMin.y, yMax.y) && !isApproxEqual(zMin.z, zMax.z)) + { + return true; + // we're good. + } + return false; + } + + /// + /// Finds initial 6 points of min/max in x, y, z dimensions. + /// Now finds the first 4 non-coplanar points + 2 extra points. + /// + /// list of points + /// initial pointss list of length 6. + /// true if it finds the initial points [xMin, xMax, yMin, yMax, zMin, zMax], false otherwise + bool FindInitialPoints(List points, out List initialPoints) + { + // just find the first 4 non-coplanar points. + initialPoints = new List(6) { -1, -1, -1, -1, -1, -1 }; + Vector3 a, b, c, d = a = b = c = Vector3.zero; + // search 4 consecutive points for a polyhedron with a volume. + for (int i = 0; i < points.Count; i++) + { + if (i + 3 >= points.Count || i + 2 >= points.Count || i + 1 >= points.Count) continue; + a = points[i]; + b = points[i + 1]; + c = points[i + 2]; + d = points[i + 3]; + // volume = | (a-d) dot ((b-d) cross (c - d)) | / 6. + float v = Mathf.Abs(Vector3.Dot((a - d), Vector3.Cross((b - d), (c - d)))) / 6; + // non zero volume = 4 points are not coplanar. + if (!IsApproxZero(v)) + { + initialPoints[0] = i; + initialPoints[1] = i + 1; + initialPoints[2] = i + 2; + initialPoints[3] = i + 3; + if (i + 4 < points.Count) + { + initialPoints[4] = i + 4; + } + else { initialPoints[4] = i; } + if (i + 5 < points.Count) + { + initialPoints[5] = i + 5; + } + else { initialPoints[5] = i; } + return true; + } + else + { + // just swap the last point to find a non-coplanar point. + for (int j = i + 4; j < points.Count; j++) + { + d = points[j]; + v = Mathf.Abs(Vector3.Dot((a - d), Vector3.Cross((b - d), (c - d)))) / 6; + if (!IsApproxZero(v)) + { + initialPoints[0] = i; + initialPoints[1] = i + 1; + initialPoints[2] = i + 2; + initialPoints[3] = j; + if (i + 4 < points.Count) + { + initialPoints[4] = i + 4; + } + else { initialPoints[4] = i; } + if (i + 5 < points.Count) + { + initialPoints[5] = i + 5; + } + else { initialPoints[5] = i; } + return true; + } + } + } + } + return false; + } + + /// + /// Is the calculation finished? (The Result has been generated) + /// + /// true if result != null, false otherwise + public bool isFinished + { + get + { + return (Result != null); + } + } + + /// + /// Given a set of points, calculate an appropriate epislon value for quickhull-ing + /// + /// local space points + private void CalculateEpsilon(List points) + { + // given a set of points determine an appropriate epislon value to use for quickhull. + // epislon is relative to maximum abs values of x,y,and z. + // float maxX, maxY, maxZ = maxY = maxX = -Mathf.Infinity; + Vector3 min = new Vector3(Mathf.Infinity, Mathf.Infinity, Mathf.Infinity); + Vector3 max = new Vector3(-Mathf.Infinity, -Mathf.Infinity, -Mathf.Infinity); + foreach (Vector3 v in points) + { + if (v.x < min.x) + { + min.x = v.x; + } + if (v.y < min.y) + { + min.y = v.y; + } + if (v.z < min.z) + { + min.z = v.z; + } + if (v.x > max.x) + { + max.x = v.x; + } + if (v.y > max.y) + { + max.y = v.y; + } + if (v.z > max.z) + { + max.z = v.z; + } + } + Epsilon = Vector3.Distance(min, max) * 0.000001f; + } + + + /// + /// Generates a convex hull from a list of local space points. + /// The resulting mesh is placed in the result variable. + /// + /// List of local space points. + public void GenerateHull(List points) + { + CalculateEpsilon(points); + VerticesList = points; + if (FindInitialHull(points)) + { + // while there is a current face on the current hull which has a non-empty outside set of vertices. + while (HaveNonEmptyFaceSet())// && whileLoopedCount < DebugMaxLoopNumber) + { + // clear unassigned vertices. + UnAssignedVertices = new HashSet(); + // clear the current horizon. + CurrentHorizon = new List(); + // get the non empty face + int currFace = GetNonEmptyFaceIndex(); + // find the point on the currFaces' surface which is farthest away from the plane of currFace. this is the eyePoint + int eyePoint = GetFurthestPointFromFace(currFace); + // compute horizon of current poly as seen from eyePoint (CALCULATE_HORIZON) + // this will mark all visible faces as not on the convex hull and place all of their outside set points + // on the list listUnclaimedVertices. it creates a list HorizonEdges which is + // a counter clockwise ordered list of edges around the horizon contour of the polyhedron as viewed from the eyepoint. + CalculateHorizon(eyePoint, null, currFace, true); + // calculate horizon updates the unassigned vertices, so we need to update the assigned vertices + AssignedVertices.ExceptWith(UnAssignedVertices); + // construct a cone from the eye point to all of the edges of the horizon. + // start face (used for last valid face that is added.) + int startFace = Faces.Count; + // end face (used for first valid face that is added.) + int endFace = Faces.Count + CurrentHorizon.Where(item => item.OnConvexHull).ToList().Count - 1; + // total number of valid valids (used to see if we're currently adding the last valid face.) + int totalValidHorizons = CurrentHorizon.Where(item => item.OnConvexHull).ToList().Count; + // reset the new faces list + NewFaces = new List(); + // count the number of valid faces we've added. + int validHorizonsDone = 0; + for (int i = 0; i < CurrentHorizon.Count; i++) + { + // makes it easier than always typing currenthorizon[i]. + Horizon h = CurrentHorizon[i]; + if (!h.OnConvexHull) { continue; } + + if (validHorizonsDone == 0) + { + // add a new face that has edge v0, v1 and eye point, calculate the normal, the shared edge (v0,v1) face is the the horizon's face. + // the next face is the next shared edge, the end face is the last face. + Faces.Add(new Face(h.V0, h.V1, eyePoint, CalcNormal(h.V0, h.V1, eyePoint), h.Face, Faces.Count + 1, endFace)); + } + else if (validHorizonsDone == totalValidHorizons - 1) + { + // this is the last face, the number of horizon faces we've added is the total count of valid faces. + Faces.Add(new Face(h.V0, h.V1, eyePoint, CalcNormal(h.V0, h.V1, eyePoint), h.Face, startFace, Faces.Count - 1)); + } + else + { + // add previous face, and next face as it's other faces. + Faces.Add(new Face(h.V0, h.V1, eyePoint, CalcNormal(h.V0, h.V1, eyePoint), h.Face, Faces.Count + 1, Faces.Count - 1)); + } + // keep track of the added face. + NewFaces.Add(Faces.Count - 1); + // update the face of the horizon to share the new edge with the new face. + UpdateFace(h, Faces.Count - 1); + + validHorizonsDone++; + } + // We had an error somewhere on the connected face not correctly being set to the correct face but I can't find the source of the bug, + // and so we are just going to force verification of all of the new faces that were just added. + // The error was in the recursive find horizon method the whole time. + // FIX_FACE_VERIFY (So if someone has an issue in the future, they can just uncomment the foreach loop below and it should all work but slower) + // foreach (int i in NewFaces) + // { + // ForceUpdateFace(i); + // } + + // All the vertices of removed faces were added to the unassigned vertices list. + // update the closed vertices list before assigning an unassigned vertex to any face's outside set. + CloseUnAssignedVertsOnFaces(); + // the remaining unassigned vertices can all be added to the new faces that were created. + for (int i = 0; i < NewFaces.Count; i++) + { + // "randomly" assign the points to the new faces outside sets. + AddToOutsideSet(Faces[NewFaces[i]], UnAssignedVertices); + } + // mark vertices as closed that are still unassigned as they are etiher on or in the convex hull + UnAssignedVertices.ExceptWith(AssignedVertices); + ClosedVertices.UnionWith(UnAssignedVertices); + } + // create the mesh from the list of faces. + Result = CreateMesh(Faces); + } + else + { + // Removed warning, as it can happen too often when selecting a face. + // Debug.LogWarning("EasyColliderEditor: Unable to find initial points, likely because all points lie on the same plane."); + } + } + + /// + /// gets the verticeslist index of the furthest point in a face's outside set (furthest signed distance) + /// + /// face we want the furthest point from + /// index of vertex at a positive signed distance furthest from the face + private int GetFurthestPointFromFace(int faceIndex) + { + Face face = Faces[faceIndex]; + float maxDistance = -Mathf.Infinity; + int furthestIndex = -1; + foreach (int i in face.OutsideVertices) + { + float d = DistanceFromPlane(VerticesList[i], face.Normal, VerticesList[face.V0]); + if (d > maxDistance) + { + furthestIndex = i; + maxDistance = d; + } + } + return furthestIndex; + } + + /// + /// Gets the index of the first face that has a non-empty outside set. + /// + /// index of the first face that has a non-empty outside set, -1 if none are found + private int GetNonEmptyFaceIndex() + { + for (int i = 0; i < Faces.Count; i++) + { + if (Faces[i].OutsideVertices.Count > 0) + { + return i; + } + } + return -1; + } + + /// + /// Do we have a face with a non-empty outside set? + /// + /// true if we have a non-empty outside set + private bool HaveNonEmptyFaceSet() + { + foreach (Face f in Faces) + { + if (f.OutsideVertices.Count > 0) + { + return true; + } + } + return false; + } + + /// + /// Checks if a > b by at least epsilon. + /// + /// + /// + /// true a > b + private bool isAGreaterThanB(float a, float b) + { + if (a - b > Epsilon) + { + return true; + } + return false; + } + + /// + /// Checks if a < b by at least epsilon + /// + /// + /// + /// true if a < b + private bool isALessThanB(float a, float b) + { + if (b - a > Epsilon) + { + return true; + } + return false; + } + + /// + /// Checks if a and b are approximately equal (the difference between them is < epsilon) + /// + /// a + /// b + /// true if they are approximately equal, false otherwise + private bool isApproxEqual(float a, float b) + { + return Mathf.Abs(a - b) < Epsilon; + } + + /// + /// Checks if value is approximately zero by comparing abs(value) < epsilon. + /// + /// a + /// true is a is approximately 0 + private bool IsApproxZero(float a) + { + return Mathf.Abs(a) < Epsilon; + } + + /// + /// Updates the faces of a horizon based on the new face created. + /// The face that was crossed from is no longer on the convex hull and is replaced with the new face in the correct + /// spot on horizon.Face's F0, F1, or F2. + /// + /// horizon edge + /// new face index + private void UpdateFace(Horizon horizon, int newFace) + { + // if the face is on the convex hull + if (Faces[horizon.Face].OnConvexHull) + { + // and f0's was the face we crossed the edge from + // the edge we crossed from is no longer on the convex hull and is replaced with the new face. + if (Faces[horizon.Face].F0 == horizon.From) + { + // set that face to the new face + Faces[horizon.Face].F0 = newFace; + } + else if (Faces[horizon.Face].F1 == horizon.From) + { + Faces[horizon.Face].F1 = newFace; + } + else if (Faces[horizon.Face].F2 == horizon.From) + { + Faces[horizon.Face].F2 = newFace; + } + } + } + + + + //Debugging methods below left in for future possible use for bug-fixing. + + /// + /// Calculates the center of a face. + /// + Vector3 CalcFaceCenter(Face face) + { + return (VerticesList[face.V0] + VerticesList[face.V1] + VerticesList[face.V2]) / 3; + } + + void DebugInitialPoints(List points, List initialPoints) + { + string ints = ""; + string vals = ""; + foreach (int i in initialPoints) + { + ints += i + " : "; + vals += points[i] + " : "; + } + } + + /// + /// Draws a faces points. + /// + void DrawFace(int face, Color color, float size = 0.08f) + { + Face f = Faces[face]; + DrawPoint(VerticesList[f.V0], color, size); + DrawPoint(VerticesList[f.V1], color, size); + DrawPoint(VerticesList[f.V2], color, size); + } + + /// + /// Draws the normal of faces conected to the face provided. (f0 = r, f1 = g, f2 = b) + /// + /// Face to draw neighbours of + void DrawFaceConnections(int face) + { + // DrawFaceNormal(Faces[face], Color.black); + DrawFaceNormal(Faces[Faces[face].F0], Color.red, 1.025f); + DrawFaceNormal(Faces[Faces[face].F1], Color.green, 1.05f); + DrawFaceNormal(Faces[Faces[face].F2], Color.blue, 1.075f); + } + + /// + /// draws a faces normal + /// + void DrawFaceNormal(Face face, Color color, float distance = 1.0f) + { + Vector3 center = CalcFaceCenter(face); + Debug.DrawLine(center, center + face.Normal * distance, color, DrawTime); + } + + /// + /// Force verifys faces. + /// Was used to help solve the issue with incorrect horizon finding. + /// Left in for future issues and solutions. + /// + /// Index of face + void ForceUpdateFace(int faceIndex) + { + bool needsToBeRepaired = true; + if (needsToBeRepaired) + { + Face f = Faces[faceIndex]; + Face o = null; + for (int i = 0; i < Faces.Count; i++) + { + if (faceIndex == i) { continue; } + if (!Faces[i].OnConvexHull) { continue; } + o = Faces[i]; + if ((f.V0 == o.V0 || f.V0 == o.V1 || f.V0 == o.V2) && (f.V1 == o.V0 || f.V1 == o.V1 || f.V1 == o.V2)) // v0-v1 edge shared + { + f.F0 = i; + } + else if ((f.V2 == o.V0 || f.V2 == o.V1 || f.V2 == o.V2) && (f.V1 == o.V0 || f.V1 == o.V1 || f.V1 == o.V2)) //v1-v2 edge shared + { + f.F1 = i; + } + else if ((f.V0 == o.V0 || f.V0 == o.V1 || f.V0 == o.V2) && (f.V2 == o.V0 || f.V2 == o.V1 || f.V2 == o.V2)) //v2-v0 edge shared. + { + f.F2 = i; + } + } + } + } + + /// + /// Generates a random color. + /// + /// + Color RandomColor() + { + return new Color(Random.Range(0f, 1f), Random.Range(0f, 1f), Random.Range(0f, 1f)); + } + + /// + /// Draws a point at poisition + /// + /// point to draw + /// color to draw with + /// size to draw point + void DrawPoint(Vector3 point, Color color, float size = 0.05f) + { + Debug.DrawLine(point - Vector3.up * size, point + Vector3.up * size, color, DrawTime); + Debug.DrawLine(point - Vector3.left * size, point + Vector3.left * size, color, DrawTime); + Debug.DrawLine(point - Vector3.forward * size, point + Vector3.forward * size, color, DrawTime); + } + } +} diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderQuickHull.cs.meta b/Assets/EasyColliderEditor/Scripts/EasyColliderQuickHull.cs.meta new file mode 100644 index 00000000..3c229026 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderQuickHull.cs.meta @@ -0,0 +1,20 @@ +fileFormatVersion: 2 +guid: ed797196cc351a34db4a5f88338cc1fe +timeCreated: 1600696552 +licenseType: Store +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/EasyColliderQuickHull.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderRotateDuplicate.cs b/Assets/EasyColliderEditor/Scripts/EasyColliderRotateDuplicate.cs new file mode 100644 index 00000000..08d97cd3 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderRotateDuplicate.cs @@ -0,0 +1,28 @@ +using UnityEngine; +namespace ECE +{ + [System.Serializable] + public class EasyColliderRotateDuplicate + { + public enum ROTATE_AXIS + { + X, + Y, + Z, + } + + public bool enabled; + + public ROTATE_AXIS axis; + + public int NumberOfDuplications = 4; + + public float StartRotation = 0.0f; + + public float EndRotation = 360f; + + public GameObject pivot; + + public GameObject attachTo; + } +} \ No newline at end of file diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderRotateDuplicate.cs.meta b/Assets/EasyColliderEditor/Scripts/EasyColliderRotateDuplicate.cs.meta new file mode 100644 index 00000000..6a7a6882 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderRotateDuplicate.cs.meta @@ -0,0 +1,20 @@ +fileFormatVersion: 2 +guid: 5e6a373c203fb6b459f89497990e1b24 +timeCreated: 1618839991 +licenseType: Store +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/EasyColliderRotateDuplicate.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderSaving.cs b/Assets/EasyColliderEditor/Scripts/EasyColliderSaving.cs new file mode 100644 index 00000000..4551c6ca --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderSaving.cs @@ -0,0 +1,397 @@ +#if(UNITY_EDITOR) +using UnityEngine; +using UnityEditor; +using System.IO; +#if (UNITY_2021_2_OR_NEWER) +// prefab stage out of experimental. +using UnityEditor.SceneManagement; +#elif (UNITY_2018_3_OR_NEWER) +// prefabstage is still experimental +// but what it inherits from, PreviewSceneStage/Stage is not experimental as of 2020.1 +using UnityEditor.Experimental.SceneManagement; +#endif +namespace ECE +{ + public static class EasyColliderSaving + { + public class PrefabData + { + public UnityEngine.Object FoundObject; + public string AssetPath; + } + + static PrefabData TryFindPrefabObject(GameObject go) + { + UnityEngine.Object foundObject = null; + PrefabData foundData = new PrefabData(); +#if UNITY_2018_2_OR_NEWER + // 2018_2+ + PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage(); + if (stage != null) + { + if (stage.prefabContentsRoot != null) + { + foundObject = PrefabUtility.GetCorrespondingObjectFromSource(stage.prefabContentsRoot); + } +#if (UNITY_2020_1_OR_NEWER) + // asset path in 2020.1+ + foundData.AssetPath = stage.assetPath; +#elif (UNITY_2018_3_OR_NEWER) + // prefab asset path 2018.3 to 2019.4 + foundData.AssetPath = stage.prefabAssetPath; +#endif + } + + // try using just the GO if were not in a prefab stage. + if (foundObject == null) + { + foundObject = PrefabUtility.GetCorrespondingObjectFromSource(go); + if (foundObject == null) + { + foundObject = PrefabUtility.GetOutermostPrefabInstanceRoot(go); + } + } + +#else + // legacy unity support. -- (I dont test in earlier than 2019.4+ anymore) + foundObject = PrefabUtility.GetPrefabParent(go); + if (foundObject == null) + { + foundObject = PrefabUtility.FindPrefabRoot(go); + } +#endif + foundData.FoundObject = foundObject; + return foundData; + } + + static PrefabData TryFindMeshOrSkinnedMeshObject(GameObject go) + { + PrefabData foundPrefabData = new PrefabData(); + MeshFilter mf = go.GetComponent(); + if (mf != null) + { + foundPrefabData.FoundObject = mf.sharedMesh; + } + else + { + SkinnedMeshRenderer smr = go.GetComponent(); + if (smr != null) + { + foundPrefabData.FoundObject = smr.sharedMesh; + } + } + return foundPrefabData; + } + + /// + /// Static preferences asset that is currently loaded. + /// + /// + static EasyColliderPreferences ECEPreferences { get { return EasyColliderPreferences.Preferences; } } + + + /// + /// removes invalid characters (so path is valid) and trailing slashes (for compatibility with older versions of unity) + /// + /// + /// Assets/Folder1/Folder + static string CleanAssetPath(string path) + { + //remove any invalid path characters. + path = string.Join("", path.Split(Path.GetInvalidPathChars())); + // no trailing slash is more compatible with older version of unity and AssetDatabase.IsValidFolder, so prefer no trailing slash when checking. + // folder path is saved with / at the end, so remove if we have to. + if (path[path.Length - 1] == '/') + { + path = path.Remove(path.Length - 1); + } + return path; + } + + + /// + /// Gets a valid path to save a convex hull at to feed into save convex hull meshes function. + /// + /// selected gameobject + /// preferences object + /// assetdb path like: Assets/Folder/OptionSubfolder/baseObjectsName + public static string GetValidConvexHullPath(GameObject go) + { + // use default specified path + // remove invalid characters from file name, just in case (user reported error, thanks!) + string goName = string.Join("_", go.name.Split(Path.GetInvalidFileNameChars())); + // start with the default path specified in preferences. + string path = ECEPreferences.SaveConvexHullPath; + // get path to gameobject + if (ECEPreferences.ConvexHullSaveMethod != CONVEX_HULL_SAVE_METHOD.Folder) + { + // bandaid for scaled temporary skinned mesh: + // as the scaled mesh filter is added during setup with the name Scaled Mesh Filter (Temporary) + if (go.name.Contains("Scaled Mesh Filter")) + { + go = go.transform.parent.gameObject; // set the gameobject to the temp's parent (as that will be a part of the prefab if it is one and thus should work.) + } + PrefabData foundPrefabData = null; + + if (ECEPreferences.ConvexHullSaveMethod == CONVEX_HULL_SAVE_METHOD.Prefab) + { + foundPrefabData = TryFindPrefabObject(go); + } + else if (ECEPreferences.ConvexHullSaveMethod == CONVEX_HULL_SAVE_METHOD.Mesh) + { + foundPrefabData = TryFindMeshOrSkinnedMeshObject(go); + } + else if (ECEPreferences.ConvexHullSaveMethod == CONVEX_HULL_SAVE_METHOD.PrefabMesh) + { + foundPrefabData = TryFindPrefabObject(go); + if (foundPrefabData.FoundObject == null && string.IsNullOrEmpty(foundPrefabData.AssetPath)) + { + foundPrefabData = TryFindMeshOrSkinnedMeshObject(go); + } + } + else if (ECEPreferences.ConvexHullSaveMethod == CONVEX_HULL_SAVE_METHOD.MeshPrefab) + { + foundPrefabData = TryFindMeshOrSkinnedMeshObject(go); + if (foundPrefabData.FoundObject == null) + { + foundPrefabData = TryFindPrefabObject(go); + } + } + // by default, use the found AssetPath, this should be the outermost prefab which is what is wanted. + string pathToPrefabOrMesh = foundPrefabData.AssetPath; + if (string.IsNullOrEmpty(pathToPrefabOrMesh) && foundPrefabData.FoundObject != null) + { + pathToPrefabOrMesh = AssetDatabase.GetAssetPath(foundPrefabData.FoundObject); + } + // but only use the path it if it exists. + // here we trim the object name just down to the last / so Asset/FolderWithPrefabInIt/ + if (!string.IsNullOrEmpty(pathToPrefabOrMesh)) + { + int index = pathToPrefabOrMesh.LastIndexOf("/"); + if (index >= 0) + { + // removes object name. + path = pathToPrefabOrMesh.Remove(index + 1); + } + } + } + string originalPath = path; + path = CleanAssetPath(path); + + // AssetDatabase.IsValidFolder("Assets/"); // not valid in 2019 + // AssetDatabase.IsValidFolder("Assets"); // valid in 2019 and 6000. Prefer this one! + + + // prefab/mesh searched for a folder to save in and failed to find a valid path, default to the save convex hull path specified in preferences. + if ((!AssetDatabase.IsValidFolder(path) && path + "/" != ECEPreferences.SaveConvexHullPath) + // saving in a non-asset path and does not have allow saving convex hulls in packages enabled + || (!path.StartsWith("Assets") && !ECEPreferences.AllowSavingConvexHullsInPackages)) + { + path = ECEPreferences.SaveConvexHullPath; + path = CleanAssetPath(path); + // could not find a valid mesh/prefab location, but the fallback save convex hull folder DOES work. + if (AssetDatabase.IsValidFolder(path)) + { + Debug.LogWarning("Easy Collider Editor: Could not find a valid location to save the collider. Saving in the folder specified in preferences: " + path); + } + } + + // path and path fallback both are clean and have no trailing / here. + + // this will automatically be true if we're using a folder (and it failed) OR the fallback on prefab/mesh fails (which also uses SaveConvexHullPath) + if (!AssetDatabase.IsValidFolder(path) && (path + "/").Contains(ECEPreferences.SaveConvexHullPath)) + { + // path to ece preferences (in scripts typically) + path = AssetDatabase.GetAssetPath(MonoScript.FromScriptableObject(ECEPreferences)); + // because the above is the full path to the file including the files name. + path = path.Remove(path.LastIndexOf("/")); + path = path.Replace("/Scripts", ""); + path = string.Join("", path.Split(Path.GetInvalidPathChars())); + // asset path for preferences file is invalid, not in assets folder a + if (path.StartsWith("Assets") || ECEPreferences.AllowSavingConvexHullsInPackages) + { + if (!AssetDatabase.IsValidFolder(path + "/Convex Hulls")) + { + AssetDatabase.CreateFolder(path, "Convex Hulls"); + AssetDatabase.Refresh(); + } + path = path + "/Convex Hulls"; + if (originalPath == ECEPreferences.SaveConvexHullPath) + { + // original path was saving to a folder. + ECEPreferences.SaveConvexHullPath = path + "/"; + Debug.LogWarning("Easy Collider Editor: Convex Hull save path specified in Easy Collider Editor's preferences could not be found, or it is an invalid asset folder. Saving in: " + path + " as a fallback and updating preferences. If the folder has been moved or deleted, update to a different folder in the edit preferences foldout.\n\n Original path: " + originalPath); + } + // so that if we already have a valid + else if (!ECEPreferences.SaveConvexHullPath.StartsWith("Assets")) + { + // used an alternative mesh/prefab/prefabmesh save method. + ECEPreferences.SaveConvexHullPath = path + "/"; + Debug.LogWarning("Easy Collider Editor: Could not find a valid location to save the collider and the save path specified in preferences could not be found, or it is an invalid asset folder. Saving in: " + path + " and updated preferences to this folder. If the folder has been moved or deleted, update to a different folder in the edit preferences foldout.\n\n Original path: " + originalPath); + } + } + else // in a packages folder probably without AllowSavingConvexHullsInPackages enabled. + { + // final fallback. + path = "Assets/Convex Hulls"; + if (originalPath == ECEPreferences.SaveConvexHullPath) + { + // change default path to a valid path. + ECEPreferences.SaveConvexHullPath = path + "/"; +#if (UNITY_2020_3_OR_NEWER) + AssetDatabase.SaveAssetIfDirty(ECEPreferences); +#endif + } + if (!AssetDatabase.IsValidFolder(path)) + { + AssetDatabase.CreateFolder("Assets", "Convex Hulls"); + AssetDatabase.Refresh(); + if (originalPath == ECEPreferences.SaveConvexHullPath) + { + // original path was saving to a folder. + Debug.LogWarning("Easy Collider Editor: Convex Hull save path specified in Easy Collider Editor's preferences could not be found, or it is an invalid asset folder. A folder to save collider assets was created at: " + path + " and automatically set in preferences. A different folder in Easy Collider Editor's preferences foldout can be specified if desired.\n\n Original path: " + originalPath); + } + else + { + // used an alternative mesh/prefab/prefabmesh save method. + Debug.LogWarning("Easy Collider Editor: Could not find a valid location to save the collider and the save path specified in preferences could not be found, or it is an invalid asset folder. A new folder has been created at " + path + " to save mesh collider assets and automatically set in preferences. A different folder in Easy Collider Editor's preferences foldout can be specified if desired. \n\n Original path: " + originalPath); + } + } + } + } + + // if they want a subfolder, create it if needed and use it. + if (!string.IsNullOrEmpty(ECEPreferences.SaveConvexHullSubFolder)) + { + if (!AssetDatabase.IsValidFolder(path + "/" + ECEPreferences.SaveConvexHullSubFolder)) + { + AssetDatabase.CreateFolder(path, ECEPreferences.SaveConvexHullSubFolder); + } + path += "/" + ECEPreferences.SaveConvexHullSubFolder + "/"; + path = CleanAssetPath(path); + } + string fullPath = path + "/" + goName; + return fullPath; + } + + /// + /// goes thorugh the path and finds the first non-existing path that can be used to save. + /// + /// Full path up to save location: ie C:/UnityProjects/ProjectName/Assets/Folder/baseObject + /// Suffix to add to save files ie _Suffix_ + /// first valid path for AssetDatabase.CreateAsset ie baseObject_Suffix_0 + private static string GetFirstValidAssetPath(string path, string suffix) + { + + string validPath = path; + if (File.Exists(validPath + suffix + "0.asset")) + { + int i = 1; + while (File.Exists(validPath + suffix + i + ".asset")) + { + i += 1; + } + validPath += suffix + i + ".asset"; + } + else + { + validPath += suffix + "0.asset"; + } + + // replace application's data path (Unity Editor: /Assets) + // "Assets" earlier in the path should no longer cause issues. + validPath = validPath.Replace(Application.dataPath, "Assets"); + return validPath; + } + + /// + /// Creates and saves a mesh asset in the asset database with appropriate path and suffix. + /// + /// mesh + /// gameobject the mesh will be attached to, used to find asset path. + public static void CreateAndSaveMeshAsset(Mesh mesh, GameObject attachTo) + { + if (mesh != null && !DoesMeshAssetExists(mesh)) + { + string savePath = GetValidConvexHullPath(attachTo); + if (savePath != "") + { + string assetPath = GetFirstValidAssetPath(savePath, ECEPreferences.SaveConvexHullSuffix); + AssetDatabase.CreateAsset(mesh, assetPath); + AssetDatabase.SaveAssets(); + } + } + } + + /// + /// Checks if the asset already exists (needed for rotate and duplicate, as the mesh is the same mesh repeated.) + /// + /// + /// + public static bool DoesMeshAssetExists(Mesh mesh) + { + string p = AssetDatabase.GetAssetPath(mesh); + if (p == null || p.Length == 0) + { + return false; + } + return true; + } + + + + /// + /// Creates and saves an array of mesh assets in the assetdatabase at the path with the the format "savePath"+"suffix"+[0-n].asset + /// + /// Full path up to save location: ie C:/UnityProjects/ProjectName/Assets/Folder/baseObject + /// Suffix to add to save files ie _Suffix_ + public static Mesh[] CreateAndSaveMeshAssets(Mesh[] meshes, string savePath, string suffix) + { + string firstAssetPath = null; + int assetSuffixIndex = -1; + for (int i = 0; i < meshes.Length; i++) + { + // get a new valid path for each mesh to save. + string path = GetFirstValidAssetPath(savePath, suffix); + try + { + if (ECEPreferences.CombinedVHACDColliders && firstAssetPath != null) + { + //adding a name to the mesh even though it isn't required to match the first assets name as it by default has the path's name. + string name = firstAssetPath.Remove(assetSuffixIndex, firstAssetPath.Length - assetSuffixIndex); + name = name.Remove(0, name.LastIndexOf("/") + 1); + meshes[i].name = name + suffix + i.ToString(); + AssetDatabase.AddObjectToAsset(meshes[i], firstAssetPath); + } + else + { + AssetDatabase.CreateAsset(meshes[i], path); + } + } + catch (System.Exception error) + { + Debug.LogError("Error saving at path:" + path + ". Try changing the save Convex Hull path in Easy Collider Editor's preferences to a different folder.\n" + error.ToString()); + } + if (firstAssetPath == null) + { + firstAssetPath = path; + assetSuffixIndex = firstAssetPath.IndexOf(suffix); + } + } + AssetDatabase.SaveAssets(); + if (ECEPreferences.CombinedVHACDColliders) + { + // need to reload the assets and update the meshes array otherwise they don't point to the correct object + // only the first one will without this as for whatever reason create asset will correctly link the objects but adding an object to an asset will not. + var assets = AssetDatabase.LoadAllAssetsAtPath(firstAssetPath); + for (int i = 0; i < assets.Length; i++) + { + meshes[i] = (Mesh)assets[i]; + } + } + return meshes; + } + + } +} +#endif diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderSaving.cs.meta b/Assets/EasyColliderEditor/Scripts/EasyColliderSaving.cs.meta new file mode 100644 index 00000000..2682e5f8 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderSaving.cs.meta @@ -0,0 +1,20 @@ +fileFormatVersion: 2 +guid: 6ddf66fc4a34fe740aee3cf08e0549af +timeCreated: 1600696849 +licenseType: Store +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/EasyColliderSaving.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderTips.cs b/Assets/EasyColliderEditor/Scripts/EasyColliderTips.cs new file mode 100644 index 00000000..810d5db2 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderTips.cs @@ -0,0 +1,26 @@ +#if (UNITY_EDITOR) +namespace ECE +{ + /// + /// A list of tips for users of easy collider editor. + /// + public static class EasyColliderTips + { + public const string TRY_MOUSE_CONTROL = "Want to be able to select vertices with the mouse? Try enabling the mouse controls in preferences."; + public const string NEW_MOUSE_CONTROL = "Use left click to select the highlighted vertex, and the right click to select the point under the mouse."; + public const string COMPUTE_SHADER_TIP = "You're system's shader model does not support shaders with a compute buffer. Be sure to use the gizmo display method instead of shader in the preferences."; + public const string EDIT_PREFS_FORCED_FOCUSED = "You are editing preferences with forced window focus enabled. To make this easier, try disabling vertex selection, or the force focus scene option in preferences."; + public const string FORCED_FOCUSED_WINDOW = "Forced window focus is enabled, you may not be able to edit values easily by typing. Disable vertex selection, or the force focus scene option in preferences."; + public const string IN_PLAY_MODE = "Vertex selection is not enabled when in play mode."; + public const string NO_MESH_FILTER_FOUND = "No mesh filter is on the selected gameobject, try enabling include child meshes."; + public const string WRONG_FOCUSED_WINDOW = "Vertex selection only works when the scene view window is focused."; + + public const string ROTATED_BOX_COLLIDER_TIP = "The first 3 points determine the orientation of a rotated box collider."; + public const string ROTATED_CAPSULE_COLLIDER_TIP = "The first 2 points determine the orientation of a rotated capsule collider."; + + public const string COLLIDER_CREATION_SHORTCUTS_1 = "Shortcuts:\n\t[1-7] Change collider preview\n\t[~] Create the collider being previewed\n\tDouble Tap [1-7] Create the collider"; + + public const string AUTO_SKILLED_CONTROL_PARAMETERS = "Holding CTRL while adjusting a parameter in Per Bone Settings will also adjust all of it's children."; + } +} +#endif \ No newline at end of file diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderTips.cs.meta b/Assets/EasyColliderEditor/Scripts/EasyColliderTips.cs.meta new file mode 100644 index 00000000..6eac19f9 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderTips.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 3855a2c50be7c1742950276affa94392 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/EasyColliderTips.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderUIHelpers.cs b/Assets/EasyColliderEditor/Scripts/EasyColliderUIHelpers.cs new file mode 100644 index 00000000..8ea9719c --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderUIHelpers.cs @@ -0,0 +1,918 @@ +#if (UNITY_EDITOR) +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using System; +namespace ECE +{ + public static class EasyColliderUIHelpers + { + + + + + /// + /// Stores all the collider icons + /// + public static Texture2D[] ColliderIcons; + public static Texture2D[] ColliderMergeIcons; + + /// + /// Creates a bunch of buttons that function similar to an enum popup where only one can be selected. + /// + /// Current selected enum + /// Selected enum + public static Enum EnumButtonArray(Enum selected, string[] labels, string[] toolTips) + { + GUIStyle style = new GUIStyle(GUI.skin.button); + style.padding.left = 5; + style.padding.right = 5; + style.padding.bottom = 2; + style.padding.top = 0; + Array a = Enum.GetValues(selected.GetType()); + int currentValue = Convert.ToInt32(selected); + for (int i = 0; i < a.Length; i++) + { + GUIContent content = new GUIContent(labels[i], toolTips[i]); + if ((int)a.GetValue(i) == currentValue) + { + Color TempGUIColor = GUI.color; + GUI.color = _DisabledButtonColor; + GUILayout.Button(content, style); + GUI.color = TempGUIColor; + } + else + { + if (GUILayout.Button(content, style)) + { + return (Enum)a.GetValue(i); + } + } + } + return selected; + } + + /// + /// Creates buttons with icons as content that return an Enum when clicked. + /// + /// + /// + /// + /// + public static Enum EnumIconButtonArray(Enum selected, string[] iconContentNames, string[] toolTips) + { + GUIStyle style = new GUIStyle(GUI.skin.box); + style.padding = new RectOffset(0, 0, 0, 0); + style.margin = new RectOffset(0, 2, 4, 0); + Array a = Enum.GetValues(selected.GetType()); + int currentValue = Convert.ToInt32(selected); + for (int i = 0; i < a.Length; i++) + { + GUIContent content = EditorGUIUtility.IconContent(iconContentNames[i], toolTips[i]); + if ((int)a.GetValue(i) == currentValue) + { + Color TempGUIColor = GUI.color; + GUI.color = _DisabledButtonColor; + GUILayout.Button(content, style); + GUI.color = TempGUIColor; + } + else + { + if (GUILayout.Button(content, style)) + { + return (Enum)a.GetValue(i); + } + } + } + return selected; + } + + /// + /// Gets all the editor icons and loads them and stores them in an array + /// + public static void GetIcons() + { + ColliderIcons = new Texture2D[7]; + ColliderMergeIcons = new Texture2D[7]; + string[] locs = AssetDatabase.FindAssets("ECEUIBox t:texture2D"); + if (locs.Length > 0) + { + ColliderIcons[0] = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(locs[0])); + ColliderMergeIcons[0] = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(locs[1])); + } + locs = AssetDatabase.FindAssets("ECEUIRotatedBox t:texture2D"); + if (locs.Length > 0) + { + ColliderIcons[1] = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(locs[0])); + ColliderMergeIcons[1] = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(locs[1])); + } + locs = AssetDatabase.FindAssets("ECEUISphere t:texture2D"); + if (locs.Length > 0) + { + ColliderIcons[2] = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(locs[0])); + ColliderMergeIcons[2] = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(locs[1])); + } + locs = AssetDatabase.FindAssets("ECEUICapsule t:texture2D"); + if (locs.Length > 0) + { + ColliderIcons[3] = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(locs[0])); + ColliderMergeIcons[3] = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(locs[1])); + } + locs = AssetDatabase.FindAssets("ECEUIRotatedCapsule t:texture2D"); + if (locs.Length > 0) + { + ColliderIcons[4] = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(locs[0])); + ColliderMergeIcons[4] = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(locs[1])); + } + locs = AssetDatabase.FindAssets("ECEUIConvexMesh t:texture2D"); + if (locs.Length > 0) + { + ColliderIcons[5] = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(locs[0])); + ColliderMergeIcons[5] = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(locs[1])); + + } + locs = AssetDatabase.FindAssets("ECEUICylinder t:texture2D"); + if (locs.Length > 0) + { + ColliderIcons[6] = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(locs[0])); + ColliderMergeIcons[6] = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(locs[1])); + // ColliderMergeIcons[3] = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(locs[1])); + } + } + + /// + /// checks to see if the icons have moved / need to be found. + /// + public static void VerifyIcons() + { + bool needsUpdate = false; + if (ColliderIcons == null || ColliderMergeIcons == null) + { + GetIcons(); + } + else + { + foreach (Texture2D t2d in ColliderIcons) + { + if (t2d == null) + { + needsUpdate = true; + } + } + foreach (Texture2D t2d in ColliderMergeIcons) + { + if (t2d == null) + { + needsUpdate = true; + } + } + if (needsUpdate) + { + GetIcons(); + } + } + } + + + + /// + /// Helper method to create a button with a 32x32 Icon where the icons are found seperately and stored in an array. + /// + /// tooltip to display when button is enabled + /// tooltip to display when disabled + /// icon number in texture2d array + /// is this icon enable? + /// true if enabled and the button is clicked, false otherwise + public static bool DisableableIconButton(string tooltip, string disabledToolTip, int iconNumber, bool isEnabled) + { + VerifyIcons(); + if (ColliderIcons[iconNumber] == null) return false; // an icon was deleted. + GUIStyle style = new GUIStyle(GUI.skin.button); + style.imagePosition = ImagePosition.ImageAbove; + style.padding = new RectOffset(4, 4, 4, 4); + GUIContent content = new GUIContent("Text", ColliderIcons[iconNumber]); + GUI.enabled = isEnabled; + bool button = GUILayout.Button(content, style, GUILayout.ExpandWidth(false)); + GUI.enabled = true; + return button; + } + + + static string[] alphaKeys = new string[7] { "1", "2", "3", "4", "5", "6", "7" }; + private static string KeyCodeToString(KeyCode keyCode) + { + return alphaKeys[((int)keyCode - 257)]; + } + + + private static GUIStyle _highlightedStyle; + + private static GUIStyle HighLightedStyle + { + get + { + if (_highlightedStyle == null) + { + _highlightedStyle = new GUIStyle(GUI.skin.box); + Texture2D text = new Texture2D(1, 1); + text.SetPixel(0, 0, Color.red); + _highlightedStyle.normal.background = text; + _highlightedStyle.fixedHeight = 0; + _highlightedStyle.padding = new RectOffset(4, 4, 1, 16); + } + return _highlightedStyle; + } + } + + + private static GUIStyle _iconButtonLabelStyle; + private static GUIStyle IconButtonLabelStyle + { + get + { + if (_iconButtonLabelStyle == null) + { + _iconButtonLabelStyle = new GUIStyle(GUI.skin.box); + _iconButtonLabelStyle.richText = true; + _iconButtonLabelStyle.fixedHeight = 16; + _iconButtonLabelStyle.fixedWidth = 40; + _iconButtonLabelStyle.padding.bottom = 1; + _iconButtonLabelStyle.alignment = TextAnchor.LowerCenter; + } + return _iconButtonLabelStyle; + } + } + + + /// + /// Helper method to create a button with a 32x32 Icon where the icons are found seperately and stored in an array. + /// + /// tooltip to display when button is enabled + /// tooltip to display when disabled + /// icon number in texture2d array + /// is this icon enable? + /// true if enabled and the button is clicked, false otherwise + public static bool DisableableIconButtonShortcutCreation(string tooltip, string disabledToolTip, int iconNumber, bool isEnabled, KeyCode shortCut, bool highlight) + { + VerifyIcons(); + if (ColliderIcons.Length - 1 < iconNumber || ColliderIcons[iconNumber] == null) return false; // an icon was deleted. + GUIStyle style = new GUIStyle(GUI.skin.button); + + style.padding = new RectOffset(4, 4, 1, 16); + + + GUIStyle labelStyle = new GUIStyle(GUI.skin.box); + labelStyle.richText = true; + labelStyle.fixedHeight = 16; + labelStyle.fixedWidth = 40; + labelStyle.padding.bottom = 1; + labelStyle.alignment = TextAnchor.LowerCenter; + GUIContent content = new GUIContent(ColliderIcons[iconNumber]); + Rect r = GUILayoutUtility.GetRect(content, style); + style.fixedHeight = 48; + + + + GUI.enabled = isEnabled; + if (highlight && isEnabled) + { + Rect rect = new Rect(r.x - 2, r.y - 2, r.width + 4, r.height + 3); + Color bc = GUI.backgroundColor; + GUI.backgroundColor = new Color(0, 0.5f, 1f, 1f); + GUI.Box(rect, GUIContent.none, HighLightedStyle); + GUI.backgroundColor = bc; + } + style.fixedHeight = 48; + content.tooltip = isEnabled ? tooltip : disabledToolTip; + bool button = GUI.Button(r, content, style); + r.y += 32; + + // better dark mode colors, also light mode enabled/disabled button colors. + string colorCode = isEnabled ? "black" : "#525252"; + if (EditorGUIUtility.isProSkin) + { + colorCode = isEnabled ? "#C4C4C4" : "#6C6C6C"; + } + GUI.Label(r, "" + KeyCodeToString(shortCut) + "", IconButtonLabelStyle); + GUI.enabled = true; + return button; + } + + public static bool DisableableIconButtonShortcutMerge(string tooltip, string disabledToolTip, int iconNumber, bool isEnabled, KeyCode shortCut, bool highlight) + { + VerifyIcons(); + if (ColliderMergeIcons.Length - 1 < iconNumber || ColliderMergeIcons[iconNumber] == null) return false; // an icon was deleted. + GUIStyle style = new GUIStyle(GUI.skin.button); + style.padding = new RectOffset(4, 4, 1, 16); + + GUIStyle labelStyle = new GUIStyle(GUI.skin.box); + labelStyle.richText = true; + labelStyle.fixedHeight = 16; + labelStyle.fixedWidth = 40; + labelStyle.padding.bottom = 1; + labelStyle.alignment = TextAnchor.LowerCenter; + GUIContent content = new GUIContent(ColliderMergeIcons[iconNumber]); + Rect r = GUILayoutUtility.GetRect(content, style); + style.fixedHeight = 48; + + + + GUI.enabled = isEnabled; + if (highlight && isEnabled) + { + Rect rect = new Rect(r.x - 2, r.y - 2, r.width + 4, r.height + 3); + Color bc = GUI.backgroundColor; + GUI.backgroundColor = new Color(0, 0.5f, 1f, 1f); + GUI.Box(rect, GUIContent.none, HighLightedStyle); + GUI.backgroundColor = bc; + } + content.tooltip = isEnabled ? tooltip : disabledToolTip; + bool button = GUI.Button(r, content, style); + r.y += 32; + + // better dark mode colors, also light mode enabled/disabled button colors. + string colorCode = isEnabled ? "black" : "#525252"; + if (EditorGUIUtility.isProSkin) + { + colorCode = isEnabled ? "#C4C4C4" : "#6C6C6C"; + } + GUI.Label(r, "" + KeyCodeToString(shortCut) + "", IconButtonLabelStyle); + GUI.enabled = true; + return button; + } + + /// + /// Background GUI Color to use for buttons when they are disabled + /// + public static Color _DisabledButtonColor = new Color(0.7f, 0.7f, 0.7f, 1f); + + /// + /// Background GUI Color for toggles when they are disabled + /// + public static Color _DisabledToggleColor = new Color(1, 1, 1, 0.33f); + + + /// + /// Creates a disableable and undoable foldout list. + /// Allows you to pass a method to check if an item a user is trying to add is valid before adding to the list. + /// Has an X button beside each item to directly remove it, and a clear list button at the bottom. + /// + /// Object to record undo on + /// GUI Content of the foldout + /// Text to be displayed when the foldout is disabled and open + /// references to the bool controlling if the foldout is open + /// List of items to display + /// Type to use in object field + /// Method that returns true if the object should be added to the list + /// Is the foldout enabled? + /// List's type + public static void DisableableFoldoutList( + UnityEngine.Object obj, + GUIContent foldoutContent, + string disabledText, ref bool isOpen, + ref List list, + System.Type objType, + Func OnAddMethod, + bool isEnabled) where T : UnityEngine.Object + { + // createa foldout. + isOpen = EditorGUILayout.Foldout(isOpen, foldoutContent); + if (isOpen) // only display if the foldout is open. + { + if (isEnabled) // if the list is enabled, display it + { + // Variables to keep track of current item being displayed + T previous; // current item prior to it changing (if it does) + T current; // current item after changing (if it does) + List itemsToRemove = new List(); + for (int i = 0; i < list.Count; i++) + { + // each item is horizontally displayed + EditorGUILayout.BeginHorizontal(); + // quick removal button + if (GUILayout.Button("X", GUILayout.ExpandWidth(false))) + { + itemsToRemove.Add(list[i]); + } + else + { + // Create a field for each list + previous = list[i]; + current = (T)EditorGUILayout.ObjectField(list[i], objType, true); + if (current == null) // if the new item is null, mark for removal. + { + itemsToRemove.Add(list[i]); + } + // otherwise, call the onadd method to see if the item in the field should be added to the list + else if (!OnAddMethod(current)) + { + // if it shouldn't be added, then set the item to the previous item + // this cleans up items that are in the list when the onadd method uses different parameters + if (!OnAddMethod(previous)) + { + // if that item is also invalid, remove it fromt he list. + itemsToRemove.Add(list[i]); + } + } + else + { + // the new item is valid, set that list item to that value. + list[i] = current; + } + } + EditorGUILayout.EndHorizontal(); + } + if (itemsToRemove.Count > 0) + { + // record the removal of all items to remove. + Undo.RecordObject(obj, "Change List"); + foreach (T item in itemsToRemove) list.Remove(item); + } + // Use an empty object field at the bottom as a way to quickly add to the list. + current = (T)EditorGUILayout.ObjectField(null, objType, true); + // if its not null and passes the OnAddMethod function + if (current != null && OnAddMethod(current)) + { + // record the undo of adding an object + Undo.RecordObject(obj, "Change List"); + list.Add(current); + } + if (GUILayout.Button("Clear List")) + { + Undo.RecordObject(obj, "Clear List"); + list.Clear(); + } + } + else // list is disabled, just display the disabled text as a lebel when the foldout is opened. + { + GUIStyle label = new GUIStyle(GUI.skin.label); + label.wordWrap = true; + EditorGUILayout.LabelField(disabledText, label); + } + } + } + + /// + /// Creates a button that allows undoable changing of a keycode value. + /// Button displays current keycode, then press any key when pressed and listens for a keypress, then updates the keycode. + /// + /// Object to record undo on. + /// Label to display beside button + /// KeyCode to change. Should be unique for each button. + /// Bool representing whether it should be listening to key presses. Should be unique for each button. + public static bool ChangeButtonKeyCodeUndoable(UnityEngine.Object obj, string label, string labelTooltip, ref KeyCode key, ref bool isChanging, float windowWidth, bool drawlabel = false) + { + float labelWidth = 100; + // -15 to add padding to prevent a horizontal scrollbar always being present, this can probably be actually calculated from some unity internal padding values but whatever. + float buttonWidth = windowWidth - labelWidth - 25; + if (buttonWidth < 120) { buttonWidth = 120; } + GUIStyle pressedButtonStyle = new GUIStyle(GUI.skin.box); + pressedButtonStyle.fontStyle = FontStyle.Bold; + pressedButtonStyle.fixedWidth = buttonWidth; + GUIStyle buttonStyle = new GUIStyle(GUI.skin.button); + buttonStyle.fixedWidth = buttonWidth; + //buttonStyle.min + //buttonStyle.stretchWidth = true; + // EditorGUILayout.BeginHorizontal(); + // GUILayout.Label(new GUIContent(label, labelTooltip)); + // EditorGUILayout.LabelField(new GUIContent(label, labelTooltip), GUILayout.ExpandWidth(false)); + string buttonTitle = isChanging ? "Press a key" : key.ToString(); + + if (drawlabel) + { + EditorGUILayout.BeginHorizontal(); + // GUILayout.Label(new GUIContent(label, labelTooltip), GUILayout.ExpandWidth(false)); + LabelWidth(label, labelTooltip, labelWidth); + } + + if (GUILayout.Button(new GUIContent(buttonTitle, "Click then press a key to change.\nModifier keys (like alt, ctrl, space, shift, etc.) can not be used."), isChanging ? pressedButtonStyle : buttonStyle)) + { + isChanging = true; + } + + if (drawlabel) + { + EditorGUILayout.EndHorizontal(); + } + // EditorGUILayout.EndHorizontal(); + if (isChanging) + { + if (CheckKeypressChangeUndoable(obj, ref key)) + { + isChanging = false; + } + } + return isChanging; + } + + /// + /// Changes the KeyCode of keyCode through an undoable action when a key is pressed down. + /// + /// Object to record undo on. + /// KeyCode to change to new key + /// true if key was changed. + private static bool CheckKeypressChangeUndoable(UnityEngine.Object obj, ref KeyCode keyCode) + { + if (Event.current != null // have an event. + && Event.current.type == EventType.KeyDown // a key down event. + && Event.current.keyCode != KeyCode.None && !IsModifierKeyUsed(Event.current.keyCode)) // a key down event that wasn't None. + { + Undo.RecordObject(obj, "Change keycode"); + keyCode = Event.current.keyCode; + return true; + } + return false; + } + + public static void HorizontalLineLight() + { + HorizontalLine(new Color(0, 0, 0, 0.25f)); + } + + /// + /// Draws a vertical line of thickness and color at the current control rect at a single line height. + /// + /// + /// + /// + /// + public static void VerticalLine(Color color, float lineThickness = 1f, float paddingLeft = 0f, float extraHeight = 2f) + { + Rect r = EditorGUILayout.GetControlRect(GUILayout.Width(lineThickness)); + r.height = EditorGUIUtility.singleLineHeight + extraHeight; + r.width = lineThickness; + r.x += paddingLeft; + + EditorGUI.DrawRect(r, color); + } + + /// + /// draws a horizontal line of a given color, thickness, and padding. Centered in the rect. + /// + /// color of the line + /// thickness of the line + /// + public static void HorizontalLine(Color color, float lineThickness = 2f, float padding = 6f) + { + Rect r = EditorGUILayout.GetControlRect(GUILayout.Height(padding)); + // thickness of line to draw is the height + r.height = lineThickness; + // center the line in the rect. + r.y = r.y + (padding - lineThickness) / 2; + // draw the rect. + EditorGUI.DrawRect(r, color); + } + + public static void VerticalSpace(float size) + { + Rect r = EditorGUILayout.GetControlRect(GUILayout.Height(size)); + EditorGUI.DrawRect(r, new Color(0, 0, 0, 0)); + } + + /// + /// checks to see if the keycode is one of the many modifier keys. + /// + /// + /// + private static bool IsModifierKeyUsed(KeyCode value) + { + switch (value) + { + case KeyCode.LeftShift: + case KeyCode.RightShift: + case KeyCode.LeftControl: + case KeyCode.RightControl: + case KeyCode.LeftAlt: + case KeyCode.RightAlt: + case KeyCode.LeftCommand: + case KeyCode.RightCommand: + case KeyCode.Numlock: + case KeyCode.CapsLock: + case KeyCode.LeftWindows: // covers both keycodes for right/left apple as well. + case KeyCode.RightWindows: + return true; + default: + return false; + } + } + + /// + /// In case we want to modify all labels used in the future more easily. + /// Just makes a GUILayout.Label(label) + /// + /// + public static void Label(string label, string tooltip = "") + { + GUILayout.Label(new GUIContent(label, tooltip)); + } + + public static void LabelWidth(string label, string tooltip = "", float width = -1) + { + GUIStyle labelStyle = new GUIStyle(GUI.skin.label); + if (width > 0) { labelStyle.fixedWidth = width; } + GUILayout.Label(new GUIContent(label, tooltip), labelStyle); + } + + public static void LabelEmptyNoStretch(float size = 50f) + { + // float currentSize = EditorGUIUtility.labelWidth; + // EditorGUIUtility.labelWidth = size; + GUIStyle style = new GUIStyle(GUI.skin.label); + style.stretchWidth = false; + GUILayout.Label("", style); + // EditorGUIUtility.labelWidth = currentSize; + } + + public static void LabelBold(string label, string tooltip = "") + { + GUIStyle style = new GUIStyle(GUI.skin.label); + style.fontStyle = FontStyle.Bold; + style.stretchWidth = false; + if (tooltip == "") + { + GUILayout.Label(label, style); + } + else + { + GUILayout.Label(new GUIContent(label, tooltip), style); + } + } + + public static bool FoldoutBold(string label, ref bool foldout, string tooltip = "") + { + GUIStyle style = new GUIStyle(EditorStyles.foldout); + style.fontStyle = FontStyle.Bold; + style.stretchWidth = false; + if (tooltip == "") + { + // GUILayout.Label(label, style); + foldout = EditorGUILayout.Foldout(foldout, label, style); + } + else + { + foldout = EditorGUILayout.Foldout(foldout, new GUIContent(label, tooltip), style); + } + return foldout; + + } + + public static void LabelIcon(string label, string iconName, string tooltip = "") + { + GUIStyle style = new GUIStyle(GUI.skin.label); + style.wordWrap = true; + GUIContent icon = EditorGUIUtility.IconContent(iconName, tooltip); + EditorGUILayout.BeginHorizontal(); + GUILayout.Label(icon, GUILayout.ExpandWidth(false)); + GUILayout.Label(new GUIContent(label, tooltip), style); + GUILayout.Label(icon, GUILayout.ExpandWidth(false)); + EditorGUILayout.EndHorizontal(); + } + + /// + /// Creates an undoable color field + /// + /// Object to record the undo on + /// GUI Content for the auto-layout field + /// String to use for undos + /// Value of the color + public static void ColorFieldUndoable(UnityEngine.Object obj, string undoString, ref Color value) + { + Color _ColorField = value; + EditorGUI.BeginChangeCheck(); + _ColorField = EditorGUILayout.ColorField(_ColorField); + if (EditorGUI.EndChangeCheck()) + { + Undo.RegisterCompleteObjectUndo(obj, undoString); + value = _ColorField; + } + } + + + public static bool DisableableButtonWithShortcut(string text, string enabledTooltip, string disabledTooltip, bool isEnabled, KeyCode shortcut, bool isShortcutEnabled) + { + if (isShortcutEnabled) + { + text = text + " (" + shortcut.ToString() + ")"; + } + GUI.enabled = isEnabled; + bool button = GUILayout.Button(new GUIContent(text, isEnabled ? enabledTooltip : disabledTooltip)); + GUI.enabled = true; + return button; + } + + /// + /// Creates a button that displays different if it is enabled or disabled. Button always returns false if disabled. + /// text of button + /// tool tip for button when enabled + /// tool tip for box when disabled + /// is the button enabled? + /// false if disabled, true if enabled and button is clicked + public static bool DisableableButton(string text, string enabledTooltip, string disabledTooltip, bool isEnabled) + { + GUI.enabled = isEnabled; + bool button = GUILayout.Button(new GUIContent(text, isEnabled ? enabledTooltip : disabledTooltip)); + GUI.enabled = true; + return button; + } + + /// + /// Creates an undoable float field that can also be disabled + /// + /// Object to record the undo on + /// Label of the float field + /// Tooltip to display when enabled + /// Tooltip to display when disabled + /// String to use for undos + /// Value of the float + /// Is the float field enabled? + public static void DisableableFloatFieldUndoable(UnityEngine.Object obj, string label, string tooltip, string disabledTooltip, string undoString, ref float value, bool isEnabled) + { + GUI.enabled = isEnabled; + FloatFieldUndoable(obj, new GUIContent(label, isEnabled ? tooltip : disabledTooltip), undoString, ref value); + GUI.enabled = true; + } + + /// + /// Creates a left toggle if the toggle is enabled that functions normally, + /// otherwise creates a style toggle that is not toggleable and grayed-out. + /// + /// Text to show beside the toggle + /// Tool tip when toggle is enabled + /// Tool tip when toggle is disabled + /// Is the toggle enabled + /// Bool the toggle controls + /// Value of toggle + public static bool DisableableToggleLeft(string text, string enabledTooltip, string disabledTooltip, bool isEnabled, bool toggle) + { + GUI.enabled = isEnabled; + bool toggleValue = GUILayout.Toggle(toggle, new GUIContent(text, isEnabled ? enabledTooltip : disabledTooltip)); + GUI.enabled = true; + return toggleValue; + } + + /// + /// Creates an undoable float field. + /// + /// Object to record the undo on + /// GUI Content for the auto-layout field + /// String to use for undos + /// Value of the float + public static void FloatFieldUndoable(UnityEngine.Object obj, GUIContent content, string undoString, ref float value, int labelSize = -1) + { + float _FloatField = value; + EditorGUI.BeginChangeCheck(); + float w = EditorGUIUtility.labelWidth; + if (labelSize > 0) + { + EditorGUIUtility.labelWidth = labelSize; + } + _FloatField = EditorGUILayout.FloatField(content, _FloatField); + if (EditorGUI.EndChangeCheck()) + { + Undo.RegisterCompleteObjectUndo(obj, undoString); + value = _FloatField; + } + EditorGUIUtility.labelWidth = w; + } + + static GUIStyle ButtonStyle; + + public static bool IconButton(string iconContentName, string tooltip) + { + ButtonStyle = new GUIStyle(GUI.skin.button); + ButtonStyle.padding = new RectOffset(4, 4, 1, 1); + ButtonStyle.margin = new RectOffset(4, 4, 2, 0); + ButtonStyle.stretchWidth = false; + GUIContent icon = EditorGUIUtility.IconContent(iconContentName, tooltip); + // text was not tooltip! + icon.tooltip = tooltip; + if (GUILayout.Button(icon, ButtonStyle)) + { + return true; + } + return false; + } + + /// + /// Creates a float slider that converts basePow^current to an integer in actual result. + /// + /// Current Value of the float slider + /// Min Value of the float slider + /// Max value of the float slider + /// Result of all calculations that is actually used + /// Min value to clamp result to + /// Max value to clamp result to + /// power to use when calculating actual result in Mathf.Pow(basePow, current) + /// Current value of the slider itself + public static float SliderFloatToIntBase2(GUIContent label, float current, float min, float max, ref int actualResult, int aMin, int aMax, float basePow = 2f) + { + EditorGUILayout.BeginHorizontal(); + // get the current control rect. + Rect r = EditorGUILayout.GetControlRect(); + // quarter of width goes to the label. + r.width = r.width / 4; + // label of the slider + EditorGUI.LabelField(r, label); + + // set up the control name for the slider + GUI.SetNextControlName("FloatSlider" + current); + // keep track if the slider changes + EditorGUI.BeginChangeCheck(); + //sliderbar uses half of space. + r.x += r.width; + r.width *= 2; + // draw the slider. + current = GUI.HorizontalSlider(r, current, min, max); + // if it changes, and is not focused, change the focus to the slider + // this helps keep the area where you can directly enter the number with the keyboard updated as the slider moves. + if (EditorGUI.EndChangeCheck() && GUI.GetNameOfFocusedControl() != "FloatSlider" + current) + { + GUI.FocusControl("FloatSlider" + current); + } + // Set the actual result using the current slider. + actualResult = (int)Mathf.Clamp(Mathf.Pow(basePow, current), aMin, aMax); + // check if the user has updated the int field input + EditorGUI.BeginChangeCheck(); + // adjust rect to use last quarter of space. + r.x += r.width + 8; + r.width = r.width / 2 - 8; + actualResult = EditorGUI.IntField(r, actualResult); + // actualResult = EditorGUILayout.IntField(actualResult, GUILayout.ExpandWidth(false)); + if (EditorGUI.EndChangeCheck()) + { + actualResult = Mathf.Clamp(actualResult, aMin, aMax); + // update the current value if the text has changed. + current = Mathf.Log(actualResult, 2); + } + EditorGUILayout.EndHorizontal(); + // were done, return the current value. + return current; + } + + + /// + /// Creates an undoable toggle field. + /// + /// Object to record the undo on + /// GUI Content for the auto-layout field + /// String to use for undos + /// Value of the toggle + public static void ToggleLeftUndoable(UnityEngine.Object obj, GUIContent content, string undoString, ref bool value, float labelWidth = 10f) + { + bool _ToggleField = value; + EditorGUI.BeginChangeCheck(); + // _ToggleField = EditorGUILayout.ToggleLeft(content, _ToggleField); + float lw = EditorGUIUtility.labelWidth; + EditorGUIUtility.labelWidth = labelWidth; + // _ToggleField = GUILayout.Toggle(_ToggleField, content, GUILayout.ExpandWidth(false)); + _ToggleField = EditorGUILayout.ToggleLeft(content, _ToggleField); + EditorGUIUtility.labelWidth = lw; + if (EditorGUI.EndChangeCheck()) + { + // again record only works in some cases, and complete works significantly better. + // ie can't record changing DrawGizmos without the complete object undo. + Undo.RegisterCompleteObjectUndo(obj, undoString); + value = _ToggleField; + } + } + + /// + /// creates a toggle left with no change check + /// + /// + /// + /// + public static bool ToggleLeft(GUIContent content, bool value, float labelWidth = 10f) + { + bool _ToggleField = value; + float lw = EditorGUIUtility.labelWidth; + EditorGUIUtility.labelWidth = labelWidth; + _ToggleField = EditorGUILayout.ToggleLeft(content, _ToggleField); + EditorGUIUtility.labelWidth = lw; + return _ToggleField; + } + + public static Enum EnumPopup(GUIContent content, Enum selected, float labelWidth = 50f) + { + float lw = EditorGUIUtility.labelWidth; + EditorGUIUtility.labelWidth = labelWidth; + GUIStyle miniPopupStyle = GUI.skin.GetStyle("MiniPopup"); + if (miniPopupStyle != null) + { + GUIStyle enumStyle = new GUIStyle(miniPopupStyle); + Enum val = EditorGUILayout.EnumPopup(content, selected, enumStyle); + EditorGUIUtility.labelWidth = lw; + return val; + } + else + { + Enum val = EditorGUILayout.EnumPopup(content, selected); + EditorGUIUtility.labelWidth = lw; + return val; + } + + + } + } +} +#endif \ No newline at end of file diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderUIHelpers.cs.meta b/Assets/EasyColliderEditor/Scripts/EasyColliderUIHelpers.cs.meta new file mode 100644 index 00000000..28d35c03 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderUIHelpers.cs.meta @@ -0,0 +1,20 @@ +fileFormatVersion: 2 +guid: 6d01889b53b9585409077f8e53708c46 +timeCreated: 1592405766 +licenseType: Store +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/EasyColliderUIHelpers.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderVHACD.cs b/Assets/EasyColliderEditor/Scripts/EasyColliderVHACD.cs new file mode 100644 index 00000000..819cfbf4 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderVHACD.cs @@ -0,0 +1,478 @@ +#if (UNITY_EDITOR) +using System.Collections.Generic; +using UnityEngine; +using System.Runtime.InteropServices; +using System; +using System.Linq; +namespace ECE +{ + + //Future potential features: + // The option to covert the convex hulls generated into other colliders, like box colliders (boxelization is a fun term). + // So that we can leverage VHACD results into alternative basic colliders. + + // New version of vhacd is out but in my testing it is much slower than the older performance branch version + // additionally, it offers less adjustable parameters, which makes it harder to get a good fit, + // even though the fit isn't perfect with any vhacd implementation since it's approximate convex decomposition + // and even though the current parameters aren't particularily understandable, you can adjust them and see the output quickly until something is close enough. + + public class EasyColliderVHACD + { + // If you're getting an error about DLL not found exception, you'll likely be pointed to this line by the error + // near the top of this file you'll find " const string dllName = "ECE_VHACD"; " , + // this is where the dll name is placed (these same comments are there as well) + // To fix this on Mac: sometimes it can not recognize the .bundle, try either "ECE_VHACD.bundle" or "ECE_VHACD" as the dllName string + // Occasionally this can also happen when the dll/bundles are updated, and imported into unity after they were already used + // to correctly update when vhacd is updated, be sure to close unity, and immediately update the asset on opening the project + // currently only supports windows, and OSX (arm / silicon and intel) versions of unity. + const string dllName = "ECE_VHACD"; + + [DllImport(dllName, EntryPoint = "GetMaxNumVerticesPerCH")] + private static extern uint GetMaxNumVerticesPerCH(); + + // extern "C" VHACD_API double* GetConvexHullCenter(); + [DllImport(dllName, EntryPoint = "GetConvexHullCenter")] + private static extern IntPtr GetConvexHullCenter(); + + // extern "C" VHACD_API unsigned int* GetConvexHullTriangles(); + [DllImport(dllName, EntryPoint = "GetConvexHullTriangles")] + private static extern IntPtr GetConvexHullTriangles(); + + // extern "C" VHACD_API double* GetConvexHullPoints(); + [DllImport(dllName, EntryPoint = "GetConvexHullPoints")] + private static extern IntPtr GetConvexHullPoints(); + + // extern "C" VHACD_API void Compute(); + [DllImport(dllName, EntryPoint = "Compute")] + private static extern void Compute(); + + // extern "C" VHACD_API bool Create(); + [DllImport(dllName, EntryPoint = "Create")] + private static extern bool Create(); + + // extern "C" VHACD_API bool Create_ASYNC(); + [DllImport(dllName, EntryPoint = "Create_ASYNC")] + private static extern bool Create_ASYNC(); + + // extern "C" VHACD_API void Destroy(); + [DllImport(dllName, EntryPoint = "Destroy")] + private static extern void Destroy(); + + // extern "C" VHACD_API int GetConvexHullNumPoints(); + [DllImport(dllName, EntryPoint = "GetConvexHullNumPoints")] + private static extern int GetConvexHullNumPoints(); + + // extern "C" VHACD_API int GetConvexHullNumTriangles(); + [DllImport(dllName, EntryPoint = "GetConvexHullNumTriangles")] + private static extern int GetConvexHullNumTriangles(); + + // extern "C" VHACD_API double GetConvexHullPoint(int index); + [DllImport(dllName, EntryPoint = "GetConvexHullPoint")] + private static extern double GetConvexHullPoint(int index); + + // extern "C" VHACD_API int GetConvexHullTriangle(int index); + [DllImport(dllName, EntryPoint = "GetConvexHullTriangle")] + private static extern int GetConvexHullTriangle(int index); + + // extern "C" VHACD_API double GetConvexHullVolume(); + [DllImport(dllName, EntryPoint = "GetConvexHullVolume")] + private static extern double GetConvexHullVolume(); + + // extern "C" VHACD_API int GetNumberOfConvexHulls(); + [DllImport(dllName, EntryPoint = "GetNumberOfConvexHulls")] + private static extern int GetNumberOfConvexHulls(); + + // extern "C" VHACD_API int GetPointSize(); + [DllImport(dllName, EntryPoint = "GetPointSize")] + private static extern int GetPointSize(); + + // extern "C" VHACD_API int GetPointSize(); + [DllImport(dllName, EntryPoint = "IsReady")] + private static extern bool IsReady(); + + // extern "C" VHACD_API void SetConvexHull(int index); + [DllImport(dllName, EntryPoint = "SetConvexHull")] + private static extern void SetConvexHull(int index); + + // extern "C" VHACD_API void SetMaxHulls(int value); + [DllImport(dllName, EntryPoint = "SetMaxHulls")] + private static extern void SetMaxHulls(int value); + + // extern "C" VHACD_API void SetMaxVerticesPerHull(int value); + [DllImport(dllName, EntryPoint = "SetMaxVerticesPerHull")] + private static extern void SetMaxVerticesPerHull(int value); + + // For branch PERFORMANCE ENHANCEMENTS + // extern "C" VHACD_API void SetParameters( + // double concavity, + // double alpha, + // double beta, + // double minVolumePerConvexHull, + // int resolution, + // int maxNumVerticesPerCH, + // int planeDownsampling, + // int convexhullDownsampling, + // int maxConvexHulls, + // bool projectHullVertices, + // unsigned int fillMode); + [DllImport(dllName, EntryPoint = "SetParameters")] + private static extern void SetParameters( + double concavity, + double alpha, + double beta, + double minVolumePerConvexHull, + int resolution, + int maxNumVerticesPerCH, + int planeDownsampling, + int convexhullDownsampling, + int maxConvexHulls, + bool projectHullVertices, + uint fillMode + ); + + // extern "C" VHACD_API void SetPoint(int index, float value); + [DllImport(dllName, EntryPoint = "SetPoint")] + private static extern void SetPoint(int index, float value); + + // extern "C" VHACD_API void SetPoints(float pointsArr[], int size); + [DllImport(dllName, EntryPoint = "SetPoints")] + private static extern void SetPoints(float[] pointsArr, int size); + + // extern "C" VHACD_API void SetPointSize(int size); + [DllImport(dllName, EntryPoint = "SetPointSize")] + private static extern void SetPointSize(int size); + + // extern "C" VHACD_API void SetResolution(int value); + [DllImport(dllName, EntryPoint = "SetResolution")] + private static extern void SetResolution(int value); + + // extern "C" VHACD_API void SetTriangle(int index, int value); + [DllImport(dllName, EntryPoint = "SetTriangle")] + private static extern void SetTriangle(int index, int value); + + // extern "C" VHACD_API void SetTriangles(int trianglesArr[], int size); + [DllImport(dllName, EntryPoint = "SetTriangles")] + private static extern void SetTriangles(int[] trianglesArr, int size); + + // extern "C" VHACD_API void SetTriangleSize(int size); + [DllImport(dllName, EntryPoint = "SetTriangleSize")] + private static extern void SetTriangleSize(int size); + + + /// + /// Is an instance of vhacd initialized? + /// + /// this is a fix for the new version of vhacd which is not used yet for various reasons. + private bool isInitialized = false; + + /// + /// Initializes a VHACD instance + /// + /// Use async process? + /// + public bool Init(bool async = true) + { + Clean(); + if (async) + { + // fixes an issue in new vhacd without changing the api. + isInitialized = true; + // If you're getting an error about DLL not found exception, you'll likely be pointed to this line by the error + // near the top of this file you'll find " const string dllName = "ECE_VHACD"; " , + // this is where the dll name is placed (these same comments are there as well) + // To fix this on Mac: sometimes it can not recognize the .bundle, try either "ECE_VHACD.bundle" or "ECE_VHACD" as the dllName string + // Occasionally this can also happen when the dll/bundles are updated, and imported into unity after they were already used + // to correctly update when vhacd is updated, be sure to close unity, and immediately update the asset on opening the project + // currently only supports windows, and OSX (arm / silicon and intel) versions of unity. + return Create_ASYNC(); + } + else + { + isInitialized = true; + return Create(); + } + } + + /// + /// Destroys the VHACD instance, if it is already initialized. + /// + /// true if destroyed, false if not destroyed(because it wasn't initialized yet) + public bool Clean() + { + // fixes issue where vhacd isn't initialized and tries to destroy itself (in new vhacd) + if (isInitialized) + { + // Debug.Log("Clean" + dllName); + isInitialized = false; + Destroy(); + return true; + } + else + { + return false; + } + } + + /// + /// Gets all calculated convex hulls and turn them into meshes. + /// + /// convex hull meshes + public Mesh[] CreateConvexHullMeshes() + { + int numHulls = GetNumberOfConvexHulls(); + Mesh[] meshes = new Mesh[numHulls]; + for (int i = 0; i < numHulls; i++) + { + // get the current convex hull + SetConvexHull(i); + // get the ch data + int pointCount = GetConvexHullNumPoints(); + int triangleCount = GetConvexHullNumTriangles(); + // create new vertex and triangles array. + Vector3[] vertices = new Vector3[pointCount]; + int[] triangles = new int[triangleCount * 3]; + for (int j = 0; j < triangleCount * 3; j++) + { + triangles[j] = GetConvexHullTriangle(j); + } + // assing each point to a vertex in the array + Vector3 point = Vector3.zero; + for (int j = 0; j < pointCount * 3; j += 3) + { + // note that in VHACD, each vertex is not a vector. + // instead each vertex is simply the 3 values in order. + point.x = (float)GetConvexHullPoint(j); + point.y = (float)GetConvexHullPoint(j + 1); + point.z = (float)GetConvexHullPoint(j + 2); + vertices[j / 3] = point; + } + // create and save the mesh. + Mesh mesh = new Mesh(); + mesh.vertices = vertices; + mesh.triangles = triangles; + // be sure to add it to our meshes array + meshes[i] = mesh; + } + //return the array of meshes so they can be added as convex mesh colliders. + return meshes; + } + + /// + /// Gets the number of convex hulls calcalated + /// + /// # convex hulls computed + public int GetConvexHullCount() + { + return GetNumberOfConvexHulls(); + } + + /// + /// Checks if the async computation of convex hulls is complete + /// + /// true if finished + public bool IsComputeFinished() + { + return IsReady(); + } + + /// + /// Checks to see if each convex hull is under 256 vertices and 256 triangles. + /// + /// True if under limits + public bool IsValid() + { + // calculate max triangle count and max vertex count + int maxTriangleCount = 0; + int maxVertexCount = 0; + int numHulls = GetConvexHullCount(); + for (int i = 0; i < numHulls; i++) + { + SetConvexHull(i); + int currentTriCount = GetConvexHullNumTriangles(); + int currentVertCount = GetConvexHullNumPoints(); + if (currentTriCount > maxTriangleCount) + { + maxTriangleCount = currentTriCount; + } + if (currentVertCount > maxVertexCount) + { + maxVertexCount = currentVertCount; + } + } + // if both are under 256, unity will generate no errors. + // (these errors are hidden in some older versions of unity.) + if (maxVertexCount < 256 && maxTriangleCount < 256) + { + return true; + } + return false; + } + + /// + /// Sets the vertices and triangles array for VHACD from mesh + /// + /// mesh filter of the source mesh + /// transform the mesh collider will be attached to + /// true if preparation succeeds, false otherwise + public bool PrepareMeshData(MeshFilter meshFilter, Transform attachTo, Mesh mesh) + { + if (mesh == null) return false; + // convert from meshes' object to world space, to attach to's local space, then calculate, then add. + Vector3[] vertices = mesh.vertices; + int[] triangles = mesh.triangles; + List localAttachVertices = new List(); + if (meshFilter != null && meshFilter.transform != attachTo && meshFilter.sharedMesh != null && meshFilter.sharedMesh == mesh) + { + foreach (Vector3 v in vertices) + { + // transform from mesh filters local space, to world space, to the attach to's local space. + localAttachVertices.Add(attachTo.transform.InverseTransformPoint(meshFilter.transform.TransformPoint(v))); + } + } + else + { + localAttachVertices = vertices.ToList(); + } + // could have used an intptr but this way is fast enough + // and we can let VHACD take care of itself. + SetPointSize(localAttachVertices.Count * 3); + SetTriangleSize(triangles.Length); + for (int i = 0; i < localAttachVertices.Count; i++) + { + SetPoint(i * 3, localAttachVertices[i].x); + SetPoint(i * 3 + 1, localAttachVertices[i].y); + SetPoint(i * 3 + 2, localAttachVertices[i].z); + } + for (int i = 0; i < triangles.Length; i++) + { + SetTriangle(i, triangles[i]); + } + return true; + } + + /// + /// prepares an array of meshfilters for a single convex hull decomposition. + /// + /// array of meshfilters + public bool PrepareMeshData(List meshFilters, Transform attachTo) + { + // convert vertices from the mesh filters local space, to world space, to the attach to's local space. + int vertexCount = 0; + int triangleCount = 0; + List localAttachVertices = new List(); + List triangles = new List(); + foreach (MeshFilter mf in meshFilters) + { + // skip any mesh filters that were deleted, or had their shared mesh changed to null + if (mf == null || mf.sharedMesh == null) continue; + // convert to local vertices of the attach to object. + Vector3[] verts = mf.sharedMesh.vertices; + foreach (Vector3 v in verts) + { + localAttachVertices.Add(attachTo.transform.InverseTransformPoint(mf.transform.TransformPoint(v))); + } + // get current shared mesh triangles + int[] tris = mf.sharedMesh.triangles; + for (int i = 0; i < tris.Length; i++) + { + // add the vertex index, plus the offset of the total running vertex count. + triangles.Add(tris[i] + vertexCount); + } + vertexCount += mf.sharedMesh.vertices.Length; + triangleCount += mf.sharedMesh.triangles.Length; + } + // could have used intptr etc. to pass all the data at once + // but this way is still fast enough, and we can just let VHACD handle it's own stuff. + SetPointSize(vertexCount * 3); + SetTriangleSize(triangleCount); + for (int i = 0; i < localAttachVertices.Count; i++) + { + SetPoint(i * 3, localAttachVertices[i].x); + SetPoint(i * 3 + 1, localAttachVertices[i].y); + SetPoint(i * 3 + 2, localAttachVertices[i].z); + } + for (int i = 0; i < triangles.Count; i++) + { + SetTriangle(i, triangles[i]); + } + return true; + } + + /// + /// Recalculates the current convex hulls based on max triangles and vertices with an aim to get max triangles below 256 + /// + /// + public bool RecomputeVHACD() + { + int maxTriangleCount = 0; + int maxVertexCount = 0; + int numHulls = GetConvexHullCount(); + // calculate max triangles and vertices from convex hulls. + for (int i = 0; i < numHulls; i++) + { + SetConvexHull(i); + int currentTriCount = GetConvexHullNumTriangles(); + int currentVertCount = GetConvexHullNumPoints(); + if (currentTriCount > maxTriangleCount) + { + maxTriangleCount = currentTriCount; + } + if (currentVertCount > maxVertexCount) + { + maxVertexCount = currentVertCount; + } + } + // reduce the max number of vertices. + float trisPerVertMax = (float)maxTriangleCount / maxVertexCount; + int maxVerticesPerConvexHull = (int)(255 / trisPerVertMax); + // set the new max number of vertices + SetMaxVerticesPerHull(maxVerticesPerConvexHull); + // compute again. + Compute(); + return true; + } + + /// + /// Calls compute method on the current VHACD instance + /// + /// + public bool RunVHACD() + { + Compute(); + return true; + } + + /// + /// Sets parameters on the current VHACD instance + /// + /// parameters to set + public bool SetParameters(VHACDParameters parameters) + { + SetParameters( + parameters.concavity, + parameters.alpha, + parameters.beta, + parameters.minVolumePerCH, + parameters.resolution, + parameters.maxNumVerticesPerConvexHull, + parameters.planeDownsampling, + parameters.convexhullDownSampling, + parameters.maxConvexHulls, + parameters.projectHullVertices, + (uint)parameters.fillMode + ); + return true; + } + } + + /// + /// Fill mode for VHACD + /// + public enum VHACD_FILL_MODE + { + FLOOD_FILL, + SURFACE_ONLY, + RAYCAST_FILL, + } +} +#endif \ No newline at end of file diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderVHACD.cs.meta b/Assets/EasyColliderEditor/Scripts/EasyColliderVHACD.cs.meta new file mode 100644 index 00000000..4324eea3 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderVHACD.cs.meta @@ -0,0 +1,20 @@ +fileFormatVersion: 2 +guid: 492957b54e69e0944a816663cdf414ff +timeCreated: 1591919081 +licenseType: Store +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/EasyColliderVHACD.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderVertex.cs b/Assets/EasyColliderEditor/Scripts/EasyColliderVertex.cs new file mode 100644 index 00000000..39a2c27a --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderVertex.cs @@ -0,0 +1,60 @@ +#if (UNITY_EDITOR) +using System; +using UnityEngine; +namespace ECE +{ + /// + /// A vertex represented by the transform it's attached to and it's local position. + /// + [System.Serializable] + public class EasyColliderVertex : IEquatable + { + /// + /// Local position of the vertex on the transform. + /// + public Vector3 LocalPosition; + + public Vector3 Normal = Vector3.zero; + + /// + /// Transform the vertex comes from. + /// + public Transform T; + + + /// + /// Create a new Easy Collider Vertex + /// + /// Transform the vertex is on + /// Local position of the vertex + public EasyColliderVertex(Transform transform, Vector3 localPosition) + { + this.T = transform; + this.LocalPosition = localPosition; + } + + public EasyColliderVertex(EasyColliderVertex ecv) + { + this.T = ecv.T; + this.LocalPosition = ecv.LocalPosition; + } + + // since we've used hashsets prior to adding normals, and we calculate smoothed normals + // when things get selected and store the those as the selected vertices, + // various things would have to change to include the normals in the equals and get hash code. + // so for now, we're still just position based. + + public bool Equals(EasyColliderVertex other) + { + return (other.LocalPosition == this.LocalPosition && other.T == this.T);// && other.Normal == this.Normal); + } + + public override int GetHashCode() + { + int hashCode = 13 * 31 + LocalPosition.GetHashCode(); + // hashCode += 17 * this.Normal.GetHashCode(); + return hashCode * 31 + T.GetHashCode(); + } + } +} +#endif \ No newline at end of file diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderVertex.cs.meta b/Assets/EasyColliderEditor/Scripts/EasyColliderVertex.cs.meta new file mode 100644 index 00000000..3e8f19a4 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderVertex.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 41abfd88235d8c444be04e8d0ddd38b7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/EasyColliderVertex.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderWindow.cs b/Assets/EasyColliderEditor/Scripts/EasyColliderWindow.cs new file mode 100644 index 00000000..ba92fb06 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderWindow.cs @@ -0,0 +1,4630 @@ +#if (UNITY_EDITOR) +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using UnityEditorInternal; +using System; +using System.Linq; +using System.Text.RegularExpressions; +using ECUI = ECE.EasyColliderUIHelpers; +using UnityEngine.SceneManagement; + +#if (UNITY_2021_2_OR_NEWER) +// prefab stage out of experimental. +using UnityEditor.SceneManagement; +#elif (UNITY_2018_3_OR_NEWER) +// prefabstage is still experimental +// but what it inherits from, PreviewSceneStage/Stage is not experimental as of 2020.1 +using UnityEditor.Experimental.SceneManagement; +#endif +// If you have purchased this asset and have any other ideas for features, please contact me at pmurph.software@gmail.com +// I would love to hear what users of this asset would like added or improved! +// If the idea is already on this list, also let me know which you would like to see so I can prioritize correctly. + +// Currently working on: +// Selection update +// Bring vertex-normal extrusion to non-mesh colliders +// Optional per bone settings for auto skinned mesh colliders. +// Various methods to get auto generated colliders on skinned meshes to not overlap with eachother. + +// Current potential future ideas: +// Triangle Selection. +// Saving callback?: when user saves -> deselect object -> save -> reselect. So mid-collider-creation saving doesn't also save attached components needed for functionality. +// Option to draw compute-shader boxes twice, once with customizable-color wireframe, and once as normal. +// best-fit-line w/3d-linear/ortho regression for creating rotated capsule colliders. (so people can just select a bunch of points and not worry about it.) +// Maybe at some point in the distant future, change to using UIToolkit + +// UI: +// Improve UI with dark mode. + +// API / Refactoring +// Code cleanup / refactoring for easier maitenance / addition of features. +// switch preview to a list so vhacd and auto skinned can use the same preview methods that merge and creation do. + +namespace ECE +{ + [System.Serializable] + public class EasyColliderWindow : EditorWindow + { + + #region Variables + + /// + /// helps detect when an alt tab is done to correctly reset snaps (as alt is used for snap -) + /// + private bool _AltTabFocusChange = false; + + + bool _ShowShortcutsFoldout = false; + /// + /// Array to check if we're recording a key change to change shortcuts. + /// + bool[] keysChanging = new bool[10]; + + /// + /// Mouse position during the drag events + /// + private Vector2 _CurrentDragPosition; + + /// + /// Current Collider that is hovered. + /// + private Collider _CurrentHoveredCollider; + + /// + /// Local position of current hovered point (not a vertex) + /// + private Vector3 _CurrentHoveredPoint; + + /// + /// Transform of current hovered point (not a vertex) + /// + private Transform _CurrentHoveredPointTransform; + + /// + /// Local position of current hovered vertex + /// + private Vector3 _CurrentHoveredPosition; + /// + /// Transform of current hovered vertex + /// + private Transform _CurrentHoveredTransform; + + /// + /// Is the EasyColliderVertex being selected an actual vertex of the mesh, or just a point on the surface. + /// + private bool isVertexSelection; + + private HashSet _CurrentHoveredVertices; + /// + /// Set of hovered vertices in whip/box select, quicker to just use a hashset of vector3's + /// + private HashSet CurrentHoveredVertices + { + get + { + if (_CurrentHoveredVertices == null) + { + _CurrentHoveredVertices = new HashSet(); + } + return _CurrentHoveredVertices; + } + set { _CurrentHoveredVertices = value; } + } + + private HashSet _CurrentSelectBoxVerts; + /// + /// Set of ECE vertices in whip/box select. These are sent to ECEditor to actually select vertices once the box select drag is done. + /// + private HashSet CurrentSelectBoxVerts + { + get + { + if (_CurrentSelectBoxVerts == null) + { + _CurrentSelectBoxVerts = new HashSet(); + } + return _CurrentSelectBoxVerts; + } + set { _CurrentSelectBoxVerts = value; } + } + + /// + /// What tab is currently selected + /// + [SerializeField] + private ECE_WINDOW_TAB CurrentTab = ECE_WINDOW_TAB.None; + + private List _CurrentTips; + /// + /// List of current tips being displayed + /// + private List CurrentTips + { + get + { + if (_CurrentTips == null) + { + _CurrentTips = new List(); + } + return _CurrentTips; + } + set + { + _CurrentTips = value; + } + } + + [SerializeField] + private EasyColliderAutoSkinned _ECAutoSkinned; + /// + /// EasyColliderEditor scriptable object. + /// + private EasyColliderAutoSkinned ECAutoSkinned + { + get + { + if (_ECAutoSkinned == null) + { + _ECAutoSkinned = ScriptableObject.CreateInstance(); + } + return _ECAutoSkinned; + } + set { _ECAutoSkinned = value; } + } + + + private EasyColliderEditor _ECEditor; + /// + /// EasyColliderEditor scriptable object. + /// + private EasyColliderEditor ECEditor + { + get + { + if (_ECEditor == null) + { + _ECEditor = ScriptableObject.CreateInstance(); + } + return _ECEditor; + } + set { _ECEditor = value; } + } + + + private EasyColliderDOTS _ECEDots; + /// + /// Class that handles Dots Conversion and UI + /// + /// + public EasyColliderDOTS ECEDots + { + get + { + if (_ECEDots == null) + { + _ECEDots = new EasyColliderDOTS(); + } + return _ECEDots; + } + } + + + private EasyColliderPreferences ECEPreferences + { + get { return EasyColliderPreferences.Preferences; } + } + + private EasyColliderPreviewer _ECPreviewer; + /// + /// Previewer class used to draw handles to preview colliders created from selected vertices + /// + /// + private EasyColliderPreviewer ECPreviewer + { + get + { + if (_ECPreviewer == null) + { + _ECPreviewer = ScriptableObject.CreateInstance(); + } + return _ECPreviewer; + } + set { _ECPreviewer = value; } + } + + /// + /// bool for toggle for dropdown to edit preferences. + /// + private bool _EditPreferences; + + /// + /// bool for toggle for dropdown on editing additional vertex scales. + /// + bool _editExtraVertexScales; + + /// + /// Keeps track of when the last raycast was done when enabled, so that we aren't constantly raycasting / drag selecting + /// + private double _LastSelectionTime = 0.0f; + + private List> _LocalSpaceVertices; + /// + /// Local space vertices as a list for each valid mesh + /// + private List> LocalSpaceVertices + { + get + { + if (_LocalSpaceVertices == null) + { + _LocalSpaceVertices = new List>(); + } + return _LocalSpaceVertices; + } + set { _LocalSpaceVertices = value; } + } + + private List> _ScreenSpaceVertices; + /// + /// Screen space vertices as a list for each valid mesh + /// + private List> ScreenSpaceVertices + { + get + { + if (_ScreenSpaceVertices == null) + { + _ScreenSpaceVertices = new List>(); + } + return _ScreenSpaceVertices; + } + set { _ScreenSpaceVertices = value; } + } + + /// + /// Scroll position for editor window + /// + private Vector2 _ScrollPosition; + + /// + /// Color of selection rectangle. + /// + private Color _SelectionRectColor = new Color(0, 255, 0, 0.2f); + + /// + /// Show the settings for each created collider? + /// + private bool _ShowColliderSettings = false; + +#if (UNITY_2022_2_OR_NEWER) + /// + /// show the settings for collidewr overrides added in 2022_2 or newer. + /// + private bool _ShowColliderOverrideSettings = false; +#endif + + /// + /// start mouse position of the drag + /// + private Vector2 _StartDragPosition = Vector2.zero; + + /// + /// Display strings of tabs in row 1 (Creation, Removal) + /// + string[] TabsRow1 = { "Creation", "Remove/Merge" }; + + /// + /// Display string of tabs in row 2 (VHACD, Auto Skinned) + /// + string[] TabsRow2 = { "VHACD", "Auto Skinned" }; + + + + + /// + /// Parameters of currently calculating VHACD instance. + /// + private VHACDParameters VHACDCurrentParameters + { + get + { + if (_VHACDCurrentParameters == null) + { + _VHACDCurrentParameters = ECEPreferences.VHACDParameters.Clone(); + } + return _VHACDCurrentParameters; + } + set { _VHACDCurrentParameters = value; } + } + private VHACDParameters _VHACDCurrentParameters; + /// + /// Current step of generation VHACD is on. + /// + private int _VHACDCurrentStep = 0; + + /// + /// Number of times vhacd progress was checked (for adding dots) + /// + private float _VHACDCheckCount = 0; + + /// + /// string of dots to add to progress bar so it still shows as calculating and not frozen if updated + /// + private string _VHACDDots = ""; + + /// + /// Is VHACD currently computing? + /// + private bool _VHACDIsComputing = false; + + /// + /// Progress bar display string + /// + private string _VHACDProgressString = ""; + + /// + /// Show advanced VHACD settings? + /// + private bool _ShowVHACDAdvancedSettings = false; + + + //fixes an issue where meshs are generated by the preview but not correctly cleaned up. + private Dictionary _VHACDPreviewResult; + private Dictionary VHACDPreviewResult + { + get { return _VHACDPreviewResult; } + set + { + if (_VHACDPreviewResult != null) + { + foreach (var kvp in _VHACDPreviewResult) + { + foreach (var m in kvp.Value) + { + DestroyImmediate(m); + } + } + } + _VHACDPreviewResult = value; + } + } + + private bool _VHACDUpdatePreview; + + + + private List> _WorldSpaceVertices; + /// + /// World space vertices as a list for each valid mesh + /// + private List> WorldSpaceVertices + { + get + { + if (_WorldSpaceVertices == null) + { + _WorldSpaceVertices = new List>(); + } + return _WorldSpaceVertices; + } + set { _WorldSpaceVertices = value; } + } + #endregion + + // ------------------------------------------------------------------------------------------------------------------------------- + // ------------------------------------------------------------------------------------------------------------------------------- + + #region EditorWindowMethods + // Default methods or functions for delegates / events + [MenuItem("Window/Easy Collider Editor")] + static void Init() + { + EditorWindow ece = EditorWindow.GetWindow(typeof(EasyColliderWindow), false, "Easy Collider Editor"); + ece.Show(); + ece.autoRepaintOnSceneChange = true; + _eceWindow = ece as EasyColliderWindow; + if (Selection.activeGameObject != null) + { + _eceWindow.ChangeToNewObject(Selection.activeGameObject); + if (_eceWindow.CurrentTab == ECE_WINDOW_TAB.None) + { + _eceWindow.CurrentTab = ECE_WINDOW_TAB.Creation; + _eceWindow.ECEditor.VertexSelectEnabled = true; + } + } + } + + // Additional methods for using with external tools. + + /// + /// the current open ece window if one exists + /// + public static EasyColliderWindow _eceWindow; + + /// + /// opens and returns an EasyColliderEditor window. + /// + /// current easy collider editor window + public static EasyColliderWindow OpenWindow() + { + Init(); + return _eceWindow; + } + + /// + /// Changes to the selected object and automatically changes to creation tab if a window was just opened. + /// + /// gameobject to be selected for collider editing + public void SetSelectedGameObject(GameObject gameObject) + { + ChangeToNewObject(gameObject); + if (CurrentTab == ECE_WINDOW_TAB.None) + { + CurrentTab = ECE_WINDOW_TAB.Creation; + ECEditor.VertexSelectEnabled = true; + } + } + + void OnDestroy() + { + ECEditor.SelectedGameObject = null; + // Unregister all the delegates + //EASY_COLLIDER_EDITOR_DELEGATES - Change the below delegates if something breaks! (and in OnEnable below) +#if UNITY_2019_1_OR_NEWER + SceneView.duringSceneGui -= OnSceneGUI; +#else + SceneView.onSceneGUIDelegate -= OnSceneGUI; +#endif + // Unregister the repaint of window when undo's are performed. + Undo.undoRedoPerformed -= OnUndoRedoPerformed; + EditorApplication.update -= OnUpdate; +#if (UNITY_2018_3_OR_NEWER) + PrefabStage.prefabStageClosing -= PrefabStageClosing; + PrefabStage.prefabStageOpened -= PrefabStageOpened; +#endif + } + + void OnEnable() + { + ECPreviewer.DrawColor = ECEPreferences.PreviewDrawColor; + // Register to scene updates so we can raycast to the mesh + //EASY_COLLIDER_EDITOR_DELEGATES - Change the below delegates if something breaks! (and in OnDisable above) +#if UNITY_2019_1_OR_NEWER + SceneView.duringSceneGui += OnSceneGUI; +#else + SceneView.onSceneGUIDelegate += OnSceneGUI; +#endif + // Register to undo/redo to repaint immediately. + Undo.undoRedoPerformed += OnUndoRedoPerformed; + EditorApplication.update += OnUpdate; +#if (UNITY_2018_3_OR_NEWER) + PrefabStage.prefabStageClosing += PrefabStageClosing; + PrefabStage.prefabStageOpened += PrefabStageOpened; +#endif + } + + +#if (UNITY_2018_3_OR_NEWER) + + // PREFAB STAGE IS STILL EXPERIMENTAL, + // SO THIS WILL CAUSE ISSUES IF closing/opened is not there. + // known issues: undo/redo can cause dangling of colliders, even though everything is done with Undo.AddComponent + // but in newer versions of unity the stuff is saved after unity calls closing, but in older it isn't, so theres slightly different undo clearing.. + + // need to keep track of current prefab stage, + // in some versions of unity, openeing subsequent stages is done before closing the previous + PrefabStage _currentPrefabStage; + private void PrefabStageClosing(PrefabStage stage) + { + if (stage == _currentPrefabStage) + { + PrefabStageClean(stage); + } + Undo.ClearAll(); + } + + + private void PrefabStageOpened(PrefabStage stage) + { +#if (UNITY_2020_0_OR_NEWER) + if (_currentPrefabStage != null && stage != _currentPrefabStage) + { + PrefabStageClean(_currentPrefabStage); + } +#else + if (ECEditor.SelectedGameObject != null && stage != _currentPrefabStage) + { + // entering a prefab stage with a selected gameobject should null out the object regardless so people don't think they can't select the prefab object. + ChangeToNewObject(null); + this.Repaint(); + } +#endif + _currentPrefabStage = stage; + + // is the current object not a part of the stage? switch it to the root object. + if (ECEPreferences.AutoSelectOnPrefabOpen && (!stage.IsPartOfPrefabContents(ECEditor.SelectedGameObject) || + // fixes issue where opening a prefab stage while in a different prefab stage wont select the new prefab stage object + // I don't know why this works in earlier versions but not newer versions. Does prefab stage close in older versions before opening? + // in newer versions it defenitely opens before closing. + (stage.IsPartOfPrefabContents(ECEditor.SelectedGameObject) && stage.prefabContentsRoot != ECEditor.SelectedGameObject))) + { + ChangeToNewObject(stage.prefabContentsRoot); + this.Repaint(); + // change tabs if we're not on one + if (CurrentTab == ECE_WINDOW_TAB.None) + { + CurrentTab = ECE_WINDOW_TAB.Creation; + } + if (CurrentTab == ECE_WINDOW_TAB.Creation) + { + ECEditor.VertexSelectEnabled = true; + } + else if (CurrentTab == ECE_WINDOW_TAB.Editing) + { + ECEditor.ColliderSelectEnabled = true; + } + } + _VHACDUpdatePreview = true; + } + + + private void PrefabStageClean(PrefabStage stage) + { + if (ECEditor.SelectedGameObject != null) + { + // cleanup object + ChangeToNewObject(null); + // save....... +#if (UNITY_2020_1_OR_NEWER) + PrefabUtility.SaveAsPrefabAsset(stage.prefabContentsRoot, stage.assetPath); +#else + PrefabUtility.SaveAsPrefabAsset(stage.prefabContentsRoot, stage.prefabAssetPath); +#endif + } + _VHACDUpdatePreview = true; + } +#endif + + + + /// + /// Register to editorapplication.update to check VHACD progress. + /// + void OnUpdate() + { + // Fixes issue where alt-tab loses focus but locks vertex snap method to remove until a key is pressed. + // should now reset to both when focus is regained and update is called. + if (!InternalEditorUtility.isApplicationActive) + { + _AltTabFocusChange = true; + } + else if (_AltTabFocusChange) + { + _AltTabFocusChange = false; + ECEPreferences.VertexSnapMethod = VERTEX_SNAP_METHOD.Both; + } + // update tips in onupdate, as we use a timer now. + if (ECEPreferences.DisplayTips) + { + UpdateTips(); + } + if (_VHACDIsComputing) + { + CheckVHACDProgress(); + } + if (_trackedMouseDownEvent != null) + { + _numUpdatesForTrackedLastMouseDown++; + CheckSelChangedForDrag(); + } + } + + /// + /// Creates a collider using the current preview. + /// + void CreateFromPreview() + { + // make sure the current preview is valid first, doesn't need preview data validity check anymore, as the number of vertices + // is checked before creating the collider in the method. + if (CurrentTab == ECE_WINDOW_TAB.Creation) + { + CreateCollider(ECEPreferences.PreviewColliderType, "Create Collider"); + } + else if (CurrentTab == ECE_WINDOW_TAB.Editing) + { + MergeColliders(ECEPreferences.PreviewColliderType, "Merge Colliders"); + } + } + + /// + /// Draws the GUI + /// + void OnGUI() + { + // check vert keys when window is focused as well. + if (!EditorGUIUtility.editingTextField) + { + CheckVertexToolsKeys(); + } + // Clear editor window's lists if we've deselected the objects. + if (!ECEditor.VertexSelectEnabled || ECEditor.SelectedGameObject == null || ECEditor.MeshFilters.Count == 0) + { + CurrentHoveredVertices = new HashSet(); + CurrentSelectBoxVerts = new HashSet(); + } + // scrollable window. + _ScrollPosition = EditorGUILayout.BeginScrollView(_ScrollPosition); + // draw settings for selecting gameobject / attach to / common settings / finish button. + DrawTopSettings(); + // common settings for all colliders created are drawn + DrawCreatedColliderSettings(); + // line above toolbar, below created collider settings. + ECUI.HorizontalLineLight(); + // draw the toolbar. (also draws the toolbar item) + DrawToolbar(); + // line after toolbar before the selected section. + ECUI.HorizontalLineLight(); + DrawSelectedToolbar(); + // line after each section before preferences. + ECUI.HorizontalLineLight(); + // draw preferences + DrawPreferences(); + // Add a flexible space, so tips are displayed at the bottom of window. + GUILayout.FlexibleSpace(); + // Draw tips + DrawTips(); + // End of gui + // end scroll view. + EditorGUILayout.EndScrollView(); + + + } + + + /// + /// returns true if the object is the selected object, the attach to object, or a child of either. + /// + /// + /// + bool IsValidEditingSelection(GameObject go) + { + if (go == null) return false; + return (go == ECEditor.SelectedGameObject + || go == ECEditor.AttachToObject + || (ECEditor.SelectedGameObject != null && go.transform.IsChildOf(ECEditor.SelectedGameObject.transform)) + || (ECEditor.AttachToObject != null && go.transform.IsChildOf(ECEditor.AttachToObject.transform))); + } + + GameObject _lastValidSelectedGameObject; + /// + /// Does raycasts and selection in the scene view updates. + /// + /// + void OnSceneGUI(SceneView sceneView) + { + + #region Various fixes for bugs in scene view. + + + // Cleanup object if we're going into play mode. + if (EditorApplication.isPlayingOrWillChangePlaymode) + { +#if (UNITY_2018_3_OR_NEWER) + PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage(); + if (stage != null) + { + PrefabStageClean(stage); + } +#endif + ECEditor.CleanUpObject(ECEditor.SelectedGameObject, true); + _VHACDIsComputing = false; + } + + // fixes bug where user can delete the current selected object without it being detected. + if (ECEditor.SelectedGameObject == null && ECEditor.SelectedVertices.Count > 0) + { + if (ECEditor.MeshFilters.Count > 0) + { + ECEditor.MeshFilters = new List(); + ECEditor.ClearSelectedVertices(); + } + } + if (ECEditor.SelectedGameObject == null) return; + + + bool isValidEditingSelection = IsValidEditingSelection(Selection.activeGameObject); + // keep track of last selected gameobject. + if (isValidEditingSelection) + { + _lastValidSelectedGameObject = Selection.activeGameObject; + } + // without background selection enabled, we want to keep selection on whatever the user has selected: the selected GO or the atatch to object (for collider visibility) + if (!ECEPreferences.AllowBackgroundSelection && !isValidEditingSelection && ECEditor.SelectedGameObject != null) + { + // is something previously selected still valid to be selected? re-select it. + if (_lastValidSelectedGameObject != null) + { + Selection.activeGameObject = _lastValidSelectedGameObject; + } + else + { + Selection.activeGameObject = ECEditor.SelectedGameObject; + } + } + + // fixes bugs with multi-sceneviews open side by side in the editor. + if (CurrentInputEventSceneView != null) + { + if (CurrentInputEventSceneView != sceneView) return; + } + + // forces enable vertex selection when in that tab, this was previously not done because we would ALWAYS force focus the scene view for raycasts + // but now we focus the scene after interactions with this window, so other things can still be edited with various selections enabled. + if (CurrentTab == ECE_WINDOW_TAB.Creation && !ECEditor.VertexSelectEnabled) + { + ECEditor.VertexSelectEnabled = true; + } + if (CurrentTab == ECE_WINDOW_TAB.VHACD && ECEPreferences.VHACDParameters.UseSelectedVertices && !ECEditor.VertexSelectEnabled) + { + ECEditor.VertexSelectEnabled = true; + } + + + #endregion + bool vhacdUseSelectedVertices = false; + vhacdUseSelectedVertices = (CurrentTab == ECE_WINDOW_TAB.VHACD && VHACDCurrentParameters.UseSelectedVertices); + // shortcut keys to change preview / create from preview / double tap to create collider when scene view is focused. + if ((CurrentTab == ECE_WINDOW_TAB.Creation || CurrentTab == ECE_WINDOW_TAB.Editing || vhacdUseSelectedVertices) + && ECEditor.SelectedGameObject != null + && SceneView.currentDrawingSceneView == EditorWindow.focusedWindow && SceneView.lastActiveSceneView == SceneView.currentDrawingSceneView) + { + CheckVertexToolsKeys(); + } + + // Only use the mouse drag events if vert select is enabled. + if ((ECEditor.VertexSelectEnabled || ECEditor.ColliderSelectEnabled) + && ECEditor.SelectedGameObject != null + && SceneView.currentDrawingSceneView == EditorWindow.focusedWindow + && Camera.current != null) + { + CheckSelectionInputEvents(); + } + else + { + // reset vertex selection keys. + IsMouseDown = false; + IsMouseDownModified = false; + IsMouseDragged = false; + IsMouseDraggedModified = false; + IsMouseRightDown = false; + IsMouseRightDragged = false; + CurrentModifiersPressed = EventModifiers.None; + LastModifierPressed = EventModifiers.None; + KeyCodePressOrder = new List(); + } + + // Selection box vertex selection. + BoxSelect(); + + // Vertex / Collider raycast selection. + // Do vertex selection by raycast only occasionally, and if we are able to + if (!IsMouseDragged // not dragging + && EditorApplication.timeSinceStartup - _LastSelectionTime > ECEPreferences.RaycastDelayTime // raycast occasionally + && SceneView.currentDrawingSceneView == EditorWindow.focusedWindow // if we're focused on the scene view + && (ECEditor.VertexSelectEnabled || ECEditor.ColliderSelectEnabled) // and selection is enabled + && ECEditor.SelectedGameObject != null // and theres something selected + // && ECEditor.MeshFilters.Count > 0 // and there's mesh filters. (not needed anymore) + && Camera.current != null & Event.current != null) // and there's a camera and an event to use. + { + _LastSelectionTime = EditorApplication.timeSinceStartup; + RaycastSelect(); + } + + + + + // Update vertex displays + CheckUpdateVertexDisplays(); + // Update previews + if (ECEPreferences.PreviewEnabled) + { + UpdatePreview(); + } + // Display mesh vertices + if (ECEditor.SelectedGameObject != null && ECEPreferences.DisplayAllVertices) + { + DrawAllVertices(); + } + // update if transforms have moved + ECEditor.HasTransformMoved(true); + + // fix order of preview drawing and selected collider drawning so that the hover functionality for colliders is more visible. + // Draw selected collider if it's enabled and we have one. + if (ECEditor.ColliderSelectEnabled && ECEditor.SelectedGameObject != null) + { + // Update the selected collider displays. + UpdateColliderDisplays(); + } + else + { + // clear if object is finished / no longer selected. + _CurrentHoveredCollider = null; + } + + if (ECEPreferences.VHACDPreview && ECEditor.SelectedGameObject != null && CurrentTab == ECE_WINDOW_TAB.VHACD) + { + // + if (VHACDPreviewResult != null) + { + _ECPreviewer.DrawVHACDResultPreview(VHACDPreviewResult); + } + else if (_VHACDCurrentParameters.ConvertTo != VHACD_CONVERSION.None) + { + _ECPreviewer.DrawVHACDConversionPreview(_ECEditor._VHACDConvertedData, _VHACDCurrentParameters.ConvertTo); + } + } + + DrawCursorForVertexSnapInSceneView(sceneView); + } + + /// + /// Repaints the editor window when undo/redo is done. + /// + void OnUndoRedoPerformed() + { + ECEditor.VerifyMeshFiltersOnUndoRedo(); + SetVHACDNeedsUpdate(); + ECPreviewer.ClearPreview(); + Repaint(); + } + #endregion + + // ------------------------------------------------------------------------------------------------------------------------------- + // ------------------------------------------------------------------------------------------------------------------------------- + #region ColliderCreationShortcuts + + /// + /// Max time between key pressed to be a double tap. + /// + const float ColliderDoubleTapTimeMax = 0.33f; + + /// + /// Time the last key was pressed + /// + private float ColliderDoubleTapTimeStart; + /// + /// Last collider creation key pressed + /// + private KeyCode ColliderLastKeyPressed; + + + /// + /// Checks if a vertex key has been double tapped. + /// + /// key released + /// true if a double tap. + private bool IsColliderCreateKeyCodeDoubleTapped(KeyCode key) + { + float currentTime = (float)EditorApplication.timeSinceStartup; + if (key == ColliderLastKeyPressed && (currentTime - ColliderDoubleTapTimeStart) < ColliderDoubleTapTimeMax) + { + return true; + } + else + { + ColliderLastKeyPressed = key; + ColliderDoubleTapTimeStart = currentTime; + return false; + } + } + + /// + /// Checks if any of the vertex tools keys were clicked (switch preview, create from preview etc) + /// + private void CheckVertexToolsKeys() + { + if (Event.current != null && Event.current.isKey) + { + bool validShortcut = false; + Event e = Event.current; + int controlID = GUIUtility.GetControlID(FocusType.Passive); + EventType type = e.GetTypeForControl(controlID); + if (type == EventType.KeyDown) + { + if (e.keyCode == ECEPreferences.CreateFromPreviewKey) + { + e.Use(); + CreateFromPreview(); + this.Repaint(); + } + if (e.keyCode == KeyCode.Alpha1 || e.keyCode == KeyCode.Keypad1) + { + e.Use(); + ECEPreferences.PreviewColliderType = CREATE_COLLIDER_TYPE.BOX; + validShortcut = true; + this.Repaint(); + } + if (e.keyCode == KeyCode.Alpha2 || e.keyCode == KeyCode.Keypad2) + { + e.Use(); + ECEPreferences.PreviewColliderType = CREATE_COLLIDER_TYPE.ROTATED_BOX; + validShortcut = true; + this.Repaint(); + } + if (e.keyCode == KeyCode.Alpha3 || e.keyCode == KeyCode.Keypad3) + { + e.Use(); + ECEPreferences.PreviewColliderType = CREATE_COLLIDER_TYPE.SPHERE; + validShortcut = true; + this.Repaint(); + } + if (e.keyCode == KeyCode.Alpha4 || e.keyCode == KeyCode.Keypad4) + { + e.Use(); + ECEPreferences.PreviewColliderType = CREATE_COLLIDER_TYPE.CAPSULE; + validShortcut = true; + this.Repaint(); + } + if (e.keyCode == KeyCode.Alpha5 || e.keyCode == KeyCode.Keypad5) + { + e.Use(); + ECEPreferences.PreviewColliderType = CREATE_COLLIDER_TYPE.ROTATED_CAPSULE; + validShortcut = true; + this.Repaint(); + } + if (e.keyCode == KeyCode.Alpha6 || e.keyCode == KeyCode.Keypad6) + { + e.Use(); + ECEPreferences.PreviewColliderType = CREATE_COLLIDER_TYPE.CONVEX_MESH; + validShortcut = true; + this.Repaint(); + } + if (e.keyCode == KeyCode.Alpha7 || e.keyCode == KeyCode.Keypad7) + { + e.Use(); + ECEPreferences.PreviewColliderType = CREATE_COLLIDER_TYPE.CYLINDER; + validShortcut = true; + this.Repaint(); + } + if (validShortcut && IsColliderCreateKeyCodeDoubleTapped(e.keyCode)) + { + CreateFromPreview(); + } + + if (ECEPreferences.EnableVertexToolsShortcuts) + { + if (e.keyCode == ECEPreferences.ShortcutClear && ECEditor.SelectedVertices.Count > 0) + { + // e.Use(); + UseVertexSelectionTool(VertexSelectionTool.Clear); + } + if (e.keyCode == ECEPreferences.ShortcutGrow && ECEditor.SelectedVertices.Count > 0) + { + // e.Use(); + UseVertexSelectionTool(VertexSelectionTool.Grow); + } + if (e.keyCode == ECEPreferences.ShortcutGrowLast && ECEditor.SelectedVertices.Count > 0) + { + // e.Use(); + UseVertexSelectionTool(VertexSelectionTool.GrowLast); + } + if (e.keyCode == ECEPreferences.ShortcutInvert && ECEditor.SelectedGameObject != null && ECEditor.MeshFilters.Count > 0) + { + // e.Use(); + UseVertexSelectionTool(VertexSelectionTool.Invert); + } + if (e.keyCode == ECEPreferences.ShortcutRing && ECEditor.SelectedVertices.Count >= 2) + { + // e.Use(); + UseVertexSelectionTool(VertexSelectionTool.Ring); + } + } + } + } + } + + #endregion + + /// + /// Clears the CurrentHoveredVertices list if one of the single point or vertex transforms is not null. + /// + private void ClearCurrentHoveredSinglePoints() + { + if (_CurrentHoveredTransform != null || _CurrentHoveredPointTransform != null) + { + CurrentHoveredVertices.Clear(); + _CurrentHoveredTransform = null; + _CurrentHoveredPointTransform = null; + } + } + + + #region Mouse And Keyboard Vertex Selection Input Handling + + /// + /// Sceneview that a mouse down or other was initially done in. + /// Fixes issues where multiple sceneviews are open beside one another. + /// + private SceneView CurrentInputEventSceneView; + + /// + /// Is the mouse pressed down? + /// + private bool IsMouseDown = false; + + /// + /// Is the mouse currently being dragged? + /// + private bool IsMouseDragged = false; + + /// + /// Did the original mouse down event have a modifier key attached? + /// /// + private bool IsMouseDownModified = false; + + /// + /// Does the mouse drag event have a modified key? (If the mouse down has a modifier, and then the mouse is dragged, this is true) + /// + private bool IsMouseDraggedModified = false; + + /// + /// Is the right mouse button down? + /// + private bool IsMouseRightDown = false; + + /// + /// Was the right mouse button dragged? + /// + private bool IsMouseRightDragged = false; + + /// + /// The last modifier key was that pressed + /// + private EventModifiers LastModifierPressed = EventModifiers.None; + /// + /// the current combination of modifiers that are pressed. + /// + private EventModifiers CurrentModifiersPressed = EventModifiers.None; + + /// + /// Current hot control id + /// + private int currentHotControl = 0; + + /// + /// Order in which keys were pressed (box-, box+, ctrl modifier, alt modifier keys tracked) Last item is the last key presed. + /// + List KeyCodePressOrder = new List(); + + + + /// + /// Updates the vertex snap method in preferences based on the last keycode KeyCodePressOrder + /// + /// True if the snap method was changed, false otherwise. + private bool UpdateVertexSnapByKeyOrder() + { + KeyCode last = KeyCodePressOrder.LastOrDefault(); + if (last == KeyCode.LeftAlt || last == ECEPreferences.BoxSelectMinusKey) + { + //Debug.Log("Add cursor rect?"); + //EditorGUIUtility.AddCursorRect(new Rect(20, 20, 140, 40), MouseCursor.ArrowMinus); + if (ECEPreferences.VertexSnapMethod != VERTEX_SNAP_METHOD.Remove) + { + ECEPreferences.VertexSnapMethod = VERTEX_SNAP_METHOD.Remove; + return true; + } + } + else if (last == KeyCode.LeftControl || last == ECEPreferences.BoxSelectPlusKey) + { + //Debug.Log("Add cursor rect?"); + //EditorGUIUtility.AddCursorRect(new Rect(20, 20, 140, 40), MouseCursor.ArrowPlus); + if (ECEPreferences.VertexSnapMethod != VERTEX_SNAP_METHOD.Add) + { + ECEPreferences.VertexSnapMethod = VERTEX_SNAP_METHOD.Add; + return true; + } + } + else + { + if (ECEPreferences.VertexSnapMethod != VERTEX_SNAP_METHOD.Both) + { + ECEPreferences.VertexSnapMethod = VERTEX_SNAP_METHOD.Both; + return true; + } + } + return false; + } + + void DrawCursorForVertexSnapInSceneView(SceneView sceneView) + { + // just do a full screen rect based on whatever the last key press order was for selection. + KeyCode last = KeyCodePressOrder.LastOrDefault(); + if (last == KeyCode.LeftAlt || last == ECEPreferences.BoxSelectMinusKey) + { + Rect fullRect = new Rect(0, 0, sceneView.position.width, sceneView.position.height); + EditorGUIUtility.AddCursorRect(fullRect, MouseCursor.ArrowMinus); + } + else if (last == KeyCode.LeftControl || last == ECEPreferences.BoxSelectPlusKey) + { + Rect fullRect = new Rect(0, 0, sceneView.position.width, sceneView.position.height); + EditorGUIUtility.AddCursorRect(fullRect, MouseCursor.ArrowPlus); + } + } + + /// + /// Tracks the order in which the important keys for vertex selection were pressed. + /// + private void TrackVertSelectionKeyPressOrder() + { + // don't track without a selected gameobject. + if (ECEditor.SelectedGameObject == null) return; + EventModifiers modifierReleased = EventModifiers.None; + int count = KeyCodePressOrder.Count; + // track modifiers and key press order. + if (Event.current != null) + { + if (Event.current.type == EventType.KeyDown) + { + // keep track of which modifier key was pressed down last. + if (Event.current.modifiers != CurrentModifiersPressed) + { + // update current modifiers held down + CurrentModifiersPressed = Event.current.modifiers; + if ((int)Event.current.modifiers == 6) + { + // if we have ctrl and alt held down, calcualte the one that was most recently pressed. + LastModifierPressed = 6 - LastModifierPressed; + } + else + { + // the last key pressed is the current modifier key. + LastModifierPressed = Event.current.modifiers; + } + // use left alt and left ctrl keycodes to keep track. + if (LastModifierPressed == EventModifiers.Alt && !KeyCodePressOrder.Contains(KeyCode.LeftAlt)) + { + KeyCodePressOrder.Add(KeyCode.LeftAlt); + } + else if (LastModifierPressed == EventModifiers.Control && !KeyCodePressOrder.Contains(KeyCode.LeftControl)) + { + KeyCodePressOrder.Add(KeyCode.LeftControl); + } + } + // // keyboards can send keys multiple times. + if (Event.current.keyCode == ECEPreferences.BoxSelectMinusKey && !KeyCodePressOrder.Contains(ECEPreferences.BoxSelectMinusKey)) + { + KeyCodePressOrder.Add(ECEPreferences.BoxSelectMinusKey); + } + else if (Event.current.keyCode == ECEPreferences.BoxSelectPlusKey && !KeyCodePressOrder.Contains(ECEPreferences.BoxSelectPlusKey)) + { + KeyCodePressOrder.Add(ECEPreferences.BoxSelectPlusKey); + } + else if (Event.current.keyCode == KeyCode.LeftAlt && !KeyCodePressOrder.Contains(KeyCode.LeftAlt)) + { + KeyCodePressOrder.Add(KeyCode.LeftAlt); + } + else if (Event.current.keyCode == KeyCode.LeftControl && !KeyCodePressOrder.Contains(KeyCode.LeftControl)) + { + KeyCodePressOrder.Add(KeyCode.LeftControl); + } + } + else if (Event.current.type == EventType.KeyUp) + { + // keep track of current modifiers held down. + if (Event.current.modifiers != CurrentModifiersPressed) + { + // calc modifier was just released + modifierReleased = (EventModifiers)(CurrentModifiersPressed - Event.current.modifiers); + // update our current modifiers. + CurrentModifiersPressed = Event.current.modifiers; + LastModifierPressed = Event.current.modifiers; + if (modifierReleased == EventModifiers.Alt) + { + KeyCodePressOrder.Remove(KeyCode.LeftAlt); + } + else if (modifierReleased == EventModifiers.Control) + { + KeyCodePressOrder.Remove(KeyCode.LeftControl); + } + } + if (Event.current.keyCode == ECEPreferences.BoxSelectMinusKey) + { + KeyCodePressOrder.Remove(ECEPreferences.BoxSelectMinusKey); + } + else if (Event.current.keyCode == ECEPreferences.BoxSelectPlusKey) + { + KeyCodePressOrder.Remove(ECEPreferences.BoxSelectPlusKey); + } + else if (Event.current.keyCode == KeyCode.LeftAlt) + { + KeyCodePressOrder.Remove(KeyCode.LeftAlt); + } + else if (Event.current.keyCode == KeyCode.LeftControl) + { + KeyCodePressOrder.Remove(KeyCode.LeftControl); + } + } + else if (Event.current.modifiers == EventModifiers.None) + { + // fixes bug in unity 6 with it's new piercing context selection menu on control + right click + // which seems to prevent normal events from processing in a way that old unity events in previous versions used to + if (KeyCodePressOrder.Contains(KeyCode.LeftControl)) + { + if (KeyCodePressOrder.Remove(KeyCode.LeftControl)) + { + UpdateVertexSnapByKeyOrder(); + } + } + } + } + // updates displayed snaps. + if (count != KeyCodePressOrder.Count) + { + Repaint(); + } + } + + /// + /// Rests all the mouse tracking variables we use. + /// + private void ResetMouseTrackingVariables() + { + IsMouseDown = false; + IsMouseDragged = false; + IsMouseDownModified = false; + IsMouseDraggedModified = false; + IsMouseRightDown = false; + IsMouseRightDragged = false; + CurrentInputEventSceneView = null; + } + Event _trackedMouseDownEvent; + int _numUpdatesForTrackedLastMouseDown = 0; + GameObject _lastSelectionGameobject; + void CheckSelChangedForDrag() + { + // because something was changed that specifically changed functionality in 2022.3.14 compared to every previous version, we'll have to do this. + // currently goes up to 2022_3_50. +#if UNITY_2022_3_14 || UNITY_2022_3_15 || UNITY_2022_3_16 || UNITY_2022_3_17 || UNITY_2022_3_18 || UNITY_2022_3_19 || UNITY_2022_3_20 || UNITY_2022_3_21 || UNITY_2022_3_22 || UNITY_2022_3_23 || UNITY_2022_3_24 || UNITY_2022_3_25 || UNITY_2022_3_26 || UNITY_2022_3_27 || UNITY_2022_3_28 || UNITY_2022_3_29 || UNITY_2022_3_30 ||UNITY_2022_3_31 ||UNITY_2022_3_32 ||UNITY_2022_3_33 || UNITY_2022_3_34 || UNITY_2022_3_35 || UNITY_2022_3_36 || UNITY_2022_3_37 || UNITY_2022_3_38 || UNITY_2022_3_39 || UNITY_2022_3_40 || UNITY_2022_3_41 || UNITY_2022_3_42 || UNITY_2022_3_43 || UNITY_2022_3_44 || UNITY_2022_3_45 || UNITY_2022_3_46 || UNITY_2022_3_47 || UNITY_2022_3_48 || UNITY_2022_3_49 || UNITY_2022_3_50 || UNITY_2022_3_51 || UNITY_2022_3_52 || UNITY_2022_3_53 || UNITY_2022_3_54 || UNITY_2022_3_55 || UNITY_2022_3_56 || UNITY_2022_3_57 || UNITY_2022_3_58 || UNITY_2022_3_59 || UNITY_2022_3_60 || UNITY_2022_3_61 || UNITY_2022_3_62 || UNITY_2022_3_63 || UNITY_2022_3_64 || UNITY_2022_3_65 || UNITY_2022_3_66 || UNITY_2022_3_67 || UNITY_2022_3_68 || UNITY_2022_3_69 || UNITY_2022_3_70 || UNITY_2023_1_OR_NEWER +#else + if (_trackedMouseDownEvent != null) + { + // Debug.Log("Last event type:" + _trackedMouseDownEvent.type); + // the event will be EventType.Used when a unity drag begins, OR when the mouse goes Up and selection changes. + if (_trackedMouseDownEvent.type == EventType.Used) + { + // something different was selected compared to what was previously selected on mouse down + // and that new selection is not null (user didn't click in empty space) + if (Selection.activeGameObject != _lastSelectionGameobject && Selection.activeGameObject != null) + { + // Debug.Log($"Selection changed to: {Selection.activeGameObject?.name}"); + } + // the user started a unity-controlled drag! (so user can click select, but not drag select scene objects when editing an object with ECE) + else + { + // create a hot control, which accomplishes what we want, overriding the default drag + // since a mouse down sets a control to non-0 by default, no new events are raised until mouse up / drag Use()'s it. + // by creating a new hot control after unity decides it is Use()'d, we can immediately override the default drag. + int controlID = GUIUtility.GetControlID(FocusType.Passive); + GUIUtility.hotControl = controlID; + currentHotControl = controlID; + } + // Debug.Log($"Num updates to used:{_numUpdatesForTrackedLastMouseDown}"); + _numUpdatesForTrackedLastMouseDown = 0; + _trackedMouseDownEvent = null; + } + } +#endif + } + + Event _mouseDownEvent; + /// + /// Checks for various vertex selection events. + /// Handles vertex selection, and box selection based on what is currently hovered as keys are pressed. + /// + private void CheckSelectionInputEvents() + { + // actual fix for all this might be using handles for clickable verts. + // then just handling the input for drag.... + + TrackVertSelectionKeyPressOrder(); + if (Event.current != null && Event.current.isMouse && Event.current.button == 0) + { + // Debug.Log(Event.current.type); + // need drag, down, and up event types. + // also need mouseleavewindow: if the user clicks and drags and releases the mouse button when the cursor + // is no longer over the window MouseLeaveWindow is the event type, so we need to handle that. + if (Event.current.type == EventType.MouseDrag || Event.current.type == EventType.MouseDown || Event.current.type == EventType.MouseUp || Event.current.type == EventType.MouseLeaveWindow) + { + if ((Event.current.type == EventType.MouseUp || Event.current.type == EventType.MouseLeaveWindow) && IsMouseDown) + { + // Mouse wasn't dragged (or dragged while modified), select the vertex. + if (!IsMouseDragged && IsMouseDown && !IsMouseDraggedModified && ECEPreferences.UseMouseClickSelection) + { + if (ECEditor.VertexSelectEnabled) + { + SelectVertex(_CurrentHoveredTransform, _CurrentHoveredPosition, isVertexSelection); + } + else if (ECEditor.ColliderSelectEnabled) + { + SelectCollider(_CurrentHoveredCollider); + } + // only reset the hot control when we've captured it. + } + // Mouse was dragged, select the vertices currently in the box. + else if (IsMouseDragged && IsMouseDown) + { + // fixes isue in 2022.3.14+ where finishing a drag would break the next drag, does not seem to effect previous versions at all + // so leaving it in there as well to avoid another gigantic list of compilation directives. + Event.current.Use(); + IsMouseDragged = false; // setting this here improves responsiveness. + if (ECEditor.VertexSelectEnabled) // only select if vert select is enabled. + { + SelectVerticesInBox(); + } + } + ResetMouseTrackingVariables(); + // Mouse is up, reset our tracking variables. + if (currentHotControl != 0) + { + GUIUtility.hotControl = 0; + currentHotControl = 0; + } + } + if (Event.current.type == EventType.MouseDrag && IsMouseDown) + { + if (!IsMouseDownModified && ECEditor.VertexSelectEnabled) + { + // We have a valid mouse drag. Lets clear the previous hovered points. + ClearCurrentHoveredSinglePoints(); + IsMouseDragged = true; + _CurrentDragPosition = Event.current.mousePosition; + // any in scene floating ui steals the events on mouse up. + // so we need to do this. + int controlID = GUIUtility.GetControlID(FocusType.Passive); + GUIUtility.hotControl = controlID; + // Important: If the event is not Use()d, the rect drawing of the box / vertices cubes do not work. + // (Likely because unity is also trying to draw it's own selection rect.) + Event.current.Use(); + } + else + { + IsMouseDraggedModified = true; + } + } +#if UNITY_2022_3_14 || UNITY_2022_3_15 || UNITY_2022_3_16 || UNITY_2022_3_17 || UNITY_2022_3_18 || UNITY_2022_3_19 || UNITY_2022_3_20 || UNITY_2022_3_21 || UNITY_2022_3_22 || UNITY_2022_3_23 || UNITY_2022_3_24 || UNITY_2022_3_25 || UNITY_2022_3_26 || UNITY_2022_3_27 || UNITY_2022_3_28 || UNITY_2022_3_29 || UNITY_2022_3_30 ||UNITY_2022_3_31 ||UNITY_2022_3_32 ||UNITY_2022_3_33 || UNITY_2022_3_34 || UNITY_2022_3_35 || UNITY_2022_3_36 || UNITY_2022_3_37 || UNITY_2022_3_38 || UNITY_2022_3_39 || UNITY_2022_3_40 || UNITY_2022_3_41 || UNITY_2022_3_42 || UNITY_2022_3_43 || UNITY_2022_3_44 || UNITY_2022_3_45 || UNITY_2022_3_46 || UNITY_2022_3_47 || UNITY_2022_3_48 || UNITY_2022_3_49 || UNITY_2022_3_50 || UNITY_2022_3_51 || UNITY_2022_3_52 || UNITY_2022_3_53 || UNITY_2022_3_54 || UNITY_2022_3_55 || UNITY_2022_3_56 || UNITY_2022_3_57 || UNITY_2022_3_58 || UNITY_2022_3_59 || UNITY_2022_3_60 || UNITY_2022_3_61 || UNITY_2022_3_62 || UNITY_2022_3_63 || UNITY_2022_3_64 || UNITY_2022_3_65 || UNITY_2022_3_66 || UNITY_2022_3_67 || UNITY_2022_3_68 || UNITY_2022_3_69 || UNITY_2022_3_70 || UNITY_2023_1_OR_NEWER + if (Event.current.type == EventType.MouseDown && (GUIUtility.hotControl == 0 || Event.current.modifiers == EventModifiers.Alt)) // alt automatically does a hot control it appears, this fixes that. + { + //Debug.Log("Mouse down."); + if (Event.current.modifiers == EventModifiers.None || Event.current.modifiers == EventModifiers.Control) + { + _trackedMouseDownEvent = Event.current; + _lastSelectionGameobject = Selection.activeGameObject; + IsMouseDown = true; + } + else + { + IsMouseDownModified = true; + IsMouseDown = true; + } + CurrentInputEventSceneView = SceneView.currentDrawingSceneView; + UpdateWorldScreenLocalSpaceVertexLists(); + _StartDragPosition = Event.current.mousePosition; + _CurrentDragPosition = Event.current.mousePosition; + + // to still allow rotation. + if (Event.current.modifiers != EventModifiers.Alt) + { + currentHotControl = GUIUtility.GetControlID(FocusType.Passive); + GUIUtility.hotControl = currentHotControl; + // prevents selection from changing when hovering over a valid mesh to select a vertex from or a valid collider to select. + if (_CurrentHoveredCollider != null || _CurrentHoveredTransform != null) + { + Event.current.Use(); + } + } + + } +#else + if (Event.current.type == EventType.MouseDown && (GUIUtility.hotControl != 0 || Event.current.modifiers == EventModifiers.Alt)) // alt automatically does a hot control it appears, this fixes that. + { + if (Event.current.modifiers == EventModifiers.None || Event.current.modifiers == EventModifiers.Control) + { + + // Only do a hot control, if we are hovering over a valid transform to select vertices from + // so if the Selection.activeGameObject changed, it would change to an object we're making colliders on. + if (_CurrentHoveredTransform != null || !ECEPreferences.AllowBackgroundSelection || _CurrentHoveredCollider != null) + { + // Only capture as a hot control if there are NO modifiers when the mouse is initially pressed down + // OR if the modifier is CTRL, as that doesn't interfere with other unity things + // ALT + LeftClick drag is used for rotation so this is needed. + int controlID = GUIUtility.GetControlID(FocusType.Passive); + GUIUtility.hotControl = controlID; + currentHotControl = controlID; + } + else + { + // Hovering an object we're not making colliders for, or empty space, so we have to track the event + // we track the event to see if the selected object changes, if it does, we don't want to start a drag. + // as the selected object will change on mouse up, a drag will trigger "Used" on the tracked event type + // so only single clicks on empty space(or objects) and not drags will be ignored. + _lastSelectionGameobject = Selection.activeGameObject; + _trackedMouseDownEvent = Event.current; + } + IsMouseDown = true; + } + else + { + IsMouseDownModified = true; + IsMouseDown = true; + } + CurrentInputEventSceneView = SceneView.currentDrawingSceneView; + UpdateWorldScreenLocalSpaceVertexLists(); + _StartDragPosition = Event.current.mousePosition; + _CurrentDragPosition = Event.current.mousePosition; + } +#endif + } + } + // Arbitrary point (non-vertex) mouse selection with right click. + else if (Event.current != null && Event.current.isMouse && Event.current.button == 1) + { + // right click. + if (Event.current.type == EventType.MouseDown) + { + CurrentInputEventSceneView = SceneView.currentDrawingSceneView; + IsMouseRightDown = true; +#if (UNITY_6000_0_OR_NEWER) + // override unity's new piercing select menu wehn we are hovering over the selected object. (Unity 6) + if (!ECEPreferences.AllowBackgroundSelection && Event.current.modifiers == EventModifiers.Control && (_CurrentHoveredPointTransform != null || _CurrentHoveredTransform != null)) + { + Event.current.Use(); + } +#endif + } + else if (Event.current.type == EventType.MouseDrag) + { + IsMouseRightDragged = true; + } + else if (Event.current.type == EventType.MouseUp && IsMouseRightDown && ECEPreferences.UseMouseClickSelection) + { + // only select arbitrary point with non-drag events. + if (!IsMouseRightDragged) + { + if (ECEPreferences.VertexSnapMethod == VERTEX_SNAP_METHOD.Add || ECEPreferences.VertexSnapMethod == VERTEX_SNAP_METHOD.Both) + { + SelectVertex(_CurrentHoveredPointTransform, _CurrentHoveredPoint, false); +#if (UNITY_6000_0_OR_NEWER) + // fixes context menu popping up regardless of where you right click in scene (Unity 6) + // now will only pop up if not hovering over anything. + if (_CurrentHoveredTransform != null || _CurrentHoveredPointTransform != null) + { + Event.current.Use(); + } +#endif + } + else + { + SelectVertex(_CurrentHoveredTransform, _CurrentHoveredPosition, false); + } + } + ResetMouseTrackingVariables(); + } + } + // Box Selection + // mouse dragged and modifier keys handled + else if (!IsMouseDownModified && IsMouseDragged && Event.current != null && Event.current.isKey) + { + // if it's a key down + if (Event.current.type == EventType.KeyDown) + { + // if the snap method was changed + if (UpdateVertexSnapByKeyOrder()) + { + // then update box selection. + BoxSelect(true); + } + } + // key was released, repeat above. + else if (Event.current.type == EventType.KeyUp) + { + if (UpdateVertexSnapByKeyOrder()) + { + BoxSelect(true); + } + } + } + // vertex selection keys (non-box selection) + else if (Event.current.isKey) + { + // if the snap method changes + if (UpdateVertexSnapByKeyOrder()) + { + // raycast select. + RaycastSelect(); + } + // check key codes. + if (Event.current.type == EventType.KeyUp && Event.current.keyCode == ECEPreferences.VertSelectKeyCode) + { + // select vertex + if (ECEditor.VertexSelectEnabled) + { + SelectVertex(_CurrentHoveredTransform, _CurrentHoveredPosition, isVertexSelection); + } + else if (ECEditor.ColliderSelectEnabled) + { + SelectCollider(_CurrentHoveredCollider); + } + // raycast again immediately afterwards to update display of hovered vertex positions. + RaycastSelect(); + } + else if (Event.current.type == EventType.KeyUp && Event.current.keyCode == ECEPreferences.PointSelectKeyCode) + { + if (ECEPreferences.VertexSnapMethod == VERTEX_SNAP_METHOD.Add || ECEPreferences.VertexSnapMethod == VERTEX_SNAP_METHOD.Both) + { + SelectVertex(_CurrentHoveredPointTransform, _CurrentHoveredPoint, false); + RaycastSelect(); + } + else + { + // let point select remove the vertices as well. + SelectVertex(_CurrentHoveredTransform, _CurrentHoveredPosition, false); + } + } + } + } + + + #endregion + + /// + /// Checks if we need to update based on the selected vertex count, then updates the vertex display depending on if we're using gizmos, or shaders + /// This helps update when Undos/Redos are used. + /// + private void CheckUpdateVertexDisplays() + { + // Update the gizmos or compute if: + // total selected vertices is different, + // hovered vertices are different, + // or the transforms have moved. + if (ECEditor.Gizmos != null && (ECEditor.Gizmos.SelectedVertexPositions.Count != ECEditor.SelectedVertices.Count || ECEditor.Gizmos.HoveredVertexPositions.Count != CurrentHoveredVertices.Count || ECEditor.HasTransformMoved())) + { + UpdateVertexDisplays(); + } + if (ECEditor.Compute != null && (ECEditor.Compute.SelectedPointCount != ECEditor.SelectedVertices.Count || ECEditor.Compute.HoveredPointCount != CurrentHoveredVertices.Count || ECEditor.HasTransformMoved())) + { + UpdateVertexDisplays(); + } + } + + /// + /// Creates a collider of collider type, with the undo string being displayed. + /// + /// Type of collider to create + /// Undo string to be displayed. + private void CreateCollider(CREATE_COLLIDER_TYPE collider_type, string undoString) + { + // make sure we have min verts at least to create the collider too so we don't try to create one that makes no sense. + if (!HasMinimumVerticesForCollider(collider_type)) + { + Debug.LogWarning("Not enough vertices are selected to create a " + collider_type + " collider."); + return; + } + + bool isSingle = !ECEPreferences.rotatedDupeSettings.enabled; + Undo.RegisterCompleteObjectUndo(ECEditor.AttachToObject, undoString); + int group = Undo.GetCurrentGroup(); + Undo.RegisterCompleteObjectUndo(ECEditor, undoString); + GameObject attachTo = ECEditor.AttachToObject; + if (ECEPreferences.ColliderHolder != COLLIDER_HOLDER.Default) + { + GameObject colliderHolder = null; + Transform t = null; + // once -> create a new collider once, and then find it for future uses. + if (ECEPreferences.ColliderHolder == COLLIDER_HOLDER.Once) + { + t = ECEditor.AttachToObject.transform.Find("EasyColliderHolder"); + colliderHolder = t != null ? t.gameObject : null; + } + // if it's still null (didn't find it for once, or is set to always) + if (colliderHolder == null) + { + // create a new collider holder. + colliderHolder = new GameObject("EasyColliderHolder"); + colliderHolder.transform.parent = ECEditor.AttachToObject.transform; + colliderHolder.transform.localPosition = Vector3.zero; + colliderHolder.transform.localRotation = Quaternion.identity; + colliderHolder.transform.localScale = Vector3.one; + if (!ECEPreferences.CopyParentObjectLayer) + { + colliderHolder.layer = ECEditor.GameObjectLayerOverride; + } + else + { + colliderHolder.layer = ECEditor.AttachToObject.layer; + } + Undo.RegisterCreatedObjectUndo(colliderHolder, "Create Collider Holder Object"); + } + ECEditor.AttachToObject = colliderHolder; + } + + + // Create colliders: + if (isSingle) + { + switch (collider_type) + { + case CREATE_COLLIDER_TYPE.BOX: + ECEditor.CreateBoxCollider(); + break; + case CREATE_COLLIDER_TYPE.ROTATED_BOX: + ECEditor.CreateBoxCollider(COLLIDER_ORIENTATION.ROTATED); + break; + case CREATE_COLLIDER_TYPE.SPHERE: + ECEditor.CreateSphereCollider(ECEPreferences.SphereColliderMethod); + break; + case CREATE_COLLIDER_TYPE.CAPSULE: + ECEditor.CreateCapsuleCollider(ECEPreferences.CapsuleColliderMethod); + break; + case CREATE_COLLIDER_TYPE.ROTATED_CAPSULE: + ECEditor.CreateCapsuleCollider(ECEPreferences.CapsuleColliderMethod, COLLIDER_ORIENTATION.ROTATED); + break; + case CREATE_COLLIDER_TYPE.CONVEX_MESH: + ECEditor.CreateConvexMeshCollider(ECEPreferences.MeshColliderMethod); + break; + case CREATE_COLLIDER_TYPE.CYLINDER: + ECEditor.CreateCylinderCollider(); + break; + } + } + else + { + ECEditor.CreateRotatedAndDuplicatedColliders(collider_type); + } + // reset attach to object. + ECEditor.AttachToObject = attachTo; + Undo.CollapseUndoOperations(group); + FocusSceneView(); + } + + #region UI Drawing Methods + + /// + /// Draws all vertices in Editor's mesh filter list + /// /// + private void DrawAllVertices() + { + if (ECEditor.Gizmos != null) + { + if (ECEditor.Gizmos.DisplayVertexPositions.Count != ECEditor.WorldMeshVertices.Count || ECEditor.HasTransformMoved()) + { + ECEditor.Gizmos.DisplayVertexPositions = ECEditor.GetAllWorldMeshVertices(); + } + } + else if (ECEditor.Compute != null) + { + if (ECEditor.Compute.DisplayPointCount != ECEditor.WorldMeshVertices.Count || ECEditor.HasTransformMoved()) + { + ECEditor.Compute.SetDisplayAllBuffer(ECEditor.GetAllWorldMeshVertices()); + } + } + } + + + /// + /// Draws the UI for automatically generating colliders along a skinned mesh's bones. + /// + private void DrawAutoSkinnedMeshTools() + { + if (ECEditor.SelectedGameObject != null && (ECPreviewer.AutoSkinnedData == null || ECPreviewer.AutoSkinnedData.Count == 0) && ECEPreferences.PreviewEnabled) + { + if (ECAutoSkinned.BoneList.Count == 0) + { + Undo.RegisterCompleteObjectUndo(ECAutoSkinned, "Initial Auto Skinned Scan"); + int group = Undo.GetCurrentGroup(); + ECAutoSkinned.InitialScanBones(ECEditor.SelectedGameObject, ECEPreferences.AutoSkinnedMinBoneWeight); + ECAutoSkinned.SetColliderTypeOnAllBones(ECEPreferences.AutoSkinnedColliderType); + Undo.CollapseUndoOperations(group); + } + SkinnedMeshRenderer[] smrs = ECEditor.SelectedGameObject.GetComponentsInChildren(); + if (smrs.Length > 0) + { + ECPreviewer.ClearPreview(); + ECPreviewer.AutoSkinnedData = ECAutoSkinned.CalculateSkinnedMeshPreview(smrs[0], ECEPreferences.AutoSkinnedColliderType, ECEditor.GetProperties(), ECEPreferences.AutoSkinnedMinBoneWeight, ECEPreferences.AutoSkinnedAllowRealign, ECEPreferences.AutoSkinnedMinRealignAngle); + } + } + + EditorGUI.BeginChangeCheck(); + + EditorGUILayout.BeginHorizontal(); + ECUI.LabelBold("Auto Skinned Mesh Colliders"); + GUILayout.FlexibleSpace(); + ECEPreferences.PreviewEnabled = ECUI.ToggleLeft(new GUIContent("Draw Preview", "When enabled, draws a preview of the collider that would be created from the selected points."), ECEPreferences.PreviewEnabled, 50f); + EditorGUILayout.EndHorizontal(); + + if (ECEditor.SelectedGameObject != null && ECAutoSkinned.BoneList != null && ECAutoSkinned.BoneList.Count == 0) + { + ECUI.LabelIcon("No valid bones found on " + ECEditor.SelectedGameObject.name + ". This can occur when \"Optimize Game Objects\" is enabled on the mesh's rig import settings.\n\nThe recommendation is to temporarily disable optimization, generate colliders, then renable optimization. Colliders on exposed transforms when optimized should be correctly transferred.", "console.warnicon.sml"); + // Debug.LogWarning("Easy Collider Editor: Unable to find any valid bones. This occurs when optimized gameobject is enabled on the mesh's rig import settings. The recommendation is to temporarily disable optimization, generate colliders, then renable optimization. Colliders on exposed transforms when optimized should be correctly transferred."); + } + // settings on which verts to include for bones and realignment. + EditorGUILayout.BeginHorizontal(); + + // realignment + EditorGUI.BeginChangeCheck(); + bool allowRealign = ECUI.ToggleLeft(new GUIContent("Allow Realign", "If all of a bone's axis are further than Min Realign angle from the direction vector to next bone in the chain, child collider holders that are pointed toward the next bone will be created on that bone."), ECEPreferences.AutoSkinnedAllowRealign, 40f); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(ECEPreferences, "Change auto skinned parameters"); + ECEPreferences.AutoSkinnedAllowRealign = allowRealign; + } + if (ECEPreferences.AutoSkinnedAllowRealign) + { + EditorGUI.BeginChangeCheck(); + float minRealignAngle = EditorGUILayout.Slider(new GUIContent("Minimum Realign Angle", "Minimum angle all of a bone's axis must be away from the direction vector to the next bone to create a child transform to hold colliders."), ECEPreferences.AutoSkinnedMinRealignAngle, 0, 45f); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(ECEPreferences, "Change auto skinned parameters"); + ECEPreferences.AutoSkinnedMinRealignAngle = minRealignAngle; + } + } + EditorGUILayout.EndHorizontal(); + EditorGUILayout.BeginHorizontal(); + + // Depenetration + EditorGUI.BeginChangeCheck(); + bool depenetrate = ECUI.ToggleLeft(new GUIContent("Depenetrate", "Attempts to make colliders that do not overlap by iteratively shrinking and shifting colliders.\n\nAmount of shrink vs shift is controlled by the slider.\nThe order the colliders are processed is determined by the dropdown. \n\nIn Order: In order of the hierarchy.\nReverse: Reverse order of the hierarchy\nInside Out: Colliders processed from the root bone outwards to end bones.\nOutside In: Colliders are processed from end bones inwards to the root bone."), ECEPreferences.AutoSkinnedDepenetrate, 35f); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(ECEPreferences, "Change auto skinned parameters"); + ECEPreferences.AutoSkinnedDepenetrate = depenetrate; + } + if (ECEPreferences.AutoSkinnedDepenetrate) + { + ECUI.Label("Shift", "Amount of shrink to do on each depenetration step. 0 means colliders will only shift to depenetrate."); + EditorGUI.BeginChangeCheck(); + float shrinkAmount = EditorGUILayout.Slider(GUIContent.none, ECEPreferences.AutoSkinnedShrinkAmount, 0, 1, GUILayout.MinWidth(125), GUILayout.MaxWidth(125)); + ECUI.Label("Shrink", "Amount of shrink to do on each depenetration step. 0 means colliders will only shift to depenetrate."); + SKINNED_MESH_DEPENETRATE_ORDER depenMethod = (SKINNED_MESH_DEPENETRATE_ORDER)EditorGUILayout.EnumPopup(ECEPreferences.AutoSkinnedDepenetrateOrder); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(ECEPreferences, "Change auto skinned parameters."); + ECEPreferences.AutoSkinnedShrinkAmount = shrinkAmount; + ECEPreferences.AutoSkinnedDepenetrateOrder = depenMethod; + ECPreviewer.ClearPreview(); + ECPreviewer.AutoSkinnedData = ECAutoSkinned.CalculateSkinnedMeshPreview(ECAutoSkinned.renderer, ECEPreferences.AutoSkinnedColliderType, ECEditor.GetProperties(), ECEPreferences.AutoSkinnedMinBoneWeight, ECEPreferences.AutoSkinnedAllowRealign, ECEPreferences.AutoSkinnedMinRealignAngle); + UpdatePreview(true); + } + } + + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + + + + + if (!ECEPreferences.AutoSkinnedPerBoneSettings) + { + EditorGUI.BeginChangeCheck(); + float minimumBoneWeight = EditorGUILayout.Slider(new GUIContent("Minimum Bone Weight", "Minimum weight of a vertex on a bone to be included in the calculation for that bone's collider."), ECEPreferences.AutoSkinnedMinBoneWeight, 0, 1); + // this needs to be undo-able as it overrides per-bone collider type when changed. + //TODO: or just hide the option when the per-bone settings are expanded? + SKINNED_MESH_COLLIDER_TYPE skinnedMeshColliderType = (SKINNED_MESH_COLLIDER_TYPE)EditorGUILayout.EnumPopup(new GUIContent("Collider Type:", "Type of colliders to create along the skinned mesh bone chain. Capsule and Sphere Colliders both use the Min Max method to calculate the colliders."), ECEPreferences.AutoSkinnedColliderType); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(ECEPreferences, "Change Auto Skinned Settings"); + ECEPreferences.AutoSkinnedColliderType = skinnedMeshColliderType; + ECEPreferences.AutoSkinnedMinBoneWeight = minimumBoneWeight; + int group = Undo.GetCurrentGroup(); + if (!ECEPreferences.AutoSkinnedPerBoneSettings) + { + Undo.RecordObject(ECAutoSkinned, "Change Auto Skinned Settings"); + ECAutoSkinned.SetColliderTypeAndWeightOnAllBones(ECEPreferences.AutoSkinnedColliderType, ECEPreferences.AutoSkinnedMinBoneWeight); + } + Undo.CollapseUndoOperations(group); + } + } + + if (ECEPreferences.AutoSkinnedColliderType == SKINNED_MESH_COLLIDER_TYPE.Convex_Mesh) + { + ECEPreferences.AutoSkinnedForce256Triangles = EditorGUILayout.ToggleLeft(new GUIContent("Force <256 triangles", "When enabled, iteratively welds vertices as needed so that each convex mesh collider reaches a target triangle count of less than 256.\n\nWhen enabled the preview may not perfectly match the result, and generation may take longer."), ECEPreferences.AutoSkinnedForce256Triangles); + } + else + { + bool showForce256Triangles = false; + for (int i = 0; i < ECAutoSkinned.BoneList.Count; i++) + { + if (ECAutoSkinned.BoneList[i].ColliderType == SKINNED_MESH_COLLIDER_TYPE.Convex_Mesh) + { + showForce256Triangles = true; + break; + } + } + if (showForce256Triangles) + { + ECEPreferences.AutoSkinnedForce256Triangles = EditorGUILayout.ToggleLeft(new GUIContent("Force <256 triangles", "When enabled, iteratively welds vertices as needed so that each convex mesh collider reaches a target triangle count of less than 256.\n\nWhen enabled the preview may not perfectly match the result, and generation may take longer."), ECEPreferences.AutoSkinnedForce256Triangles); + } + } + + ECEPreferences.AutoSkinnedPerBoneSettings = EditorGUILayout.Foldout(ECEPreferences.AutoSkinnedPerBoneSettings, "Per Bone Settings"); + if (ECEPreferences.AutoSkinnedPerBoneSettings) + { + EditorGUILayout.BeginHorizontal(); + ECEPreferences.AutoSkinnedIndents = ECUI.ToggleLeft(new GUIContent("Indents", "Enables or disables the indenting of the bones in the UI."), ECEPreferences.AutoSkinnedIndents); + ECEPreferences.AutoSkinnedPairing = ECUI.ToggleLeft(new GUIContent("Pairing", "When enabled, bones that have been identified as pairs (Bone's with same lenght transform-chains Ie. arms/legs) only display one of the chains, but modifications to the values apply to both."), ECEPreferences.AutoSkinnedPairing); + EditorGUILayout.EndHorizontal(); + foreach (EasyColliderAutoSkinnedBone b in ECAutoSkinned.SortedBoneList) + { + if (!b.IsValid) continue; + if (ECEPreferences.AutoSkinnedPairing && b.IsPaired && !b.IsPairDisplayBone) continue; + + EditorGUILayout.BeginHorizontal(); + SetLabelWidth(0, true); + if (ECEPreferences.AutoSkinnedIndents) + { + // Indents and vertical lines for visibility of hierarchy. + for (int i = 0; i < b.IndentLevel; i++) + { + ECUI.VerticalLine(Color.gray, 1f, 6f); + ECUI.LabelEmptyNoStretch(); + } + } + EditorGUI.BeginChangeCheck(); + bool enabled = ECUI.ToggleLeft(new GUIContent(b.BoneName), b.Enabled); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(ECAutoSkinned, "Change Auto Skinned Parameters"); + int group = Undo.GetCurrentGroup(); + ECAutoSkinned.ChangeBoneEnabled(b, enabled, Event.current.modifiers == EventModifiers.Control); + Undo.CollapseUndoOperations(group); + } + EditorGUI.BeginChangeCheck(); + SKINNED_MESH_COLLIDER_TYPE colliderType = (SKINNED_MESH_COLLIDER_TYPE)ECUI.EnumPopup(GUIContent.none, b.ColliderType, 1); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(ECAutoSkinned, "Change Auto Skinned Parameters"); + int group = Undo.GetCurrentGroup(); + ECAutoSkinned.ChangeBoneColliderType(b, colliderType, Event.current.modifiers == EventModifiers.Control); + Undo.CollapseUndoOperations(group); + } + SetLabelWidth(45f, false); + EditorGUI.BeginChangeCheck(); + float vertexWeight = EditorGUILayout.Slider(new GUIContent("Weight", "Minimum weight of a vertex on a bone to be included in the calculation for that bone's collider."), b.BoneWeight, 0, 1); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(ECAutoSkinned, "Change Auto Skinned Parameters"); + int group = Undo.GetCurrentGroup(); + ECAutoSkinned.ChangeBoneWeight(b, vertexWeight, Event.current.modifiers == EventModifiers.Control); + Undo.CollapseUndoOperations(group); + } + + RestoreLabelWidth(); + + EditorGUILayout.EndHorizontal(); + // extra debug area + // EditorGUILayout.BeginHorizontal(); + // EditorGUILayout.LabelField("Index:" + b.BoneIndex + " Children:" + b.ChildIndexs.Count); + // EditorGUILayout.EndHorizontal(); + } + } + + if (EditorGUI.EndChangeCheck() || ECAutoSkinned.HasSkinnedMeshRendererTransformed()) + { + ECPreviewer.ClearPreview(); + ECPreviewer.AutoSkinnedData = ECAutoSkinned.CalculateSkinnedMeshPreview(ECAutoSkinned.renderer, ECEPreferences.AutoSkinnedColliderType, ECEditor.GetProperties(), ECEPreferences.AutoSkinnedMinBoneWeight, ECEPreferences.AutoSkinnedAllowRealign, ECEPreferences.AutoSkinnedMinRealignAngle); + UpdatePreview(true); + } + + + if (ECUI.DisableableButton("Generate Colliders on Skinned Mesh", "Creates colliders along the chain of a skinned mesh collider's bones.", "No skinned mesh found on the selected gameobject or it's children", ECEditor.HasSkinnedMeshRenderer || ECAutoSkinned.renderer != null)) + { + // see if we have one skinned mesh renderer + if (ECAutoSkinned.renderer != null) + { + //because this shouldn't be used, but the methods used are the same so it will get them from there anyway. +#if(UNITY_EDITOR) + ECEPreferences.ShrinkGrow = 1f; +#endif + + Undo.RegisterCompleteObjectUndo(ECAutoSkinned.renderer.gameObject, "Generate Colliders on Skinned Mesh"); + int group = Undo.GetCurrentGroup(); + string savePath = ""; + if (ECEPreferences.AutoSkinnedColliderType == SKINNED_MESH_COLLIDER_TYPE.Convex_Mesh) + { + if (ECEPreferences.SaveConvexHullAsAsset) + { + savePath = EasyColliderSaving.GetValidConvexHullPath(ECEditor.SelectedGameObject); + } + EditorUtility.DisplayProgressBar("Creating Convex Hulls on Skinned Mesh", "Generating Convex Hulls..", 0.5f); + } + List generatedColliders = ECAutoSkinned.GenerateSkinnedMeshColliders(ECAutoSkinned.renderer, ECEPreferences.AutoSkinnedColliderType, ECEditor.GetProperties(), ECEPreferences.AutoSkinnedMinBoneWeight, ECEPreferences.AutoSkinnedAllowRealign, ECEPreferences.AutoSkinnedMinRealignAngle, savePath); + if (ECEPreferences.AutoSkinnedColliderType == SKINNED_MESH_COLLIDER_TYPE.Convex_Mesh) + { + EditorUtility.ClearProgressBar(); + } + Undo.RecordObject(ECEditor, "Generate Colliders on Skinned Mesh"); + foreach (Collider c in generatedColliders) + { + ECEditor.DisableCreatedCollider(c); + } + Undo.CollapseUndoOperations(group); + } + } + } + + /// + /// Draws the preview toggle and the dropdown beside it. + /// + private void DrawPreviewAndDropDown() + { + // Preview UI + EditorGUILayout.BeginHorizontal(); + EditorGUI.BeginChangeCheck(); + ECEPreferences.PreviewEnabled = ECUI.DisableableToggleLeft("Draw Preview:", "When enabled, draws a preview of the collider that would be created from the selected points.", "", true, ECEPreferences.PreviewEnabled); + if (EditorGUI.EndChangeCheck()) + { + if (ECEPreferences.PreviewEnabled) + { + UpdatePreview(); + } + SceneView.RepaintAll(); + FocusSceneView(); + } + EditorGUI.BeginChangeCheck(); + ECEPreferences.PreviewColliderType = (CREATE_COLLIDER_TYPE)ECUI.EnumPopup(GUIContent.none, ECEPreferences.PreviewColliderType, 75f); + if (EditorGUI.EndChangeCheck()) + { + UpdatePreview(); + SceneView.RepaintAll(); + FocusSceneView(); + } + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + } + + /// + /// Checks if the current offset parameters create enough vertices to get over the vertex limit. + /// + /// + /// + private bool OffsetColliderVertexCount(int regularCount) + { + // only both matters. + if (ECEPreferences.VertexNormalOffsetType == NORMAL_OFFSET.Both) + { + if ((ECEPreferences.VertexNormalOffset != 0 || ECEPreferences.VertexNormalInset != 0) && ECEditor.SelectedVertices.Count * 2 >= regularCount) + { + return true; + } + } + return (ECEditor.SelectedVertices.Count >= regularCount); + } + + public bool HasMinimumVerticesForCollider(CREATE_COLLIDER_TYPE colliderType) + { + if (colliderType == CREATE_COLLIDER_TYPE.BOX) { return ECEditor.SelectedVertices.Count >= 2 || OffsetColliderVertexCount(2); } + if (colliderType == CREATE_COLLIDER_TYPE.ROTATED_BOX) { return ECEditor.SelectedVertices.Count >= 3 || OffsetColliderVertexCount(3); } + if (colliderType == CREATE_COLLIDER_TYPE.SPHERE) { return ECEditor.SelectedVertices.Count >= 2 || OffsetColliderVertexCount(2); } + if (colliderType == CREATE_COLLIDER_TYPE.CAPSULE) + { + return ECEPreferences.CapsuleColliderMethod == CAPSULE_COLLIDER_METHOD.BestFit ? (ECEditor.SelectedVertices.Count >= 3 || OffsetColliderVertexCount(3)) : + (ECEditor.SelectedVertices.Count >= 2 || OffsetColliderVertexCount(2)); + } + if (colliderType == CREATE_COLLIDER_TYPE.ROTATED_CAPSULE) { return ECEditor.SelectedVertices.Count >= 3 || OffsetColliderVertexCount(3); } + if (colliderType == CREATE_COLLIDER_TYPE.CONVEX_MESH) { return (ECEditor.SelectedVertices.Count >= 4) || OffsetColliderVertexCount(4); } + if (colliderType == CREATE_COLLIDER_TYPE.CYLINDER) { return ECEditor.SelectedVertices.Count >= 2 || OffsetColliderVertexCount(2); } + return false; + } + + /// + /// Draws the collider creation tools UI. (preview, buttons, methods) + /// + private void DrawColliderCreationTools() + { + ECUI.LabelBold("Collider Creation"); + // Preview UI + DrawPreviewAndDropDown(); + + EditorGUILayout.BeginHorizontal(); + EditorGUI.BeginChangeCheck(); + GUILayout.FlexibleSpace(); + if (ECUI.DisableableIconButtonShortcutCreation( + "Creates a box collider from the selected points.", + "At least 2 points must be selected to create a box collider.", 0, + HasMinimumVerticesForCollider(CREATE_COLLIDER_TYPE.BOX), + KeyCode.Keypad1, ECEPreferences.PreviewColliderType == CREATE_COLLIDER_TYPE.BOX)) + { + CreateCollider(CREATE_COLLIDER_TYPE.BOX, "Create Box Collider"); + } + if (ECUI.DisableableIconButtonShortcutCreation( + "Creates a rotated box collider from the selected points.", + "At least 3 points must be selected to create a rotated box collider.", 1, + HasMinimumVerticesForCollider(CREATE_COLLIDER_TYPE.ROTATED_BOX), + KeyCode.Keypad2, ECEPreferences.PreviewColliderType == CREATE_COLLIDER_TYPE.ROTATED_BOX)) + { + CreateCollider(CREATE_COLLIDER_TYPE.ROTATED_BOX, "Create Rotated Box Collider"); + } + if (ECUI.DisableableIconButtonShortcutCreation( + "Creates a sphere collider from the selected points using the Sphere Method selected.", + "At least 2 points must be selected to create a sphere collider.", 2, + HasMinimumVerticesForCollider(CREATE_COLLIDER_TYPE.SPHERE), + KeyCode.Keypad3, ECEPreferences.PreviewColliderType == CREATE_COLLIDER_TYPE.SPHERE)) + { + CreateCollider(CREATE_COLLIDER_TYPE.SPHERE, "Create Sphere Collider"); + } + // capsule button + if (ECUI.DisableableIconButtonShortcutCreation( + "Creates a capsule collider from the points selected using the Capsule Method selected.", + ECEPreferences.CapsuleColliderMethod == CAPSULE_COLLIDER_METHOD.BestFit ? + "At least 3 points must be selected to use the Best Fit Capsule Method." : + "At least 2 points must be selected to use the Min Max Capsule Method.", 3, + HasMinimumVerticesForCollider(CREATE_COLLIDER_TYPE.CAPSULE), + KeyCode.Keypad4, ECEPreferences.PreviewColliderType == CREATE_COLLIDER_TYPE.CAPSULE + )) + { + CreateCollider(CREATE_COLLIDER_TYPE.CAPSULE, "Create Capsule Collider"); + } + // rotated capsule + if (ECUI.DisableableIconButtonShortcutCreation( + "Creates a rotated capsule collider from the points selected using the Capsule Method selected.", + "At least 3 points must be selected to create a rotated capsule collider.", 4, + HasMinimumVerticesForCollider(CREATE_COLLIDER_TYPE.ROTATED_CAPSULE), + KeyCode.Keypad5, ECEPreferences.PreviewColliderType == CREATE_COLLIDER_TYPE.ROTATED_CAPSULE)) + { + CreateCollider(CREATE_COLLIDER_TYPE.ROTATED_CAPSULE, "Create Rotated Capsule Collider"); + } + // convex mesh + if (ECUI.DisableableIconButtonShortcutCreation("Creates a Convex Mesh Collider from the selected points.", + "At least 4 points must be selected to create a convex hull. Additionally, the 4 points must not lay on the same plane.", 5, + HasMinimumVerticesForCollider(CREATE_COLLIDER_TYPE.CONVEX_MESH), + KeyCode.Keypad6, ECEPreferences.PreviewColliderType == CREATE_COLLIDER_TYPE.CONVEX_MESH)) + { + CreateCollider(CREATE_COLLIDER_TYPE.CONVEX_MESH, "Create Convex Mesh Collider"); + } + // Cylinder + if (ECUI.DisableableIconButtonShortcutCreation("Creates a Cylinder shaped Convex Mesh Collider from the selected points.", + "At least 3 points must be selected to cylinder collider.", 6, + HasMinimumVerticesForCollider(CREATE_COLLIDER_TYPE.CYLINDER), + KeyCode.Keypad7, ECEPreferences.PreviewColliderType == CREATE_COLLIDER_TYPE.CYLINDER)) + { + CreateCollider(CREATE_COLLIDER_TYPE.CYLINDER, "Create Cylinder Collider"); + } + if (EditorGUI.EndChangeCheck()) + { + // reset vertex snap when a button is pressed. + ECEPreferences.VertexSnapMethod = VERTEX_SNAP_METHOD.Both; + } + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + DrawColliderCreationMethods(); + EditorGUI.BeginChangeCheck(); + SetLabelWidth(125f); + ECEPreferences.ColliderHolder = (COLLIDER_HOLDER)EditorGUILayout.EnumPopup(new GUIContent("Collider Holders:", "Default: Only rotated collider holders are created.\nOnce: One empty child gameobject is created to hold all colliders.\nAlways: An empty child gameobject is created to hold each collider."), ECEPreferences.ColliderHolder); + RestoreLabelWidth(); + if (EditorGUI.EndChangeCheck()) + { + FocusSceneView(); + } + } + + + /// + /// Draws the method enums for collider creation. + /// + private void DrawColliderCreationMethods() + { + + + if (CurrentTab != ECE_WINDOW_TAB.Editing) + { + EditorGUILayout.BeginHorizontal(); + + SetLabelWidth(30f); + EditorGUI.BeginChangeCheck(); + NORMAL_OFFSET offsetType = (NORMAL_OFFSET)ECUI.EnumPopup(new GUIContent("Normal Offset:", "Creates extra vertices that are grown out along each selected vertex's averaged normal.\n\nOut: Extrudes in direction of normals.\nIn: Extrudes in opposite direction of normals.\nBoth: Option to extrude along both directions.\n\nDoes not work on vertices that were selected by editing a collider."), ECEPreferences.VertexNormalOffsetType, 100f); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(ECEPreferences, "Change offset type"); + ECEPreferences.VertexNormalOffsetType = offsetType; + UpdatePreview(true); + } + if (ECEPreferences.VertexNormalOffsetType != NORMAL_OFFSET.In) + { + EditorGUI.BeginChangeCheck(); + float offset = EditorGUILayout.FloatField(new GUIContent("Out:", "Offset amount in the direction of the normal"), ECEPreferences.VertexNormalOffset, GUILayout.MinWidth(40f)); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(ECEPreferences, "Change offset out value"); + ECEPreferences.VertexNormalOffset = offset; + UpdatePreview(true); + } + if (ECUI.IconButton("d_Refresh", "Reset value to 0.0f")) + { + Undo.RecordObject(ECEPreferences, "Reset offset out value"); + ECEPreferences.VertexNormalOffset = 0.0f; + UpdatePreview(true); + } + } + if (ECEPreferences.VertexNormalOffsetType != NORMAL_OFFSET.Out) + { + EditorGUI.BeginChangeCheck(); + float inset = EditorGUILayout.FloatField(new GUIContent("In:", "Offset amount opposite of the normal."), ECEPreferences.VertexNormalInset, GUILayout.MinWidth(40f)); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(ECEPreferences, "Change offset in value"); + ECEPreferences.VertexNormalInset = inset; + UpdatePreview(true); + } + if (ECUI.IconButton("d_Refresh", "Reset value to 0.0f")) + { + Undo.RecordObject(ECEPreferences, "Reset offset in value"); + ECEPreferences.VertexNormalInset = 0.0f; + UpdatePreview(true); + } + } + RestoreLabelWidth(); + + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.BeginHorizontal(); + + SetLabelWidth(100f); + EditorGUI.BeginChangeCheck(); + float shrinkGrow = EditorGUILayout.FloatField(new GUIContent("Shrink/Grow:", "Shrink or grow the resulting collider by this value, similar to scaling an object.\n\n Useful for when you want to match the exact shape, but want it to be slightly larger or smaller for gameplay reasons."), ECEPreferences.ShrinkGrow, GUILayout.MinWidth(40f)); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(ECEPreferences, "Change Shrink Grow value"); + ECEPreferences.ShrinkGrow = shrinkGrow; + UpdatePreview(true); + } + if (ECUI.IconButton("d_Refresh", "Reset value to 1.0f")) + { + Undo.RecordObject(ECEPreferences, "Reset shrink grow value to 1"); + ECEPreferences.ShrinkGrow = 1.0f; + UpdatePreview(true); + } + RestoreLabelWidth(); + + EditorGUILayout.EndHorizontal(); + } + + + + // options for each specific collider type go below the general options. + if (ECEPreferences.PreviewColliderType == CREATE_COLLIDER_TYPE.SPHERE) + { + ECEPreferences.SphereColliderMethod = (SPHERE_COLLIDER_METHOD)ECUI.EnumPopup(new GUIContent("Sphere Method:", "Algorithm to use during sphere collider creation."), ECEPreferences.SphereColliderMethod, 100f); + } + else if (ECEPreferences.PreviewColliderType == CREATE_COLLIDER_TYPE.CAPSULE || ECEPreferences.PreviewColliderType == CREATE_COLLIDER_TYPE.ROTATED_CAPSULE) + { + ECEPreferences.CapsuleColliderMethod = (CAPSULE_COLLIDER_METHOD)ECUI.EnumPopup(new GUIContent("Capsule Method:", "Algorithm to use during capsule collider creation."), ECEPreferences.CapsuleColliderMethod, 100f); + } + else if (ECEPreferences.PreviewColliderType == CREATE_COLLIDER_TYPE.CONVEX_MESH) + { + ECEPreferences.MeshColliderMethod = (MESH_COLLIDER_METHOD)ECUI.EnumPopup(new GUIContent("Mesh Method:", "Algorithm to use during convex mesh collider creation."), ECEPreferences.MeshColliderMethod, 100f); + } + else if (ECEPreferences.PreviewColliderType == CREATE_COLLIDER_TYPE.CYLINDER) + { + SetLabelWidth(125f); + EditorGUI.BeginChangeCheck(); + ECEPreferences.CylinderOrientation = (CYLINDER_ORIENTATION)ECUI.EnumPopup(new GUIContent((ECEPreferences.CylinderAsCapsuleOrientation ? "Cylinder & Capsule Orientation" : "Cylinder Orientation:"), "Controls the way cylinders " + (ECEPreferences.CylinderAsCapsuleOrientation ? "and capsules " : "") + "are oriented during creation. \nAutomatic: Uses the largest axis.\nX,Y,Z:Orient Along local X, Y, and Z axis respectively."), ECEPreferences.CylinderOrientation, (ECEPreferences.CylinderAsCapsuleOrientation ? 200f : 125f)); + if (EditorGUI.EndChangeCheck() && !EditorGUIUtility.editingTextField) + { + FocusSceneView(); + } + EditorGUI.BeginChangeCheck(); + ECEPreferences.CylinderNumberOfSides = EditorGUILayout.IntSlider(new GUIContent("Cylinder Sides:", "Number of sides to try and create when making a cylinder shaped collider.\nThis value is not guarunteed to be the number of sides, but it should be in most cases."), ECEPreferences.CylinderNumberOfSides, 3, 64); + ECEPreferences.CylinderRotationOffset = EditorGUILayout.Slider(new GUIContent("Cylinder Offset:", "Offsets the cylinder by the degrees specified."), ECEPreferences.CylinderRotationOffset, 0, 120f); + RestoreLabelWidth(); + if (EditorGUI.EndChangeCheck()) + { + RepaintLastActiveSceneView(); + } + } + } + + + /// + /// Draws the collider removal tools UI: remove selected and remove all button + /// + private void DrawColliderTools() + { + ECUI.LabelBold("Collider Tools"); + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.BeginVertical(); + + + if (ECUI.DisableableButton("Remove Selected", + "Removes the colliders that are currently selected, these colliders are drawn by the color set in the preferences.", + "No collider is currently selected.", + ECEditor.ColliderSelectEnabled && ECEditor.SelectedColliders.Count > 0)) + { + Undo.RegisterCompleteObjectUndo(ECEditor, "Remove Collider"); + int group = Undo.GetCurrentGroup(); + ECEditor.RemoveSelectedColliders(); + Undo.CollapseUndoOperations(group); + FocusSceneView(); + } + + if (ECUI.DisableableButton("Edit", + "Converts the selected colliders to selected vertices and changes to the Creation tab. This removes the colliders.", + "No collider is selected", + ECEditor.SelectedColliders.Count > 0)) + { + StartEditColliders(); + } + + EditorGUILayout.EndVertical(); + EditorGUILayout.BeginVertical(); + + if (ECUI.DisableableButton("Remove All", + "Removes all colliders on the selected gameobject, attach to gameobject, and their children.", + "No gameobject is currently selected.", + ECEditor.SelectedGameObject != null)) + { + Undo.RegisterCompleteObjectUndo(ECEditor, "Remove All Colliders"); + int group = Undo.GetCurrentGroup(); + ECEditor.RemoveAllColliders(); + Undo.CollapseUndoOperations(group); + FocusSceneView(); + } + + if (ECUI.DisableableButton("Select Collider Vertices", + "Selects the vertices of the selected colliders, and changes to the Creation Tab. This keeps the existing colliders.", + "No collider is selected", + ECEditor.SelectedColliders.Count > 0)) + { + StartEditColliders(false); + } + + EditorGUILayout.EndVertical(); + EditorGUILayout.EndHorizontal(); + } + + private void StartEditColliders(bool remove = true) + { + Undo.RegisterCompleteObjectUndo(ECEditor, "Edit Collider"); + int group = Undo.GetCurrentGroup(); + + Collider c = ECEditor.SelectedColliders[0]; + CREATE_COLLIDER_TYPE previousType = CREATE_COLLIDER_TYPE.BOX; + if (c is BoxCollider) + { + previousType = CREATE_COLLIDER_TYPE.BOX; + // using this is still brittle, but I don't want to clutter user's tags, and can't think of a better way to identify rotated colliders. + if (c.gameObject.name.Contains("Rotated")) + { + previousType = CREATE_COLLIDER_TYPE.ROTATED_BOX; + } + } + else if (c is CapsuleCollider) + { + previousType = CREATE_COLLIDER_TYPE.CAPSULE; + if (c.gameObject.name.Contains("Rotated")) + { + previousType = CREATE_COLLIDER_TYPE.ROTATED_CAPSULE; + } + } + else if (c is MeshCollider) + { + previousType = CREATE_COLLIDER_TYPE.CONVEX_MESH; + } + else if (c is SphereCollider) + { + previousType = CREATE_COLLIDER_TYPE.SPHERE; + } + Undo.RecordObject(ECEPreferences, "Edit Collider"); + ECEPreferences.PreviewColliderType = previousType; + + ECEditor.EditColliders(ECEditor.SelectedColliders, remove); + Undo.RecordObject(this, "Edit Collider"); + CurrentTab = ECE_WINDOW_TAB.Creation; + Undo.RecordObject(ECEPreferences, "Edit Collider"); + ECEPreferences.CurrentWindowTab = CurrentTab; + Undo.CollapseUndoOperations(group); + FocusSceneView(); + } + + /// + /// Draws the collection selection tools: clear and invert. + /// + private void DrawColliderSelectionTools() + { + ECUI.LabelBold("Collider Selection Tools"); + EditorGUILayout.BeginHorizontal(); + if (ECUI.DisableableButton("Clear", "Deselects all currently selected colliders", "No colliders are selected.", (ECEditor.SelectedGameObject != null && ECEditor.SelectedColliders.Count > 0))) + { + Undo.RegisterCompleteObjectUndo(ECEditor, "Clear selected colliders"); + int group = Undo.GetCurrentGroup(); + ECEditor.DeselectAllColliders(); + Undo.CollapseUndoOperations(group); + FocusSceneView(); + } + if (ECUI.DisableableButton("Invert", "Deselects all currently selected colliders, and selects all unselected colliders.", "No gameobject is currently selected.", (ECEditor.SelectedGameObject != null))) + { + Undo.RegisterCompleteObjectUndo(ECEditor, "Invert selected colliders"); + int group = Undo.GetCurrentGroup(); + ECEditor.InvertSelection(); + Undo.CollapseUndoOperations(group); + FocusSceneView(); + } + EditorGUILayout.EndHorizontal(); + } + + /// + /// Draws the collider merge tools. (preview, buttons, methods) + /// + private void DrawColliderMergeTools() + { + ECUI.LabelBold("Merge Tools"); + // hide option for now. + // ECEPreferences.MergeCollidersRoundnessAccuracy = EditorGUILayout.IntField("Accuracy:", ECEPreferences.MergeCollidersRoundnessAccuracy); + DrawPreviewAndDropDown(); + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + if (ECUI.DisableableIconButtonShortcutMerge( + "Merges the selected colliders into a single box collider.", + "At least 1 collider must be selected.", 0, + ECEditor.SelectedColliders.Count >= 1, + KeyCode.Keypad1, ECEPreferences.PreviewColliderType == CREATE_COLLIDER_TYPE.BOX)) + { + MergeColliders(CREATE_COLLIDER_TYPE.BOX, "Merge to Box Collider"); + } + if (ECUI.DisableableIconButtonShortcutMerge( + "Merges the selected colliders into a single rotated box collider.\nCollider is rotated based on the first selected colliders transform.", + "At least 1 collider must be selected.", 1, + ECEditor.SelectedColliders.Count >= 1, + KeyCode.Keypad2, ECEPreferences.PreviewColliderType == CREATE_COLLIDER_TYPE.ROTATED_BOX)) + { + MergeColliders(CREATE_COLLIDER_TYPE.ROTATED_BOX, "Merge to Rotated Box Collider"); + } + if (ECUI.DisableableIconButtonShortcutMerge( + "Merges the selected colliders into a single sphere collider.", + "At least 1 collider must be selected.", 2, + ECEditor.SelectedColliders.Count >= 1, + KeyCode.Keypad3, ECEPreferences.PreviewColliderType == CREATE_COLLIDER_TYPE.SPHERE)) + { + MergeColliders(CREATE_COLLIDER_TYPE.SPHERE, "Merge to Sphere Collider"); + } + // capsule button + if (ECUI.DisableableIconButtonShortcutMerge( + "Merges the selected colliders into a single capsule collider.", + "At least 1 collider must be selected.", 3, + ECEditor.SelectedColliders.Count >= 1, + KeyCode.Keypad4, ECEPreferences.PreviewColliderType == CREATE_COLLIDER_TYPE.CAPSULE + )) + { + MergeColliders(CREATE_COLLIDER_TYPE.CAPSULE, "Merge to Capsule Collider"); + } + if (ECUI.DisableableIconButtonShortcutMerge( + "Merges the selected colliders into a single rotated capsule collider.\nCollider is rotated based on the first selected colliders transform.", + "At least 1 collider must be selected.", 4, + ECEditor.SelectedColliders.Count >= 1, + KeyCode.Keypad5, ECEPreferences.PreviewColliderType == CREATE_COLLIDER_TYPE.ROTATED_CAPSULE + )) + { + MergeColliders(CREATE_COLLIDER_TYPE.ROTATED_CAPSULE, "Merge to Capsule Collider"); + } + // convex mesh + if (ECUI.DisableableIconButtonShortcutMerge("Merges the selected colliders into a single convex mesh collider.", + "At least 1 collider must be selected.", 5, + ECEditor.SelectedColliders.Count >= 1, + KeyCode.Keypad6, ECEPreferences.PreviewColliderType == CREATE_COLLIDER_TYPE.CONVEX_MESH)) + { + MergeColliders(CREATE_COLLIDER_TYPE.CONVEX_MESH, "Merge to Cylinder Collider"); + } + if (ECUI.DisableableIconButtonShortcutMerge("Merges the selected colliders into a cylinder shaped convex mesh collider.", + "At least 1 collider must be selected.", 6, + ECEditor.SelectedColliders.Count >= 1, + KeyCode.Keypad7, ECEPreferences.PreviewColliderType == CREATE_COLLIDER_TYPE.CYLINDER)) + { + MergeColliders(CREATE_COLLIDER_TYPE.CYLINDER, "Merge to Cylinder Convex Mesh Collider"); + } + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + EditorGUI.BeginChangeCheck(); + ECEPreferences.RemoveMergedColliders = EditorGUILayout.ToggleLeft(new GUIContent("Remove Merged Colliders", "When enabled, colliders that are merged together are removed."), ECEPreferences.RemoveMergedColliders); + if (EditorGUI.EndChangeCheck()) + { + FocusSceneView(); + } + DrawColliderCreationMethods(); + } + + /// + /// Draws the settings UI for setting collders that are common to all colliders created. + /// + private void DrawCreatedColliderSettings() + { + _ShowColliderSettings = EditorGUILayout.Foldout(_ShowColliderSettings, "Created Collider Settings"); + if (_ShowColliderSettings) + { + EditorGUILayout.BeginHorizontal(); + // create as trigger. + EditorGUI.BeginChangeCheck(); + bool createAsTrigger = ECUI.ToggleLeft(new GUIContent("Create as Trigger", "Creates the colliders as triggers"), ECEditor.IsTrigger); + // bool createAsTrigger = EditorGUILayout.ToggleLeft(new GUIContent("Create as Trigger", "Creates the colliders as triggers"), ECEditor.IsTrigger); + if (EditorGUI.EndChangeCheck()) + { + Undo.RegisterCompleteObjectUndo(ECEditor, "Toggle Create As Trigger"); + ECEditor.IsTrigger = createAsTrigger; + FocusSceneView(); + } + + // Physic material + EditorGUI.BeginChangeCheck(); + SetLabelWidth(100f); + // PhysicMaterial renamed to PhysicsMaterial +#if (UNITY_6000_0_OR_NEWER) + PhysicsMaterial physicMaterial = (PhysicsMaterial)EditorGUILayout.ObjectField(new GUIContent("Physic Material:", "PhysicMaterial to set on collider upon creation."), ECEditor.PhysicMaterial, typeof(PhysicsMaterial), false); +#else + + PhysicMaterial physicMaterial = (PhysicMaterial)EditorGUILayout.ObjectField(new GUIContent("Physic Material:", "PhysicMaterial to set on collider upon creation."), ECEditor.PhysicMaterial, typeof(PhysicMaterial), false); +#endif + RestoreLabelWidth(); + if (EditorGUI.EndChangeCheck()) + { + Undo.RegisterCompleteObjectUndo(ECEditor, "Set PhysicMaterial"); + ECEditor.PhysicMaterial = physicMaterial; + FocusSceneView(); + } + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.BeginHorizontal(); +#if (UNITY_2022_2_OR_NEWER) + EditorGUI.BeginChangeCheck(); + bool providesContacts = ECUI.ToggleLeft(new GUIContent("Provides Contacts"), ECEditor.ProvidesContacts); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(ECEditor, "Change provides contacts"); + ECEditor.ProvidesContacts = providesContacts; + } +#endif + // Rotated Collider Layer + if (!ECEPreferences.CopyParentObjectLayer) + { + EditorGUI.BeginChangeCheck(); + int gameobjectLayerOverride = EditorGUILayout.LayerField(new GUIContent("GameObject Layer:", "The layer to set on any created gameobjects that hold colliders."), ECEditor.GameObjectLayerOverride); + if (EditorGUI.EndChangeCheck()) + { + ECEditor.GameObjectLayerOverride = gameobjectLayerOverride; + FocusSceneView(); + } + } + EditorGUILayout.EndHorizontal(); +#if (UNITY_2022_2_OR_NEWER) + _ShowColliderOverrideSettings = EditorGUILayout.Foldout(_ShowColliderOverrideSettings, "Layer Overrides"); + if (_ShowColliderOverrideSettings) + { + ECEditor.LayerOverridePriority = EditorGUILayout.IntField(new GUIContent("Layer Override Priority"), ECEditor.LayerOverridePriority); + EditorGUI.BeginChangeCheck(); + LayerMask tempIncludeMask = EditorGUILayout.MaskField(new GUIContent("Include Layers"), InternalEditorUtility.LayerMaskToConcatenatedLayersMask(ECEditor.IncludeLayers), InternalEditorUtility.layers); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(ECEditor, "Change include layers override"); + ECEditor.IncludeLayers = InternalEditorUtility.ConcatenatedLayersMaskToLayerMask(tempIncludeMask); + } + EditorGUI.BeginChangeCheck(); + LayerMask tempExcludeMask = EditorGUILayout.MaskField(new GUIContent("Exclude Layers"), InternalEditorUtility.LayerMaskToConcatenatedLayersMask(ECEditor.ExcludeLayers), InternalEditorUtility.layers); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(ECEditor, "Change include layers override"); + ECEditor.ExcludeLayers = InternalEditorUtility.ConcatenatedLayersMaskToLayerMask(tempExcludeMask); + } + } +#endif + } + } + + /// + /// Draws the finish currently selected gameobject button with vertical space around it. + /// + private void DrawFinishButton() + { + // finish button, space around it + ECUI.VerticalSpace(0.5f); + if (ECUI.DisableableButton("Finish Currently Selected GameObject", "Cleans up the currently selected gameobject and deselects it.", "No GameObject is currently selected.", ECEditor.SelectedGameObject != null)) + { + if (ECEPreferences.PopupDialogOnFinish) + { + string selectedThing = ""; + if (ECEditor.SelectedVertices.Count > 0) + { + if (CurrentTab == ECE_WINDOW_TAB.Creation) + { + selectedThing = "vertices"; + } + if (CurrentTab == ECE_WINDOW_TAB.VHACD && ECEPreferences.VHACDParameters.UseSelectedVertices) + { + selectedThing = "vertices"; + } + } + if (ECEditor.SelectedColliders.Count > 0 && CurrentTab == ECE_WINDOW_TAB.Editing) + { + selectedThing = "colliders"; + } + + if (selectedThing != "") + { + if (EditorUtility.DisplayDialog("Are you sure?", "Are you sure you want to finish the currently selected gameobject?\n\nYou still have " + selectedThing + " selected.", "Yes", "Cancel")) + { + ChangeToNewObject(null); + } + } + else + { + ChangeToNewObject(null); + } + } + else + { + ChangeToNewObject(null); + } + FocusSceneView(true); + } + ECUI.VerticalSpace(0.5f); + } + + /// + /// Changes the selected object field to a new object, with undo support and making sure it doesn't break any other components + /// that don't interact directly with the EasyColliderEditor class + /// + /// + private void ChangeToNewObject(GameObject selected) + { + // NOTE that using a name and registering a group and collapsing doesn't actually work, undo's will only display the last undo name + // when when adding components is add component. + // I will leave the code in places where undo's should be grouped and named correctly in the hopes that one day it works as expected + // even though this bug is listed as will not fix in unity's bug reports + string undoString = selected == null ? "Finish Currently Selected Gameobject" : "Change Selected Object"; + Undo.RegisterCompleteObjectUndo(ECEditor, undoString); + int group = Undo.GetCurrentGroup(); + if (ECEditor.SelectedGameObject != selected) + { + ECPreviewer.ClearPreview(); + } + ECEditor.SelectedGameObject = selected; + ECEPreferences.VertexSnapMethod = VERTEX_SNAP_METHOD.Both; + Undo.RegisterCompleteObjectUndo(ECEPreferences, undoString); + ECEPreferences.rotatedDupeSettings.pivot = selected; + if (ECAutoSkinned != null) + { + Undo.RegisterCompleteObjectUndo(ECAutoSkinned, undoString); + ECAutoSkinned.Clean(); + } + ECPreviewer.ClearPreview(); + Undo.CollapseUndoOperations(group); + + + // other operations when changing the selected gameobject, generally to fix various bugs. + // reset vertex snaps. + ECEPreferences.VertexSnapMethod = VERTEX_SNAP_METHOD.Both; + // automatically select the gameobject in the heirarchy so collider gizmos etc are drawn. + if (selected != null) + { + Selection.activeGameObject = selected; + // Reenable vertex or collider selection if that tab is currently open. + if (CurrentTab == ECE_WINDOW_TAB.Creation && ECEditor.VertexSelectEnabled == false) + { + ECEditor.VertexSelectEnabled = true; + ECEditor.ColliderSelectEnabled = false; + } + else if (CurrentTab == ECE_WINDOW_TAB.Editing && ECEditor.ColliderSelectEnabled == false) + { + ECEditor.ColliderSelectEnabled = true; + ECEditor.VertexSelectEnabled = false; + } + else if (CurrentTab == ECE_WINDOW_TAB.None) + { + CurrentTab = ECE_WINDOW_TAB.Creation; + ECEditor.VertexSelectEnabled = true; + } + // automatically focus the window. + FocusSceneView(); + } + // clean up VHACD if needed. + if (_VHACDIsComputing) + { + _VHACDCurrentStep = 5; + CheckVHACDProgress(); + } + // clear the VHACD preview if the selected gameobject changes. + VHACDPreviewResult = null; + // tell the preview it needs updating once something is reselected. + SetVHACDNeedsUpdate(); + } + + + + /// + /// Draws the Preferences UI. + /// + private void DrawPreferences() + { + _EditPreferences = EditorGUILayout.Foldout(_EditPreferences, new GUIContent("Edit Preferences", "Allows you to edit preferences for various settings.")); + if (_EditPreferences) + { + + #region keys and input + + ECUI.LabelBold("Input", "To change keys, press the button, then press a key to change.\nModifier keys (like alt, ctrl, space, shift, etc.) can not be used."); + EditorGUILayout.BeginHorizontal(); + ECEPreferences.UseMouseClickSelection = ECUI.ToggleLeft(new GUIContent("Mouse Click Selection", + "When enabled the mouse can be used to select and deselect vertices along with the usual keys.\nLeft click will select vertices.\nRight click will select points."), ECEPreferences.UseMouseClickSelection); + + ECEPreferences.EnableVertexToolsShortcuts = ECUI.ToggleLeft(new GUIContent("Vertex Tools Shortcuts", "Enables the vertex tools shortcut keys set in the Shortcut keys foldout."), ECEPreferences.EnableVertexToolsShortcuts); + EditorGUILayout.EndHorizontal(); + _ShowShortcutsFoldout = EditorGUILayout.Foldout(_ShowShortcutsFoldout, "Shortcut Keys"); + if (_ShowShortcutsFoldout) + { + float width = this.position.width; + + keysChanging[0] = ECUI.ChangeButtonKeyCodeUndoable(ECEPreferences, "Select:", "Key used to select vertices on the mesh.\nKey also used to select colliders.", ref ECEPreferences.VertSelectKeyCode, ref keysChanging[0], width, true); + keysChanging[1] = ECUI.ChangeButtonKeyCodeUndoable(ECEPreferences, "Select Point:", "Key used to select points on the mesh that aren't vertices.", ref ECEPreferences.PointSelectKeyCode, ref keysChanging[1], width, true); + keysChanging[2] = ECUI.ChangeButtonKeyCodeUndoable(ECEPreferences, "Snap Add:", "Key held while box selecting to only add vertices. Key held to vertex snap to only selectable vertices.\nIn addition to this key, CTRL can be held for the same functionality.", ref ECEPreferences.BoxSelectPlusKey, ref keysChanging[2], width, true); + keysChanging[3] = ECUI.ChangeButtonKeyCodeUndoable(ECEPreferences, "Snap Remove:", "Key held while box selecting to only remove points. Key held to vertex snap to only removeable vertices.\nIn addition to this key, ALT can be held for the same functionality.", ref ECEPreferences.BoxSelectMinusKey, ref keysChanging[3], width, true); + + keysChanging[4] = ECUI.ChangeButtonKeyCodeUndoable(ECEPreferences, "Create Collider:", "Key used to create a collider that matches the preview.", ref ECEPreferences.CreateFromPreviewKey, ref keysChanging[4], width, true); + + ECUI.LabelBold("Vertex Selection Tools Shortcuts"); + //clear, grow, growlast, invert, ring + keysChanging[5] = ECUI.ChangeButtonKeyCodeUndoable(ECEPreferences, "Clear:", "Vertex Selection Tools Clear shortcut key.", ref ECEPreferences.ShortcutClear, ref keysChanging[5], width, true); + keysChanging[6] = ECUI.ChangeButtonKeyCodeUndoable(ECEPreferences, "Invert:", "Vertex Selection Tools Invert shortcut key.", ref ECEPreferences.ShortcutInvert, ref keysChanging[6], width, true); + keysChanging[7] = ECUI.ChangeButtonKeyCodeUndoable(ECEPreferences, "Grow:", "Vertex Selection Tools Grow shortcut key.", ref ECEPreferences.ShortcutGrow, ref keysChanging[7], width, true); + keysChanging[8] = ECUI.ChangeButtonKeyCodeUndoable(ECEPreferences, "Grow Last:", "Selection Tools grow last shortcut key.", ref ECEPreferences.ShortcutGrowLast, ref keysChanging[8], width, true); + keysChanging[9] = ECUI.ChangeButtonKeyCodeUndoable(ECEPreferences, "Ring:", "Vertex Selection Tools Ring shortcut key.", ref ECEPreferences.ShortcutRing, ref keysChanging[9], width, true); + + + foreach (var item in keysChanging) + { + if (item) + { + if (ECEditor.VertexSelectEnabled) { ECEditor.VertexSelectEnabled = false; } + this.Focus(); + } + } + + } + + + #endregion + + ECUI.HorizontalLineLight(); + + #region Convex Hull Preferences + + // Convex hull saving stuff + ECUI.LabelBold("Convex Hulls"); + EditorGUILayout.BeginHorizontal(); + ECUI.ToggleLeftUndoable(ECEPreferences, new GUIContent("Save as Asset", "When true, saves colliders created from VHACD and Convex Mesh Colliders as .asset files."), "Toggle Save Convex Hulls as Assets", ref ECEPreferences.SaveConvexHullAsAsset); + // ECEPreferences.SaveConvexHullMeshAtSelected = ECUI.DisableableToggleLeft("Save at Selected's Path", "Saves the convex hull mesh at the selected gameobject's path if possible.", "Save Convex Hulls as Assets must be enabled", ECEPreferences.SaveConvexHullAsAsset, ECEPreferences.SaveConvexHullMeshAtSelected); + ECEPreferences.ConvexHullSaveMethod = (CONVEX_HULL_SAVE_METHOD)ECUI.EnumPopup(new GUIContent("Save Method", "Different options provide different orders in trying to find a location to save the mesh asset. If the first search method fails, it goes to the next search. All have a fallback location of the folder as well.\n\nPrefab: Saves at the prefab's location.\nMesh: Saves at the mesh's location.\nPrefab Mesh: Saves at the prefab's location, if not found saves the mesh's location.\nMesh Prefab: Saves at the mesh's location, if not found saves at the prefab's location.\nFolder: Always saves at the folder specified."), ECEPreferences.ConvexHullSaveMethod, 80); + EditorGUILayout.EndHorizontal(); + // Save folder selection + GUILayout.BeginHorizontal(); + GUILayout.Label("Save Convex Hull Path:"); + if (ECUI.DisableableButton( + ECEPreferences.SaveConvexHullPath, //whatever just show the whole path I guess. + //(ECEPreferences.SaveConvexHullPath.Length > 23 ? "..." + ECEPreferences.SaveConvexHullPath.Substring(ECEPreferences.SaveConvexHullPath.Length - 22, 22) : ECEPreferences.SaveConvexHullPath), + "Location to save the convex hull if Save Convex Hull at Selected GameObject is disabled, or that method fails.", "Save Convex Hulls as Assets must be enabled", ECEPreferences.SaveConvexHullAsAsset)) + { + string path = EditorUtility.OpenFolderPanel("Select folder to store convex hull meshes", "Assets", ""); + if (path != "" && path != null) + { + if (path.Contains(Application.dataPath)) + { + path = path.Replace(Application.dataPath, "Assets"); + // docs say this should mark it as dirty, and save, but the change does not correctly persist across opening and closing in this case. + Undo.RegisterCompleteObjectUndo(ECEPreferences, "Change convex hull save path"); + int group = Undo.GetCurrentGroup(); + ECEPreferences.SaveConvexHullPath = path + "/"; + //manually marking dirty fixes bug where changing the path does not correctly persist across unity editor close/open. + EditorUtility.SetDirty(ECEPreferences); + Undo.CollapseUndoOperations(group); + // focus so we can immediately undo. + this.Focus(); + } + else + { + Debug.LogWarning("Easy Collider Editor: Save path must be located under this projects Assets/ folder."); + } + } + } + + GUILayout.EndHorizontal(); + EditorGUI.BeginChangeCheck(); + string suffix = EditorGUILayout.TextField(new GUIContent("Saved Convex Hull Suffix:", "Suffix to append to end of gameobject's name when saving convex hulls. ie: _ConvexHull_ produces ObjectName_ConvexHull_1 etc.\nCan only contain A-Z, a-z, 1-9, -, and _"), ECEPreferences.SaveConvexHullSuffix); + if (EditorGUI.EndChangeCheck()) + { + // make sure its only letters a-z, A-Z, 1-9, _ and - + if (Regex.IsMatch(suffix, @"^[a-zA-Z1-9_-]+$")) + { + Undo.RecordObject(ECEPreferences, "Change Preferences Suffix"); + ECEPreferences.SaveConvexHullSuffix = suffix; + } + else + { + // otherwise reclicking into the box has the same previous illegal value. + suffix = ECEPreferences.SaveConvexHullSuffix; + } + } + + // sub folder to save meshes in when not usign the "Folder" save method. + EditorGUI.BeginChangeCheck(); + string subFolder = EditorGUILayout.TextField(new GUIContent("Subfolder:", "Subfolder to save generated convex meshes. Ie. If a mesh or prefab is in an Envrionments/Meshes folder, it would be saved in Enviroments/Meshes/SubFolder. Does not apply when using Folder saving method. \nCan only contain A-Z, a-z, 1-9, -, and _"), ECEPreferences.SaveConvexHullSubFolder); + if (EditorGUI.EndChangeCheck()) + { + // make sure its only letters a-z, A-Z, 1-9, _ and - + // null or empty to fix issue where you can't remove subfolder. + if (Regex.IsMatch(subFolder, @"^[a-zA-Z1-9_-]+$") || string.IsNullOrEmpty(subFolder)) + { + Undo.RecordObject(ECEPreferences, "Change Preferences Suffix"); + ECEPreferences.SaveConvexHullSubFolder = subFolder; + } + else + { + //otherwise reclicking into the box has the same previous illegal value? + subFolder = ECEPreferences.SaveConvexHullSubFolder; + } + } + EditorGUILayout.BeginHorizontal(); + ECUI.ToggleLeftUndoable(ECEPreferences, new GUIContent("Read/Write enabled", "When a mesh used for a convex mesh collider is created, this setting specifies if it should be marked as read/write enabled. For limitations caused by disabling read/write on meshes, see unity documentation on mesh colliders."), "Toggle ReadWrite Enabled", ref ECEPreferences.ConvexMeshReadWriteEnabled, 50f); + ECUI.ToggleLeftUndoable(ECEPreferences, new GUIContent("VHACD output to single asset", "When using VHACD, all convex mesh colliders generated in a single use of the VHACD - Generate Convex Mesh Colliders button are combined into a single asset file."), "Toggle Combine VHACD into asset", ref ECEPreferences.CombinedVHACDColliders, 25f); + EditorGUILayout.EndHorizontal(); + ECUI.ToggleLeftUndoable(ECEPreferences, new GUIContent("Allow saving in packages", "Allows saving convex mesh colliders in packages folders. This is an advanced usage as the files are then not project specific and won't be shared with team members unless specifically set up in your team environment."), "Toggle save in packages", ref ECEPreferences.AllowSavingConvexHullsInPackages, 25f); + if (ECEPreferences.AllowSavingConvexHullsInPackages) + { + ECUI.LabelIcon("Project must be specifically set up to share packages for the mesh assets created to also be shared when sharing the project.", "console.warnicon.sml"); + } + #endregion + + ECUI.HorizontalLineLight(); + + #region Preferences - Colors + + EditorGUI.BeginChangeCheck(); + // COLORS ---- + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.BeginVertical(); + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.BeginVertical(); + // label + 2 colors vertical column + // EditorGUILayout.BeginVertical(); + //color labels. + ECUI.LabelBold("Colors"); + ECUI.Label("Selected:", "Color of selected vertices and colliders."); + ECUI.Label("Hovered:", "Color of hovered vertices and colliders."); + EditorGUILayout.EndVertical(); + EditorGUILayout.BeginVertical(); + // color fields + ECUI.Label(""); + ECUI.ColorFieldUndoable(ECEPreferences, "Change selected vertices color", ref ECEPreferences.SelectedVertColour); + ECUI.ColorFieldUndoable(ECEPreferences, "Change hovered vertices color", ref ECEPreferences.HoverVertColour); + EditorGUILayout.EndVertical(); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + EditorGUILayout.BeginVertical(); + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.BeginVertical(); + ECUI.Label("Preview:", "Color of the preview of colliders."); + ECUI.Label("Overlapped:", "Color of overlapped vertices and colliders. Overlapped vertices will be deselected if already selected, and selected again."); + ECUI.Label("Display All:", "Color used when display all vertices is enabled."); + EditorGUILayout.EndVertical(); + EditorGUILayout.BeginVertical(); + ECUI.ColorFieldUndoable(ECEPreferences, "Change collider preview color", ref ECEPreferences.PreviewDrawColor); + ECUI.ColorFieldUndoable(ECEPreferences, "Change overlapped vertices color", ref ECEPreferences.OverlapSelectedVertColour); + ECUI.ColorFieldUndoable(ECEPreferences, "Change display vertices color", ref ECEPreferences.DisplayVerticesColour); + EditorGUILayout.EndVertical(); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + EditorGUILayout.EndHorizontal(); + if (EditorGUI.EndChangeCheck()) + { + RepaintLastActiveSceneView(); + } + #endregion + + ECUI.HorizontalLineLight(); + + #region Misc Preferences + + ECUI.LabelBold("Miscellaneous"); + // over-all changecheck used to check undo's for all prefs. + EditorGUI.BeginChangeCheck(); + // all the toggles. + EditorGUILayout.BeginHorizontal(); + ECUI.FloatFieldUndoable(ECEPreferences, new GUIContent("Vertex Scale:", "Multiplier to all types of displayed points."), "Change common multiplier", ref ECEPreferences.CommonScalingMultiplier, 85); + ECUI.ToggleLeftUndoable(ECEPreferences, new GUIContent("Display Tips", "Disable to stop helpful tips from displaying at the bottom of this window."), "Toggle display tips", ref ECEPreferences.DisplayTips); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.BeginHorizontal(); + _editExtraVertexScales = EditorGUILayout.Foldout(_editExtraVertexScales, "Additional Scale Multipliers"); + EditorGUILayout.EndHorizontal(); + if (_editExtraVertexScales) + { + EditorGUILayout.BeginHorizontal(); + ECUI.FloatFieldUndoable(ECEPreferences, new GUIContent("Selected:", "Multiplier to vertices that are currently selected"), "Change common multiplier", ref ECEPreferences.SelectedScaleMult, 85); + ECUI.FloatFieldUndoable(ECEPreferences, new GUIContent("Overlapped:", "Multiplier to vertices that will be removed"), "Change common multiplier", ref ECEPreferences.OverlapScaleMult, 85); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.BeginHorizontal(); + ECUI.FloatFieldUndoable(ECEPreferences, new GUIContent("Hovered:", "Multiplier to vertices that are currently being hovered"), "Change common multiplier", ref ECEPreferences.HoveredScaleMult, 85); + ECUI.FloatFieldUndoable(ECEPreferences, new GUIContent("Display All:", "Multiplier to vertices shown when display all vertices is enabled"), "Change common multiplier", ref ECEPreferences.DisplayAllScaleMult, 85); + EditorGUILayout.EndHorizontal(); + } + EditorGUILayout.BeginHorizontal(); + ECUI.ToggleLeftUndoable(ECEPreferences, new GUIContent("Copy Parent Object Layer", "When enabled and a child object is created to hold a collider, the layer of that object is the same as it's parent. When disabled lets you choose the layer from a dropdown menu under collider options."), "Toggle rotated on selected layer", ref ECEPreferences.CopyParentObjectLayer); + ECUI.ToggleLeftUndoable(ECEPreferences, new GUIContent("Include Child Skinned Meshes", "Automatically includes skinned meshes when include child meshes is enabled."), "Toggle auto include child skinned meshes", ref ECEPreferences.AutoIncludeChildSkinnedMeshes); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.BeginHorizontal(); + ECUI.ToggleLeftUndoable(ECEPreferences, new GUIContent("Show Selected Vertex Count", "When enabled, displays the number of selected vertices between the Vertex Selection Tools label, and Snaps buttons"), "Toggle Display Selected Vertex Count", ref ECEPreferences.ShowSelectedVertexCount); + ECUI.ToggleLeftUndoable(ECEPreferences, new GUIContent("Finish Confirmation Dialog", "Displays a confirmation dialog when the finish button is pressed, but you still have things selected."), "Toggle Finish Popup", ref ECEPreferences.PopupDialogOnFinish); + EditorGUILayout.EndHorizontal(); + // Raycast delay time. + EditorGUILayout.BeginHorizontal(); + ECUI.ToggleLeftUndoable(ECEPreferences, new GUIContent("Autoselect on Prefab Open", "When enabled and you enter into prefab editing mode (2018.3+) the root object is automatically selected if the Easy Collider Editor window is open."), "Toggle Autoselect on Prefab Open", ref ECEPreferences.AutoSelectOnPrefabOpen); + ECUI.ToggleLeftUndoable(ECEPreferences, new GUIContent("Rotated pivot at center", "When true, new rotated collider holders are created with their pivot approximately at their center."), "Toggle rotated pivot at center", ref ECEPreferences.RotatedColliderPivotAtCenter); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.BeginHorizontal(); + ECUI.ToggleLeftUndoable(ECEPreferences, new GUIContent("Capsule Orientation from Cylinder", "When enabled, the orientation dropdown that applies to cylinders applies to capsules as well. Unlike cylinder alignment, capsules do not visually change, and instead simply are rotated to align along the specified axis. Extra gameobjects are made to hold the capsules that need alignment."), "Toggle Capsule Orientation", ref ECEPreferences.CylinderAsCapsuleOrientation); + ECUI.ToggleLeftUndoable(ECEPreferences, new GUIContent("Allow Scene Selection", "When enabled, selecting objects in the background of the scene view can be done with a single click even when an object is currently selected for collider creation."), "Allow background selection", ref ECEPreferences.AllowBackgroundSelection); + EditorGUILayout.EndHorizontal(); + ECUI.FloatFieldUndoable(ECEPreferences, new GUIContent("Raycast Delay:", "How often to do a raycast to select a vertex / collider."), "Change raycast delay time", ref ECEPreferences.RaycastDelayTime); + + // shader vs gizmo for rendering all points + EditorGUI.BeginChangeCheck(); + RENDER_POINT_TYPE render_type = (RENDER_POINT_TYPE)EditorGUILayout.EnumPopup(new GUIContent("Render Vertex Method:", "Gizmos are usable by everyone, but slow when large amount of vertices are selected. The shader uses a compute buffer which requires at least shader model 4.5, but is significantly faster."), ECEPreferences.RenderPointType); + if (EditorGUI.EndChangeCheck()) + { + + int group = Undo.GetCurrentGroup(); + Undo.RecordObject(ECEPreferences, "Change Render Method"); + ECEPreferences.RenderPointType = render_type; + Undo.RecordObject(ECEditor, "Change Render Method"); + ECEditor.ChangeRenderPointType(render_type); + Undo.CollapseUndoOperations(group); + } + + // if using gizmos: + if (ECEPreferences.RenderPointType == RENDER_POINT_TYPE.GIZMOS) + { + EditorGUI.BeginChangeCheck(); + GIZMO_TYPE gizmo_type = (GIZMO_TYPE)EditorGUILayout.EnumPopup(new GUIContent("Gizmo Type:", "Type of gizmos to draw for selected/hovered/displayed vertices"), ECEPreferences.GizmoType); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(ECEPreferences, "Change gizmo type"); + ECEPreferences.GizmoType = gizmo_type; + } + EditorGUILayout.BeginHorizontal(); + if (ECEditor.Gizmos != null) + { + ECUI.ToggleLeftUndoable(ECEditor.Gizmos, new GUIContent("Draw Gizmos", "Drawing gizmo can be slow with a significant number of vertices enabled."), "Toggle Draw Gizmos", ref ECEditor.Gizmos.DrawGizmos); + } + ECUI.ToggleLeftUndoable(ECEPreferences, new GUIContent("Fixed Gizmo Scale", "If true uses a fixed screen size for hovered, selected, and displayed vertices regardless of world position."), "Toggle use fixed gizmo scale", ref ECEPreferences.UseFixedGizmoScale); + EditorGUILayout.EndHorizontal(); + } + else if (ECEPreferences.RenderPointType == RENDER_POINT_TYPE.SHADER) + { + if (SystemInfo.graphicsShaderLevel < 45) + { + ECUI.LabelIcon("Your system does not support compute shaders, please change to using gizmos.", "console.warnicon.sml"); + } + } + + #endregion + + ECUI.HorizontalLineLight(); + + // reset preferences to all default values. + if (GUILayout.Button(new GUIContent("Reset Preferences to Default", "Resets preferences to their default settings.")) && EditorUtility.DisplayDialog("Reset Preferences", "Are you sure you want to reset Easy Collider Editor's preferences to the default values?", "Yes", "Cancel")) + { + ECEPreferences.SetDefaultValues(); + for (int i = 0; i < keysChanging.Length; i++) + { + keysChanging[i] = false; + } + } + + if (EditorGUI.EndChangeCheck()) + { + RepaintLastActiveSceneView(); + } + } + } + + + private bool showDuplicationTools; + + /// + /// Draws the collider duplication tools ui + /// + void DrawColliderDuplicationTools() + { + EditorGUI.BeginChangeCheck(); + showDuplicationTools = ECUI.FoldoutBold("Collider Duplication Tools", ref showDuplicationTools, "Allows creating multiple colliders around at once in a ring shape."); + if (EditorGUI.EndChangeCheck()) + { + if (showDuplicationTools) + { + ECEPreferences.rotatedDupeSettings.enabled = true; + } + ECPreviewer.ClearPreview(); + UpdatePreview(true); + FocusSceneView(); + } + if (showDuplicationTools) + { + EditorGUI.BeginChangeCheck(); + bool enabled = EditorGUILayout.ToggleLeft("Enable Duplication", ECEPreferences.rotatedDupeSettings.enabled); + GameObject pivot = (GameObject)EditorGUILayout.ObjectField(new GUIContent("Pivot", "Transform to rotate and duplicate around. Can be different than the AttachTo object."), ECEPreferences.rotatedDupeSettings.pivot, typeof(GameObject), true); + EasyColliderRotateDuplicate.ROTATE_AXIS axis = (EasyColliderRotateDuplicate.ROTATE_AXIS)EditorGUILayout.EnumPopup(new GUIContent("Rotation Axis", "Local axis around which the colliders are rotated and duplicated."), ECEPreferences.rotatedDupeSettings.axis); + if (EditorGUI.EndChangeCheck() || (pivot == null && ECEditor.SelectedGameObject != null)) + { + if (pivot == null && ECEditor.SelectedGameObject != null) + { + pivot = ECEditor.SelectedGameObject; + } + Undo.RecordObject(ECEPreferences, "Change Duplication Settings"); + int group = Undo.GetCurrentGroup(); + ECEPreferences.rotatedDupeSettings.enabled = enabled; + ECEPreferences.rotatedDupeSettings.pivot = pivot; + ECEPreferences.rotatedDupeSettings.axis = axis; + ECPreviewer.ClearPreview(); + UpdatePreview(true); + FocusSceneView(); + Undo.CollapseUndoOperations(group); + } + EditorGUI.BeginChangeCheck(); + int numberOfColliders = EditorGUILayout.IntSlider(new GUIContent("Number of Colliders", "Number of colliders to create"), ECEPreferences.rotatedDupeSettings.NumberOfDuplications, 1, 128); + float startRotation = EditorGUILayout.Slider(new GUIContent("Start Angle", "Angle to start the duplication at"), ECEPreferences.rotatedDupeSettings.StartRotation, -360, 360); + float endRotation = EditorGUILayout.Slider(new GUIContent("End Angle", "Angle to end the duplication at"), ECEPreferences.rotatedDupeSettings.EndRotation, -360, 360); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(ECEPreferences, "Change duplication settings."); + int group = Undo.GetCurrentGroup(); + ECEPreferences.rotatedDupeSettings.NumberOfDuplications = numberOfColliders; + ECEPreferences.rotatedDupeSettings.StartRotation = startRotation; + ECEPreferences.rotatedDupeSettings.EndRotation = endRotation; + ECPreviewer.ClearPreview(); + UpdatePreview(true); + RepaintLastActiveSceneView(); + Undo.CollapseUndoOperations(group); + } + } + else if (!showDuplicationTools && ECEPreferences.rotatedDupeSettings.enabled) + { + ECEPreferences.rotatedDupeSettings.enabled = false; + ECPreviewer.ClearPreview(); + UpdatePreview(true); + FocusSceneView(); + } + } + + /// + /// Draws the currently selected toolbar item UI. + /// + private void DrawSelectedToolbar() + { + if (CurrentTab == ECE_WINDOW_TAB.Creation) + { // create or edit colliders + DrawVertexSelectionTools(); + DrawColliderCreationTools(); + DrawColliderDuplicationTools(); + } + else if (CurrentTab == ECE_WINDOW_TAB.Editing) + { + DrawColliderSelectionTools(); + // select / remove colliders + DrawColliderTools(); + DrawColliderMergeTools(); + } + else if (CurrentTab == ECE_WINDOW_TAB.VHACD) + { // VHACD + DrawVHACDTools(); + } + else if (CurrentTab == ECE_WINDOW_TAB.AutoSkinned) + { // auto skinned mesh generation. + DrawAutoSkinnedMeshTools(); + } + } + + /// + /// Checks if tips are enabled in preferences, and updates and draws them as needed. + /// + private void DrawTips() + { + // Draw tips if set in preferences. + if (ECEPreferences.DisplayTips) + { + if (CurrentTips.Count > 0) + { + GUIStyle tipStyle = new GUIStyle(GUI.skin.label); + tipStyle.fontStyle = FontStyle.Bold; + tipStyle.alignment = TextAnchor.UpperCenter; + GUILayout.Label("Tips", tipStyle); + tipStyle.wordWrap = true; + tipStyle.alignment = TextAnchor.UpperLeft; + tipStyle.fontStyle = FontStyle.Normal; + foreach (string tip in CurrentTips) + { + EditorGUILayout.LabelField("- " + tip, tipStyle); + } + } + } + // always draw documentation link. + DrawDocumentationTip(); + } + + /// + /// Draws the toolbar that allows the user to select which ui is being displayed + /// + private void DrawToolbar() + { + // + // tool bars for individual things + GUIStyle toolbarStyle = new GUIStyle(GUI.skin.button); + toolbarStyle.margin = new RectOffset(2, 2, 0, 0); + int currentSelectedTab = (int)CurrentTab; + EditorGUI.BeginChangeCheck(); + int currentTabRow1 = GUILayout.Toolbar(currentSelectedTab, TabsRow1, toolbarStyle); + + if (ECEditor.VertexSelectEnabled == true && CurrentTab != ECE_WINDOW_TAB.Creation && CurrentTab != ECE_WINDOW_TAB.VHACD) + { + CurrentTab = ECE_WINDOW_TAB.Creation; + } + else if (ECEditor.ColliderSelectEnabled && CurrentTab != ECE_WINDOW_TAB.Editing) + { + CurrentTab = ECE_WINDOW_TAB.Editing; + } + // Row 1: Creation / Removal. + if (EditorGUI.EndChangeCheck()) + { + Undo.RegisterCompleteObjectUndo(ECEditor, "Change tabs"); + int group = Undo.GetCurrentGroup(); + if (currentTabRow1 == (int)CurrentTab) + { + currentTabRow1 = -1; + ECEditor.VertexSelectEnabled = false; + ECEditor.ColliderSelectEnabled = false; + } + Undo.RegisterCompleteObjectUndo(this, "Change tabs"); + // ECE_WINDOW_TAB previousTab = CurrentTab; + CurrentTab = (ECE_WINDOW_TAB)currentTabRow1; + if (CurrentTab == ECE_WINDOW_TAB.Creation) + { + ECEditor.VertexSelectEnabled = true; + ECEditor.ColliderSelectEnabled = false; + } + else if (CurrentTab == ECE_WINDOW_TAB.Editing) + { + ECEditor.VertexSelectEnabled = false; + ECEditor.ColliderSelectEnabled = true; + } + Undo.CollapseUndoOperations(group); + ECPreviewer.ClearPreview(); + FocusSceneView(); + } + // offset for row 2 (VHACD + Autoskinned.) + currentSelectedTab = (int)CurrentTab - TabsRow1.Length; + EditorGUI.BeginChangeCheck(); + int currentTabRow2 = GUILayout.Toolbar(currentSelectedTab, TabsRow2, toolbarStyle); + if (EditorGUI.EndChangeCheck()) + { + Undo.RegisterCompleteObjectUndo(ECEditor, "Change tabs"); + int group = Undo.GetCurrentGroup(); + if (currentTabRow2 + TabsRow1.Length == (int)CurrentTab) + { + currentTabRow2 = -TabsRow1.Length - TabsRow2.Length; + } + ECEditor.VertexSelectEnabled = false; + ECEditor.ColliderSelectEnabled = false; + Undo.RegisterCompleteObjectUndo(this, "Change tabs"); + CurrentTab = (ECE_WINDOW_TAB)(currentTabRow2 + TabsRow1.Length); + if (CurrentTab == ECE_WINDOW_TAB.VHACD && ECEPreferences.VHACDParameters.UseSelectedVertices) + { + ECEditor.VertexSelectEnabled = true; + } + Undo.CollapseUndoOperations(group); + ECPreviewer.ClearPreview(); + FocusSceneView(); + } + ECEPreferences.CurrentWindowTab = CurrentTab; + } + + + /// + /// Draws the top settings of selected gameobject, attach, finish button, and toggles for vert/collider/display all/include child meshes. + /// + private void DrawTopSettings() + { + if (EditorApplication.isPlaying) + { + ECUI.LabelIcon("Exit play mode before editing colliders.", "console.warnicon.sml"); + } + // Selected Gameobject field. + EditorGUILayout.BeginHorizontal(); + EditorGUI.BeginChangeCheck(); + GameObject selected = (GameObject)EditorGUILayout.ObjectField(new GUIContent("Selected GameObject", "Selected GameObject is usually the gameobject with the mesh, or its parent."), ECEditor.SelectedGameObject, typeof(GameObject), true); + if (EditorGUI.EndChangeCheck() && !EditorApplication.isPlaying) + { + ChangeToNewObject(selected); + } + if (ECUI.IconButton("SceneLoadOut", "Select the object currently selected in the scene view.")) + { + if (Selection.activeGameObject != null) + { + ChangeToNewObject(Selection.activeGameObject); + } + } + EditorGUILayout.EndHorizontal(); + // draw a warning label if there are no mesh's found and we're on creation or VHACD tabs. + if ((CurrentTab == ECE_WINDOW_TAB.Creation || CurrentTab == ECE_WINDOW_TAB.VHACD) && ECEditor.SelectedGameObject != null && ECEditor.MeshFilters.Count == 0) + { + ECUI.LabelIcon("No mesh found on " + ECEditor.SelectedGameObject.name + ". Try enabling child meshes.", "console.erroricon.sml"); + } + // draw a warning on auto skinned tab if there's no skinned mesh renderer found. + else if (CurrentTab == ECE_WINDOW_TAB.AutoSkinned && ECEditor.SelectedGameObject != null && !ECEditor.HasSkinnedMeshRenderer && ECAutoSkinned.renderer == null) + { + ECUI.LabelIcon("No Skinned Mesh Renderer found on " + ECEditor.SelectedGameObject.name + " or it's children.", "console.erroricon.sml"); + } + + // Attach to gameobject field. + EditorGUI.BeginChangeCheck(); + GameObject attachTo = (GameObject)EditorGUILayout.ObjectField(new GUIContent("Attach Collider To", "Gameobject to attach the collider to, usually the selected gameobject."), ECEditor.AttachToObject, typeof(GameObject), true); + if (EditorGUI.EndChangeCheck() && !EditorApplication.isPlaying) + { + Undo.RegisterCompleteObjectUndo(ECEditor, "Change AttachTo GameObject"); + int group = Undo.GetCurrentGroup(); + ECEditor.AttachToObject = attachTo; + Undo.CollapseUndoOperations(group); + FocusSceneView(); + } + + DrawFinishButton(); + + ECEDots.OnInspectorGUI(ECEditor); + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.BeginVertical(); + // vertex selection + EditorGUI.BeginChangeCheck(); + bool vertexToggle = ECUI.DisableableToggleLeft("Vertex Selection", "Allows selection of vertices and points by raycast in the sceneview", "Select a gameobject with a mesh, or enable child meshes.", ECEditor.SelectedGameObject != null && ECEditor.MeshFilters.Count > 0, ECEditor.VertexSelectEnabled); + if (EditorGUI.EndChangeCheck()) + { + Undo.RegisterCompleteObjectUndo(ECEditor, "Toggle Vertex Selection"); + int group = Undo.GetCurrentGroup(); + ECEditor.VertexSelectEnabled = vertexToggle; + if (vertexToggle) + { + ECEditor.ColliderSelectEnabled = false; + } + Undo.CollapseUndoOperations(group); + } + // Include child meshes. + EditorGUI.BeginChangeCheck(); + // bool includeChildMeshes = EditorGUILayout.ToggleLeft(new GUIContent("Include Child Meshes", "Enables child mesh vertices in vertex selection."), ECEditor.IncludeChildMeshes); + bool includeChildMeshes = GUILayout.Toggle(ECEditor.IncludeChildMeshes, new GUIContent("Child Meshes", "Enables child mesh vertices in vertex selection.")); + if (EditorGUI.EndChangeCheck()) + { + Undo.RegisterCompleteObjectUndo(ECEditor, "ECE: " + (includeChildMeshes ? "Enable " : "Disable ") + " include child meshes."); + int group = Undo.GetCurrentGroup(); + ECEditor.IncludeChildMeshes = includeChildMeshes; + Undo.CollapseUndoOperations(group); + SetVHACDNeedsUpdate(); + // focus on child mesh togle change. + FocusSceneView(); + } + + + EditorGUILayout.EndVertical(); + EditorGUILayout.BeginVertical(); + + // Collider selection + EditorGUI.BeginChangeCheck(); + bool colliderSelectEnabled = ECUI.DisableableToggleLeft("Collider Selection", "Allows selection of colliders by raycast in the sceneview.", "Select a GameObject.", ECEditor.SelectedGameObject != null, ECEditor.ColliderSelectEnabled); + if (EditorGUI.EndChangeCheck()) + { + Undo.RegisterCompleteObjectUndo(ECEditor, "Toggle Collider Selection"); + int group = Undo.GetCurrentGroup(); + ECEditor.ColliderSelectEnabled = colliderSelectEnabled; + if (ECEditor.ColliderSelectEnabled) + { + ECEditor.VertexSelectEnabled = false; + } + Undo.CollapseUndoOperations(group); + FocusSceneView(); + } + // Display all vertices toggle + EditorGUI.BeginChangeCheck(); + bool displayAllVertices = ECUI.DisableableToggleLeft("Display All Vertices", "Helps make sure everything is properly set up, as it will display all the vertices that are able to be selected.", "Select a GameObject.", + ECEditor.SelectedGameObject != null, + ECEPreferences.DisplayAllVertices); + if (EditorGUI.EndChangeCheck()) + { + Undo.RegisterCompleteObjectUndo(ECEPreferences, "Toggle display all vertices"); + int group = Undo.GetCurrentGroup(); + ECEPreferences.DisplayAllVertices = displayAllVertices; + if (ECEPreferences.DisplayAllVertices) + { + if (ECEditor.Compute != null) + { + ECEditor.Compute.SetDisplayAllBuffer(ECEditor.GetAllWorldMeshVertices()); + } + // would occasionally not update gizmos, not 100% sure how to reproduce, but it has happened, so this ensures it'll work when changing. + else if (ECEditor.Gizmos != null) + { + ECEditor.Gizmos.DisplayVertexPositions = ECEditor.GetAllWorldMeshVertices(); + } + } + Undo.CollapseUndoOperations(group); + // Repaint so it gets updated immediately. + SceneView.RepaintAll(); + FocusSceneView(); + } + + EditorGUILayout.EndVertical(); + EditorGUILayout.EndHorizontal(); + } + + /// + /// Tool tips for vertex snap tools. + /// + readonly string[] VERTEX_SNAP_TOOLTIPS = new string[3] { "Snap to only selectable vertices.\nAuto enabled when CTRL or Snap Add key is held.", "Snap to only removeable vertices.\nAuto enabled when ALT or Snap Remove key is held.", "Snap to both." }; + readonly string[] VERTEX_SNAP_LABELS = new string[3] { "+", "-", "+-" }; + + public enum VertexSelectionTool + { + None, + Clear, + Grow, + GrowLast, + Invert, + Ring, + } + + void UseVertexSelectionTool(VertexSelectionTool tool) + { + Undo.RegisterCompleteObjectUndo(ECEditor, tool.ToString() + " vertices tool"); + int group = Undo.GetCurrentGroup(); + + if (tool == VertexSelectionTool.Clear) + { + ECEditor.ClearSelectedVertices(); + } + else if (tool == VertexSelectionTool.Grow) + { + // flood to max if control is held when button is clicked. + bool growMax = (Event.current != null && Event.current.modifiers == EventModifiers.Control) ? true : false; + if (growMax) + { + ECEditor.GrowAllSelectedVerticesMax(); + } + else + { + ECEditor.GrowAllSelectedVertices(); + } + } + else if (tool == VertexSelectionTool.GrowLast) + { + bool growMax = (Event.current != null && Event.current.modifiers == EventModifiers.Control) ? true : false; + if (growMax) + { + ECEditor.GrowLastSelectedVerticesMax(); + } + else + { + ECEditor.GrowLastSelectedVertices(); + } + } + else if (tool == VertexSelectionTool.Invert) + { + ECEditor.InvertSelectedVertices(); + } + else if (tool == VertexSelectionTool.Ring) + { + ECEditor.RingSelectVertices(); + } + + + Undo.CollapseUndoOperations(group); + // repaint to update quickly, as clicking the editor window de-focuses the scene which stops the visual update. + UpdateVertexDisplays(); + FocusSceneView(); + UpdatePreview(true); + SetVHACDNeedsUpdate(true); + } + + /// + /// Draws the vertex selection tools UI. (Label, snaps, and buttons) + /// + private void DrawVertexSelectionTools() + { + EditorGUILayout.BeginHorizontal(); + ECUI.LabelBold("Vertex Selection Tools"); + GUILayout.FlexibleSpace(); + if (ECEPreferences.ShowSelectedVertexCount) + { + ECUI.Label(ECEditor.SelectedVertices.Count.ToString(), "Number of vertices currently selected."); + GUILayout.FlexibleSpace(); + } + GUIStyle snapsStyle = new GUIStyle(GUI.skin.label); + snapsStyle.padding.top = 3; + GUILayout.Label("Snaps:", snapsStyle); + EditorGUI.BeginChangeCheck(); + ECEPreferences.VertexSnapMethod = (VERTEX_SNAP_METHOD)ECUI.EnumButtonArray(ECEPreferences.VertexSnapMethod, VERTEX_SNAP_LABELS, VERTEX_SNAP_TOOLTIPS); + if (EditorGUI.EndChangeCheck()) + { + FocusSceneView(); + } + EditorGUILayout.EndHorizontal(); + EditorGUILayout.BeginHorizontal(); + // deselect all + if (ECUI.DisableableButtonWithShortcut("Clear", + "Deselects all currently selected points.", + "No points are currently selected.", + ECEditor.SelectedVertices.Count > 0, ECEPreferences.ShortcutClear, ECEPreferences.EnableVertexToolsShortcuts)) + { + UseVertexSelectionTool(VertexSelectionTool.Clear); + } + // grow all vertices + if (ECUI.DisableableButtonWithShortcut("Grow", + "Grows the selection of vertices to all the connected vertices.\nHold CTRL and click to flood the vertices from the current selected vertices.", + "No vertices are current selected.", + ECEditor.SelectedVertices.Count > 0, ECEPreferences.ShortcutGrow, ECEPreferences.EnableVertexToolsShortcuts)) + { + UseVertexSelectionTool(VertexSelectionTool.Grow); + } + // grow last select vertices + if (ECUI.DisableableButtonWithShortcut("Grow Last", + "Grows the selection of vertices from the last selected vertices.\nHold CTRL and click to flood the vertices from the last selected vertices.", + "No vertices are currently selected.", + ECEditor.SelectedVertices.Count > 0, ECEPreferences.ShortcutGrowLast, ECEPreferences.EnableVertexToolsShortcuts)) + { + UseVertexSelectionTool(VertexSelectionTool.GrowLast); + } + // invert selected + if (ECUI.DisableableButtonWithShortcut("Invert", + "Deselects all currently selected vertices and points, and selects all unselected vertices.", + "No gameobject is currently selected, or no meshes found on the selected gameobject.", + ECEditor.SelectedGameObject != null && ECEditor.MeshFilters.Count > 0, ECEPreferences.ShortcutInvert, ECEPreferences.EnableVertexToolsShortcuts + )) + { + UseVertexSelectionTool(VertexSelectionTool.Invert); + } + if (ECUI.DisableableButtonWithShortcut("Ring", + "Attempts to do a ring select from the last 2 vertices around the object.", + "At least 2 vertices must be selected to do a ring select.", + ECEditor.SelectedVertices.Count >= 2, ECEPreferences.ShortcutRing, ECEPreferences.EnableVertexToolsShortcuts)) + { + UseVertexSelectionTool(VertexSelectionTool.Ring); + } + + + + EditorGUILayout.EndHorizontal(); + + + if (ECEditor.HasSkinnedMeshRenderer) + { + ECUI.LabelBold("Skinned Mesh Bone Selection Tools"); + EditorGUILayout.BeginHorizontal(); + bool setAttachOnBoneChange = EditorGUILayout.ToggleLeft(new GUIContent("Set Attach to Bone", "When enabled, automatically sets the attach to object to the selected bone."), ECEditor.SetAttachToOnBoneChange); + if (setAttachOnBoneChange != ECEditor.SetAttachToOnBoneChange) + { + Undo.RegisterCompleteObjectUndo(ECEditor, "Change attach to on bone change toggle"); + ECEditor.SetAttachToOnBoneChange = setAttachOnBoneChange; + } + bool clearVertsOnBoneChange = EditorGUILayout.ToggleLeft(new GUIContent("Clear Vertices on Bone Change", "When enabled, clears selected vertices when the selected bone changes."), ECEditor.CleanVerticesOnBoneChange); + if (clearVertsOnBoneChange != ECEditor.CleanVerticesOnBoneChange) + { + Undo.RegisterCompleteObjectUndo(ECEditor, "Clear verts on change"); + ECEditor.CleanVerticesOnBoneChange = clearVertsOnBoneChange; + } + EditorGUILayout.EndHorizontal(); + + Transform selectedBone = ECEditor.LastSelectedBone; + string contentDisplay = "No Bone Selected"; + if (selectedBone != null) + { + contentDisplay = selectedBone.name; + } + + EditorGUILayout.BeginHorizontal(); + if (EditorGUILayout.DropdownButton(new GUIContent(contentDisplay), FocusType.Passive)) + { + GenericMenu menu = new GenericMenu(); + foreach (var bone in ECEditor.BoneTransforms) + { + menu.AddItem(new GUIContent(bone.name), bone == selectedBone, () => SetSelectedBone(bone, ECEditor.SelectedBoneWeight)); + } + menu.DropDown(GUILayoutUtility.GetLastRect()); + } + + if (selectedBone != null && GUILayout.Button(new GUIContent("X", "Clears vertices of selected bone."), GUILayout.ExpandWidth(false), GUILayout.MaxHeight(14f))) + { + ClearSelectedBoneVertices(selectedBone); + } + + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.BeginHorizontal(); + float newWeight = EditorGUILayout.Slider(new GUIContent("Weight Cutoff", "Vertices that have a weight for the selected bone above this amount will be selected"), ECEditor.SelectedBoneWeight, 0, 1); + if (newWeight != ECEditor.SelectedBoneWeight) + { + SetSelectedBone(ECEditor.LastSelectedBone, newWeight); + } + EditorGUILayout.EndHorizontal(); + } + } + + void ClearSelectedBoneVertices(Transform bone) + { + if (bone == null) return; + Undo.RegisterCompleteObjectUndo(ECEditor, "Select vertices"); + int group = Undo.GetCurrentGroup(); + ECEditor.ClearVerticesForBone(bone); + Undo.CollapseUndoOperations(group); + // repaint so buttons appear for vertex selection + UpdateVertexDisplays(); + // updates VHACD preview + SetVHACDNeedsUpdate(true); + this.Repaint(); + } + + void SetSelectedBone(Transform bone, float weight) + { + if (bone == null) return; + Undo.RegisterCompleteObjectUndo(ECEditor, "Select vertices"); + int group = Undo.GetCurrentGroup(); + ECEditor.SelectedBoneWeight = weight; + if (ECEditor.CleanVerticesOnBoneChange) + { + ECEditor.ClearSelectedVertices(); + } + if (ECEditor.LastSelectedBone != bone && ECEditor.SetAttachToOnBoneChange) + { + ECEditor.AttachToObject = bone.gameObject; + } + ECEditor.SelectVerticesForBone(bone, ECEditor.SelectedBoneWeight); + Undo.CollapseUndoOperations(group); + // repaint so buttons appear for vertex selection + UpdateVertexDisplays(); + // updates VHACD preview + SetVHACDNeedsUpdate(true); + this.Repaint(); + } + + #endregion + #region VHACD + + /// + /// Checks and updates VHACD progress through it's various stages. + /// + private void CheckVHACDProgress() + { + // in case someone deletes the object(s) vhacd is calculating on while it is calculating.. + if (ECEditor.SelectedGameObject == null)// || !VerifyMeshFilters()) + { + _VHACDCurrentStep = 5; // final step where we clean up everything. + } + // if we're processing multiple mesh filters using separate child meshes we need to reset the current step to 0, and increase the current mesh filter. + if ( + _VHACDCurrentStep == 5 + && VHACDCurrentParameters.SeparateChildMeshes + && VHACDCurrentParameters.CurrentMeshFilter < VHACDCurrentParameters.MeshFilters.Count - 1 + ) + { + _VHACDCurrentStep = 0; + VHACDCurrentParameters.CurrentMeshFilter += 1; + } + // adjust save path on step 0. + if (_VHACDCurrentStep == 0 && VHACDCurrentParameters.SeparateChildMeshes && !VHACDCurrentParameters.IsCalculationForPreview) + { + if (VHACDCurrentParameters.MeshFilters[VHACDCurrentParameters.CurrentMeshFilter] != null) + { + VHACDCurrentParameters.SavePath = EasyColliderSaving.GetValidConvexHullPath(VHACDCurrentParameters.MeshFilters[VHACDCurrentParameters.CurrentMeshFilter].gameObject); + } + else + { + // user deleted one of the objects vhacd is running on. + _VHACDCurrentStep = 5; // change to step where we clean up/cancel the calculation. + } + + } + else if (VHACDCurrentParameters.SavePath == "") + { + VHACDCurrentParameters.SavePath = EasyColliderSaving.GetValidConvexHullPath(ECEditor.SelectedGameObject); + } + if (_VHACDIsComputing) + { + // prevents errors where we are mid process and something like script saving occurs. + if (_VHACDCurrentStep != 0 && !ECEditor.VHACDExists()) + { + _VHACDIsComputing = false; + return; + } + _VHACDCheckCount += 1; + string current = _VHACDProgressString; + if (VHACDCurrentParameters.SeparateChildMeshes) + { + // progress string for separate meshes displays a mesh #/total# + _VHACDProgressString = "Mesh: " + VHACDCurrentParameters.CurrentMeshFilter + " / " + VHACDCurrentParameters.MeshFilters.Count + " | "; + } + else + { + _VHACDProgressString = ""; + } + // pretty much just updates the progress string and sees if the step is complete. + switch (_VHACDCurrentStep) + { + case 0: + _VHACDCheckCount = 0; + _VHACDProgressString += "Initializing"; + goto default; + case 1: + _VHACDProgressString += "Preparing Mesh Data"; + goto default; + case 2: + // dots so people know it's still calculating... + if (_VHACDCheckCount % 25 == 0) + { + _VHACDDots += "."; + if (_VHACDDots.Length == 4) + { + _VHACDDots = ""; + } + } + _VHACDProgressString += "Calculating Convex Hulls" + _VHACDDots; + goto default; + case 3: + _VHACDProgressString += "Saving Convex Meshes"; + goto default; + case 4: + _VHACDProgressString += "Adding Convex Colliders"; + goto default; + case 5: + _VHACDProgressString = "Ending VHACD"; + goto default; + default: + // each step returns true when it is complete, so we can increase the current step. + // but doesn't run the next step until it's checked again. Although this slightly slows it down, it's not really a big issue. + // and makes it easy to reset the step on multiple meshes using separate child meshes. + if (ECEditor.VHACDRunStep(_VHACDCurrentStep, VHACDCurrentParameters, ECEPreferences.SaveConvexHullAsAsset)) + { + _VHACDCurrentStep += 1; + } + // vhacd is finished, set the preview if required. + if (VHACDCurrentParameters.IsCalculationForPreview && _VHACDCurrentStep == 6) + { + VHACDPreviewResult = ECEditor.VHACDGetPreview(); + // repaint scene on vhacd finished for preview + SceneView.RepaintAll(); + } + else if (_VHACDCurrentStep == 6) + { + // clear preview if at the end of calculation and it wasn't for the preview. + ECEditor.VHACDClearPreviewResult(); + SceneView.RepaintAll(); + } + break; + } + + // reset everything when done computing. + if (_VHACDCurrentStep == 6) + { + _VHACDIsComputing = false; + _VHACDCurrentStep = 0; + } + // update the UI so the progress bar shows it's doing something. + if (current != _VHACDProgressString) + { + this.Repaint(); + } + } + } + + /// + /// Draws the VHACD tools UI. + /// + private void DrawVHACDTools() + { + if (ECEPreferences.VHACDParameters.UseSelectedVertices) + { + if (!ECEditor.VertexSelectEnabled) + { + ECEditor.VertexSelectEnabled = true; + } + DrawVertexSelectionTools(); + } + if (ECEPreferences.VHACDParameters.UseSelectedVertices) + { + EditorGUI.BeginChangeCheck(); + ECEPreferences.VHACDParameters.NormalExtrudeMultiplier = EditorGUILayout.FloatField("Normal Extrusion", ECEPreferences.VHACDParameters.NormalExtrudeMultiplier); + if (EditorGUI.EndChangeCheck()) + { + SetVHACDNeedsUpdate(true); + } + } + else + { + ECEPreferences.VHACDParameters.NormalExtrudeMultiplier = 0.0f; + } + + GUIStyle style = new GUIStyle(GUI.skin.label); + style.fontStyle = FontStyle.Bold; + EditorGUILayout.BeginHorizontal(); + GUILayout.Label("VHACD", style); + bool VHACDPReview = ECEPreferences.VHACDPreview; + ECUI.ToggleLeftUndoable(ECEPreferences, new GUIContent("Preview Result", "When enabled, as parameters are being changed, will draw the result of the VHACD calculation without creating any colliders. Note that the preview calculation uses the minimum resolution setting of 10,000 regardless of set resolution."), "Toggle VHACD Preview", ref ECEPreferences.VHACDPreview); + if (VHACDPReview != ECEPreferences.VHACDPreview) + { + SetVHACDNeedsUpdate(); + } + + + + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Load Settings", GUILayout.ExpandWidth(false))) + { + VHACDScriptableSettings settingsToLoad = VHACDScriptableSettings.Load(); + if (settingsToLoad != null) + { + Undo.RegisterCompleteObjectUndo(ECEPreferences, "Load VHACD Settings"); + int group = Undo.GetCurrentGroup(); + ECEPreferences.VHACDParameters = new VHACDParameters(settingsToLoad.GetParameters()); + ECEPreferences.VHACDResFloat = Mathf.Log(ECEPreferences.VHACDParameters.resolution, 2); + Undo.CollapseUndoOperations(group); + SetVHACDNeedsUpdate(); + } + } + if (GUILayout.Button("Save Settings", GUILayout.ExpandWidth(false))) + { + VHACDScriptableSettings.Save(ECEPreferences.VHACDParameters); + } + + + EditorGUILayout.EndHorizontal(); + _ShowVHACDAdvancedSettings = EditorGUILayout.Foldout(_ShowVHACDAdvancedSettings, "Advanced VHACD Settings"); + EditorGUI.BeginChangeCheck(); + if (_ShowVHACDAdvancedSettings) + { + EditorGUILayout.BeginHorizontal(); + ECEPreferences.VHACDParameters.forceUnder256Triangles = EditorGUILayout.ToggleLeft(new GUIContent("Force <256 Tris", "Enables recalculation of convex hulls to ensure all generated hulls have less than 256 triangles. Convex mesh colliders with >256 triangles generate errors in some versions of unity."), ECEPreferences.VHACDParameters.forceUnder256Triangles); + if (GUILayout.Button(new GUIContent("Default", "Reset all VHACD settings to default values."))) + { + ECEPreferences.VHACDSetDefaultParameters(); + ECEPreferences.VHACDResFloat = Mathf.Log(ECEPreferences.VHACDParameters.resolution, 2); + } + EditorGUILayout.EndHorizontal(); + ECEPreferences.VHACDParameters.concavity = (double)EditorGUILayout.Slider(new GUIContent("Concavity", "Maximum concavity."), (float)ECEPreferences.VHACDParameters.concavity, 0, 1); + ECEPreferences.VHACDParameters.alpha = (double)EditorGUILayout.Slider(new GUIContent("Alpha", "Controls bias towards clipping along symmetry planes."), (float)ECEPreferences.VHACDParameters.alpha, 0, 1); + ECEPreferences.VHACDParameters.beta = (double)EditorGUILayout.Slider(new GUIContent("Beta", "Controls bias towards clipping along revolution axes."), (float)ECEPreferences.VHACDParameters.beta, 0, 1); + ECEPreferences.VHACDParameters.minVolumePerCH = (double)EditorGUILayout.Slider(new GUIContent("Min Volume per Convex Hull", "Minimum volume for each convex hull. Higher values can cause some convex hulls to be removed."), (float)ECEPreferences.VHACDParameters.minVolumePerCH, 0, 1); + ECEPreferences.VHACDParameters.planeDownsampling = EditorGUILayout.IntSlider(new GUIContent("Plane Downsampling", "Controls granularity of the search for the best clipping plane."), ECEPreferences.VHACDParameters.planeDownsampling, 1, 16); + ECEPreferences.VHACDParameters.convexhullDownSampling = EditorGUILayout.IntSlider(new GUIContent("Convex Hull Downsampling", "Controls the precision of the convex-hull generation process during the clipping plane selection stage."), ECEPreferences.VHACDParameters.convexhullDownSampling, 1, 16); + // ECEPreferences.VHACDParameters.resolution = EditorGUILayout.IntSlider(new GUIContent("Resolution", "Maximum number of voxels used. Higher is more accurate, but significantly slower."), ECEPreferences.VHACDParameters.resolution, 10000, 64000000); // max resolution can be changed up to 64000000, but that will take a long time. + // the float values are the log2(10000) which is the min val, and the max val is the other. ie 2^x = 10000. + ECEPreferences.VHACDResFloat = EasyColliderUIHelpers.SliderFloatToIntBase2(new GUIContent("Resolution", "Maximum number of voxels used. Higher is more accurate, but significantly slower."), ECEPreferences.VHACDResFloat, 13.27f, 25.94f, ref ECEPreferences.VHACDParameters.resolution, 10000, 64000000); + ECEPreferences.VHACDParameters.maxConvexHulls = EditorGUILayout.IntSlider(new GUIContent("Max Convex Hulls", "Maximum number of convex hulls to create. Higher is more accurate but creates a greater number of mesh colliders."), ECEPreferences.VHACDParameters.maxConvexHulls, 1, 128); + ECEPreferences.VHACDParameters.maxNumVerticesPerConvexHull = EditorGUILayout.IntSlider(new GUIContent("Max Vertices per Hull", "Maximum number of vertices for each convex hull can have."), ECEPreferences.VHACDParameters.maxNumVerticesPerConvexHull, 4, 1024); + } + else + { + ECEPreferences.VHACDParameters.resolution = EditorGUILayout.IntSlider(new GUIContent("Resolution", "Maximum number of voxels used. Higher is more accurate, but significantly slower."), ECEPreferences.VHACDParameters.resolution, 10000, 128000); // max resolution can be changed up to 64000000, but that will take a long time. + ECEPreferences.VHACDResFloat = Mathf.Log(ECEPreferences.VHACDParameters.resolution, 2); + ECEPreferences.VHACDParameters.maxConvexHulls = EditorGUILayout.IntSlider(new GUIContent("Max Convex Hulls", "Maximum number of convex hulls to create. Higher is more accurate but creates a greater number of mesh colliders."), ECEPreferences.VHACDParameters.maxConvexHulls, 1, 128); + ECEPreferences.VHACDParameters.maxNumVerticesPerConvexHull = EditorGUILayout.IntSlider(new GUIContent("Max Vertices per Hull", "Maximum number of vertices for each convex hull can have."), ECEPreferences.VHACDParameters.maxNumVerticesPerConvexHull, 4, 255); + } + ECEPreferences.VHACDParameters.fillMode = (VHACD_FILL_MODE)EditorGUILayout.EnumPopup(new GUIContent("", + "Method used during voxelization to determine which are inside or outside the mesh's surface. \n FLOOD_FILL: A normal flood fill. Generally use this method. \n RAYCAST_FILL: Raycasting is used to determine which is inside or outside. Useful for when the mesh has holes. \n SURFACE_ONLY: Use when you want the source mesh to be treated as a hollow object." + ), ECEPreferences.VHACDParameters.fillMode); + // two column toggles + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.BeginVertical(); + ECEPreferences.VHACDParameters.projectHullVertices = GUILayout.Toggle(ECEPreferences.VHACDParameters.projectHullVertices, new GUIContent("Project Hull Vertices", "When true, each point on the hull is projected onto the mesh. Each vertex in the convex hull will lay on the source mesh.")); + bool seperatedChildMeshes = ECEPreferences.VHACDParameters.SeparateChildMeshes; + ECEPreferences.VHACDParameters.SeparateChildMeshes = ECUI.DisableableToggleLeft("Separate Child Meshes", "When enabled, child meshes are run seperately with the same settings through VHACD. Mesh Colliders generated are attached to the child mesh's object.", "Include child meshes is disabled.", ECEditor.IncludeChildMeshes, ECEPreferences.VHACDParameters.SeparateChildMeshes); + EditorGUILayout.EndVertical(); + + EditorGUILayout.BeginVertical(); + ECUI.ToggleLeftUndoable(ECEPreferences, new GUIContent("Save Hulls as Assets", "When true, saves colliders created from VHACD and Convex Mesh Colliders as .asset files."), "Toggle Save Convex Hulls as Assets", ref ECEPreferences.SaveConvexHullAsAsset); + bool useSelectedVertices = ECEPreferences.VHACDParameters.UseSelectedVertices; + EditorGUI.BeginChangeCheck(); + ECUI.ToggleLeftUndoable(ECEPreferences, new GUIContent("Use Selected Verts", "Runs VHACD only on a mesh created from the currently selected vertices."), "Toggle Only Selected Vertices", ref ECEPreferences.VHACDParameters.UseSelectedVertices); + if (EditorGUI.EndChangeCheck()) + { + FocusSceneView(); + } + EditorGUILayout.EndVertical(); + EditorGUILayout.EndHorizontal(); + + ECEPreferences.VHACDParameters.ConvertTo = (VHACD_CONVERSION)EditorGUILayout.EnumPopup("Convert To:", ECEPreferences.VHACDParameters.ConvertTo); + if (ECEPreferences.CylinderAsCapsuleOrientation && ECEPreferences.VHACDParameters.ConvertTo == VHACD_CONVERSION.Capsules) + { + ECEPreferences.CylinderOrientation = (CYLINDER_ORIENTATION)ECUI.EnumPopup(new GUIContent((ECEPreferences.CylinderAsCapsuleOrientation ? "Cylinder & Capsule Orientation" : "Cylinder Orientation:"), "Controls the way cylinders " + (ECEPreferences.CylinderAsCapsuleOrientation ? "and capsules " : "") + "are oriented during creation. \nAutomatic: Uses the largest axis.\nX,Y,Z:Orient Along local X, Y, and Z axis respectively."), ECEPreferences.CylinderOrientation, (ECEPreferences.CylinderAsCapsuleOrientation ? 200f : 130f)); + } + // only one of seperate child meshes, or use selected vertices can be enabled at once. + if (seperatedChildMeshes != ECEPreferences.VHACDParameters.SeparateChildMeshes && useSelectedVertices) + { + ECEPreferences.VHACDParameters.UseSelectedVertices = false; + } + else if (useSelectedVertices != ECEPreferences.VHACDParameters.UseSelectedVertices) + { + ECEPreferences.VHACDParameters.SeparateChildMeshes = false; + ECEditor.VertexSelectEnabled = ECEPreferences.VHACDParameters.UseSelectedVertices; + } + + // if any of the VHACD settings are changed, run a calculation using the current settings with low resolution for the preview. + if ((EditorGUI.EndChangeCheck() || _VHACDUpdatePreview) && ECEditor.SelectedGameObject != null && ECEPreferences.VHACDPreview) + { + // reset the forced update when selected vertices changes. + _VHACDUpdatePreview = false; + // can be null when editor is initially opened. + if (VHACDCurrentParameters == null) + { + VHACDCurrentParameters = ECEPreferences.VHACDParameters.Clone(); + } + // only restart the preview if vhacd isn't currently calculating, or the current calculation is for a preview. + if (VHACDCurrentParameters.IsCalculationForPreview || !_VHACDIsComputing) + { + if ((ECEditor.SelectedGameObject != null && ECEditor.MeshFilters.Count > 0) && (ECEPreferences.VHACDParameters.UseSelectedVertices ? (ECEditor.SelectedVertices.Count >= 4 || ECEditor.SelectedVertices.Count == 0) : true)) + { + ECEditor.VHACDClearPreviewResult(); + // set up the calculation. + ECEPreferences.VHACDParameters.MeshFilters = ECEditor.MeshFilters; + ECEPreferences.VHACDParameters.AttachTo = ECEditor.AttachToObject; + VHACDCurrentParameters = ECEPreferences.VHACDParameters.Clone(); + // we force resolution to between 10,000 and 128,000 so that preview speed is fast (ie clamped to non-advanced expanded parameters range.) + VHACDCurrentParameters.resolution = Mathf.Clamp(VHACDCurrentParameters.resolution, 10000, 128000); + VHACDCurrentParameters.IsCalculationForPreview = true; + _VHACDProgressString = "Initializing"; + _VHACDCurrentStep = 0; + _VHACDCheckCount = 0; + _VHACDIsComputing = true; + } + else + { + VHACDPreviewResult = null; + } + } + } + if (ECEPreferences.VHACDParameters.SeparateChildMeshes) + { + EditorGUILayout.BeginHorizontal(); + } + + ECEPreferences.VHACDParameters.vhacdResultMethod = (VHACD_RESULT_METHOD)EditorGUILayout.EnumPopup( + new GUIContent("Attach Method:", "Method to use to attach convex mesh colliders.\nAttach To: Attach all colliders to the object in Attach To field. \nChild Object: Attach colliders to a child of Attach To field.\nIndividual Child Objects: Each collider is attached to its own child whos parent is a child of the Attach To field."), ECEPreferences.VHACDParameters.vhacdResultMethod); + if (ECEPreferences.VHACDParameters.SeparateChildMeshes) + { + ECEPreferences.VHACDParameters.PerMeshAttachOverride = EditorGUILayout.ToggleLeft(new GUIContent("Per Mesh", "When enabled, as each mesh is ran through VHACD the Attach To field is automatically changed to the gameobject the mesh is from."), ECEPreferences.VHACDParameters.PerMeshAttachOverride); + EditorGUILayout.EndHorizontal(); + } + + if (ECUI.DisableableButton("VHACD - Generate Convex Mesh Colliders", "Generates convex mesh colliders using VHACD to create convex hulls using the given parameters.", + (ECEPreferences.VHACDParameters.UseSelectedVertices ? "When use only selected vertices is enabled, at least 4 vertices must be selected." : "Select a gameobject with a mesh, or enable child meshes."), + (ECEditor.SelectedGameObject != null && ECEditor.MeshFilters.Count > 0) && (ECEPreferences.VHACDParameters.UseSelectedVertices ? ECEditor.SelectedVertices.Count >= 4 : true))) + { + bool confirmVHACD = false; + // lets use a confirmation dialog if using advanced settings and the resolution is high + if (ECEPreferences.VHACDParameters.resolution >= 512000) + { + if (EditorUtility.DisplayDialog("VHACD", "Resolution for VHACD is set to a very high value. This could potentially take a lot of time, are you sure you wish to generate convex hulls?", "Yes", "Cancel")) + { + confirmVHACD = true; + } + } + else + { + confirmVHACD = true; + } + if (confirmVHACD) + { + _VHACDProgressString = "Initializing"; + _VHACDIsComputing = true; + _VHACDCurrentStep = 0; + _VHACDCheckCount = 0; + ECEPreferences.VHACDParameters.MeshFilters = ECEditor.MeshFilters; + ECEPreferences.VHACDParameters.AttachTo = ECEditor.AttachToObject; + VHACDCurrentParameters = ECEPreferences.VHACDParameters.Clone(); + VHACDCurrentParameters.SaveSuffix = ECEPreferences.SaveConvexHullSuffix; + VHACDCurrentParameters.SavePath = ""; + VHACDPreviewResult = null; + } + } + // Computing progress bar & steps. + if (_VHACDIsComputing) + { + Rect r = EditorGUILayout.BeginVertical(); + // center it a little better. + r.width -= 20; + r.x += 10; + if (VHACDCurrentParameters.SeparateChildMeshes && VHACDCurrentParameters.MeshFilters.Count > 1) + { + //progress bar displays currentMesh / total meshes + EditorGUI.ProgressBar(r, (float)VHACDCurrentParameters.CurrentMeshFilter / VHACDCurrentParameters.MeshFilters.Count, _VHACDProgressString); + } + else + { + // single mesh / combined meshes (1 computation) display steps as progress. + // will really only show halfway full as that's the only one that takes any real amount of time. + EditorGUI.ProgressBar(r, _VHACDCurrentStep / 4.0f, _VHACDProgressString); + } + GUILayout.Space(18); + EditorGUILayout.EndVertical(); + } + } + + /// + /// Sets vhacd to update the preview if it is enabled, or clears preview if it is not. + /// + private void SetVHACDNeedsUpdate(bool fromVertexSelection = false) + { + // essentially a method so we don't have to write #if #endif directives everywhere the preview needs to be updated. + if (ECEPreferences.VHACDPreview) + { + if (VHACDCurrentParameters == null) + { + VHACDCurrentParameters = ECEPreferences.VHACDParameters; + } + if (fromVertexSelection && ECEPreferences.VHACDParameters.UseSelectedVertices) + { + _VHACDUpdatePreview = true; + } + else if (!fromVertexSelection) + { + _VHACDUpdatePreview = true; + } + } + else + { + // clear preview and repaint. + _VHACDUpdatePreview = false; + VHACDPreviewResult = null; + SceneView.RepaintAll(); + } + } + + #endregion + + + /// + /// repaints the last active scene view. + /// + private void RepaintLastActiveSceneView() + { + SceneView.lastActiveSceneView.Repaint(); + } + + /// + /// Focuses the last active scene view if the selected object is not null. + /// + private void FocusSceneView(bool force = false) + { + if (ECEditor.SelectedGameObject != null || force) + { + // focus the last active sceneview automatically. + if (SceneView.lastActiveSceneView != null) + { + SceneView.lastActiveSceneView.Focus(); + } + } + } + + #region BoxAndRaycastSelection + + /// + /// Big method to handle box selection. + /// + /// Does the selection need to be immediately updated? + private void BoxSelect(bool forceUpdate = false) + { + // + if (IsMouseDragged + && ECEditor.SelectedGameObject != null + && SceneView.currentDrawingSceneView == EditorWindow.focusedWindow + && Camera.current != null) + { + // Draw selection box. + Handles.BeginGUI(); + _CurrentDragPosition.x = Mathf.Clamp(_CurrentDragPosition.x, Camera.current.pixelRect.xMin, Camera.current.pixelRect.xMax); + _CurrentDragPosition.y = Mathf.Clamp(_CurrentDragPosition.y, Camera.current.pixelRect.yMin, Camera.current.pixelRect.yMax); + EditorGUI.DrawRect(new Rect(_StartDragPosition, _CurrentDragPosition - _StartDragPosition), _SelectionRectColor); + Handles.EndGUI(); + // we need to draw the UI rect every frame, but should only update the displayed dots occasionally. + // but we also need to draw them constantly. + if ((EditorApplication.timeSinceStartup - _LastSelectionTime > ECEPreferences.RaycastDelayTime && Camera.current != null) || forceUpdate) + { + _LastSelectionTime = EditorApplication.timeSinceStartup; + // use handle utility to get gui point in screen coords instead of my own calculation. + Vector2 endDragM = HandleUtility.GUIPointToScreenPixelCoordinate(_CurrentDragPosition); + Vector2 startDragM = HandleUtility.GUIPointToScreenPixelCoordinate(_StartDragPosition); + + // Limit selection box to scene view pixel rect. + endDragM.x = Mathf.Clamp(endDragM.x, Camera.current.pixelRect.xMin, Camera.current.pixelRect.xMax); + endDragM.y = Mathf.Clamp(endDragM.y, Camera.current.pixelRect.yMin, Camera.current.pixelRect.yMax); + + // Plane to clip verts behind the camera. + Plane planeForward = new Plane(Camera.current.transform.forward, Camera.current.transform.position); + Vector3 currentVertexPos = Vector3.zero; + Vector3 transformedPoint = Vector3.zero; + + for (int i = 0; i < ScreenSpaceVertices.Count; i++) + { + if (i >= ECEditor.MeshFilters.Count && ECEditor.MeshFilters[i] == null) continue; + if (ECEditor.MeshFilters[i] == null || ECEditor.MeshFilters[i].sharedMesh == null) continue; + // all lists are creating by traversing the ECE.MeshFilters list in order. + // so each list's index should be the mesh filter's index. + Transform t = ECEditor.MeshFilters[i].transform; + for (int j = 0; j < ScreenSpaceVertices[i].Count; j++) + { + currentVertexPos = ScreenSpaceVertices[i][j]; + transformedPoint = WorldSpaceVertices[i][j]; + EasyColliderVertex ecv = new EasyColliderVertex(t, LocalSpaceVertices[i][j]); + // if the vertex's screen pos is within the drag area + if ( + ((currentVertexPos.x >= startDragM.x && currentVertexPos.x <= endDragM.x) || (currentVertexPos.x <= startDragM.x && currentVertexPos.x >= endDragM.x)) + && ((currentVertexPos.y >= startDragM.y && currentVertexPos.y <= endDragM.y) || (currentVertexPos.y <= startDragM.y && currentVertexPos.y >= endDragM.y)) + && planeForward.GetSide(transformedPoint) + ) + { + if (ECEPreferences.VertexSnapMethod == VERTEX_SNAP_METHOD.Add) // box plus is held, and box minus is currently overriding. + { + if (!ECEditor.SelectedVerticesSet.Contains(ecv)) // if it's not already selected + { + if (CurrentHoveredVertices.Add(transformedPoint)) // and it's not in our hovered list. + { + CurrentSelectBoxVerts.Add(ecv); + } + } + else if (CurrentHoveredVertices.Remove(transformedPoint)) // otherwise, if its in the box and currently selected + { + CurrentSelectBoxVerts.Remove(ecv); + } + } + else if (ECEPreferences.VertexSnapMethod == VERTEX_SNAP_METHOD.Remove) // box minus is held + { + if (ECEditor.SelectedVerticesSet.Contains(ecv)) // if it's selected + { + if (CurrentHoveredVertices.Add(transformedPoint)) // and it's not in our hovered list + { + CurrentSelectBoxVerts.Add(ecv); + } + } + else if (CurrentHoveredVertices.Remove(transformedPoint)) //otherwise, if it's within the box, and not currently selected. + { + CurrentSelectBoxVerts.Remove(ecv); + } + } + else if (CurrentHoveredVertices.Add(transformedPoint)) // default functionality (not currently hovered, but in box -> mark it at hovered.) + { + CurrentSelectBoxVerts.Add(ecv); + } + } + // remove it if no longer in the box, and in our lists. + else if (CurrentHoveredVertices.Remove(transformedPoint)) + { + // + CurrentSelectBoxVerts.Remove(ecv); + } + } + } + // force update selection displays while dragging a box + UpdateVertexDisplaysHovered(); + } + } + } + + /// + /// Usings a raycast and highlights whatever vertex is the closest. + /// Sets the current hovered filter and current hovered vertex + /// Also selects collider + /// + private void RaycastSelect() + { + // Next update will re-do most of this to use handle-selection instead of the mess it is now. + // clear current hovered vertices + CurrentHoveredVertices.Clear(); + // Use physics scene for the current scene to allow for proper raycasting in the prefab editing scene. + // PhysicsScene physicsScene = PhysicsSceneExtensions.GetPhysicsScene(ECEditor.SelectedGameObject.scene); + Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition); + RaycastHit hit; + + + // use the current physics scene if possible (so that in prefab editing while a scene is open, the scene used is the prefab isolation scene) + // this shouldn't be needed anymore as we're creating a raycastable colliders list, but it is for raycasting for collider removal. + + // collider select just uses a basic raycast. + if (ECEditor.ColliderSelectEnabled) + { + //raycast to find the closest valid collider hit. + RaycastHit[] hits = Physics.RaycastAll(ray.origin, ray.direction, Mathf.Infinity, Physics.AllLayers, QueryTriggerInteraction.Collide); + int hitCount = hits.Length; +#if UNITY_2018_3_OR_NEWER + if (ECEditor.SelectedGameObject.scene.IsValid()) + { + PhysicsScene physicsScene = PhysicsSceneExtensions.GetPhysicsScene(ECEditor.SelectedGameObject.scene); + hits = new RaycastHit[20]; + hitCount = physicsScene.Raycast(ray.origin, ray.direction, hits); + } +#endif + // issues with collider selection.. + UpdateVertexSnapByKeyOrder(); + if (hits != null && hitCount > 0) + { + RaycastHit closest = new RaycastHit(); + closest.distance = Mathf.Infinity; + foreach (RaycastHit hitC in hits) + { + if (hitC.collider == null) continue; + if (hitC.distance < closest.distance) + { + if (ECEditor.IsSelectableCollider(hitC.collider)) + { + if (ECEPreferences.VertexSnapMethod == VERTEX_SNAP_METHOD.Both) + { + _CurrentHoveredCollider = hitC.collider; + closest = hitC; + } + else if ((ECEPreferences.VertexSnapMethod == VERTEX_SNAP_METHOD.Add || Event.current.modifiers == EventModifiers.Control)) + { + if (!ECEditor.SelectedColliders.Contains(hitC.collider)) + { + _CurrentHoveredCollider = hitC.collider; + closest = hitC; + } + } + else if ((ECEPreferences.VertexSnapMethod == VERTEX_SNAP_METHOD.Remove || Event.current.modifiers == EventModifiers.Alt)) + { + if (ECEditor.SelectedColliders.Contains(hitC.collider)) + { + _CurrentHoveredCollider = hitC.collider; + closest = hitC; + } + } + } + } + } + } + else + { + _CurrentHoveredCollider = null; + } + } + // find the closest collider. + else if (ECEditor.VertexSelectEnabled) + { + float minDist = Mathf.Infinity; + Collider closest = null; + // cast against each collider + foreach (Collider col in ECEditor.RaycastableColliders) + { + if (col == null) continue; //... just in case. + if (col.Raycast(ray, out hit, Mathf.Infinity)) + { + float dist = Vector3.Distance(ray.origin, hit.point); + if (dist < minDist) + { + minDist = dist; + closest = col; + } + } + } + if (closest != null && closest.Raycast(ray, out hit, Mathf.Infinity)) + { + // vertex selection + isVertexSelection = true; + float minDistance = Mathf.Infinity; + Transform closestTransform = ECEditor.SelectedGameObject.transform; + Vector3 closestLocalPosition = Vector3.zero; + Vector3 closestNormal = Vector3.zero; + bool isValidSelection = false; + // allows removal from non-vertex points if handled seperately. + if (ECEPreferences.VertexSnapMethod == VERTEX_SNAP_METHOD.Remove) + { + // remove only selected vertices. + foreach (EasyColliderVertex ecv in ECEditor.SelectedVerticesSet) + { + if (ecv.T == null) continue; + Vector3 worldV = ecv.T.TransformPoint(ecv.LocalPosition); + float distance = Vector3.Distance(worldV, hit.point); + if (distance < minDistance) + { + isValidSelection = true; + minDistance = distance; + closestTransform = ecv.T; + closestLocalPosition = ecv.LocalPosition; + closestNormal = ecv.Normal; + } + } + EasyColliderVertex v = new EasyColliderVertex(closestTransform, closestLocalPosition); + if (ECEditor.SelectedNonVerticesSet.Contains(v)) + { + isVertexSelection = false; + } + } + else + { + // current vertex we are checking distance to (for add/remove snaps) + foreach (MeshFilter meshFilter in ECEditor.MeshFilters) + { + if (meshFilter == null || meshFilter.sharedMesh == null) + { + continue; + } + // Get transform and verts of each mesh to make things a little quicker. + Transform t = meshFilter.transform; + // update the current vertex to use the transform of the new mesh. (keep the same vertex thoughout the same meshfilter's vertices but update local position) + EasyColliderVertex currentVertex = new EasyColliderVertex(t, Vector3.zero); + Vector3[] vertices = meshFilter.sharedMesh.vertices; + // Get the closest by checking the distance. + // convert world hit point to local hit point for each meshfilter's transform. + Vector3 localHit = t.InverseTransformPoint(hit.point); + for (int i = 0; i < vertices.Length; i++) + { + float distance = Vector3.Distance(vertices[i], localHit); + if (distance < minDistance) + { + // default method, just closest distance add or remove + if (ECEPreferences.VertexSnapMethod == VERTEX_SNAP_METHOD.Both && Event.current.modifiers != EventModifiers.Alt && Event.current.modifiers != EventModifiers.Control) + { + isValidSelection = true; + minDistance = distance; + closestTransform = t; + closestLocalPosition = vertices[i]; + } + else + { + // update the current vertex local position + currentVertex.LocalPosition = vertices[i]; + // if we're adding and it's not already selected + if ((ECEPreferences.VertexSnapMethod == VERTEX_SNAP_METHOD.Add || Event.current.modifiers == EventModifiers.Control) && !ECEditor.SelectedVerticesSet.Contains(currentVertex)) + { + isValidSelection = true; + minDistance = distance; + closestTransform = t; + closestLocalPosition = vertices[i]; + } + } + } + } + } + } + if (ECEPreferences.VertexSnapMethod == VERTEX_SNAP_METHOD.Both) + { + foreach (EasyColliderVertex v in ECEditor.SelectedNonVerticesSet) + { + Vector3 wp = v.T.TransformPoint(v.LocalPosition); + float distance = Vector3.Distance(wp, hit.point); + if (distance < minDistance) + { + minDistance = distance; + closestLocalPosition = v.LocalPosition; + closestTransform = v.T; + isValidSelection = true; + isVertexSelection = false; + } + } + } + + // if the closest changed from the one we already have. + if (closestTransform != null && isValidSelection) + { + CurrentHoveredVertices.Add(closestTransform.TransformPoint(closestLocalPosition)); + _CurrentHoveredPosition = closestLocalPosition; + _CurrentHoveredTransform = closestTransform; + } + else + { + // no valid selection. + _CurrentHoveredTransform = null; + } + _CurrentHoveredPointTransform = hit.transform; + if (_CurrentHoveredPointTransform != null && isValidSelection) + { + // with point selection, you can more easily select points that aren't on the selected or child meshes + _CurrentHoveredPoint = _CurrentHoveredPointTransform.InverseTransformPoint(hit.point); + CurrentHoveredVertices.Add(hit.point); + } + } + else if (ECEditor.VertexSelectEnabled && _CurrentHoveredTransform != null) + { + // clear hovered display if we're not over anything. + CurrentHoveredVertices.Remove(_CurrentHoveredTransform.TransformPoint(_CurrentHoveredPosition)); + _CurrentHoveredTransform = null; + // need to clear this transform as well, as it controls the arbitrary selected points. + _CurrentHoveredPointTransform = null; + } + if (ECEditor.VertexSelectEnabled) + { + // if we're not collider selecting, update the vertex display + UpdateVertexDisplaysHovered(); + } + } + } + + #endregion + + /// + /// Merges the selected colliders into the the mergeTo type. + /// + private void MergeColliders(CREATE_COLLIDER_TYPE mergeTo, string undoString) + { + Undo.RegisterCompleteObjectUndo(ECEditor.AttachToObject, undoString); + int group = Undo.GetCurrentGroup(); + Undo.RegisterCompleteObjectUndo(ECEditor, undoString); + ECEditor.MergeSelectedColliders(mergeTo, ECEPreferences.RemoveMergedColliders); + Undo.CollapseUndoOperations(group); + FocusSceneView(); + } + + /// + /// Registers an undo and selects a collider + /// + /// Collider to select + private void SelectCollider(Collider collider) + { + Undo.RegisterCompleteObjectUndo(ECEditor, "Select Collider"); + int group = Undo.GetCurrentGroup(); + ECEditor.SelectCollider(collider); + Undo.CollapseUndoOperations(group); + if (collider == _CurrentHoveredCollider) + { + _CurrentHoveredCollider = null; + } + this.Repaint(); + UpdateColliderDisplays(); + } + + + /// + /// Registers an undo and selects a vertex. + /// + /// transform of vertex' mesh filter to select + /// local position of vertex + private void SelectVertex(Transform transform, Vector3 localPosition, bool isVertexSelection) + { + if (transform != null) + { + // Vertex selection by screen distance. + Undo.RegisterCompleteObjectUndo(ECEditor, "Select Vertex"); + int group = Undo.GetCurrentGroup(); + ECEditor.SelectVertex(new EasyColliderVertex(transform, localPosition), isVertexSelection); + Undo.CollapseUndoOperations(group); + this.Repaint(); + // update display and vhacd if needed. + UpdateVertexDisplays(); + SetVHACDNeedsUpdate(true); + } + } + + /// + /// Selects the vertices that are currently in the displaced drag selection box. + /// + private void SelectVerticesInBox() + { + // Done dragging, select everything in the box. + Undo.RegisterCompleteObjectUndo(ECEditor, "Select Vertices"); + int group = Undo.GetCurrentGroup(); + ECEditor.SelectVertices(CurrentSelectBoxVerts); + // Clear sets. + CurrentHoveredVertices = new HashSet(); + CurrentSelectBoxVerts = new HashSet(); + Undo.CollapseUndoOperations(group); + // repaint so buttons appear for vertex selection + UpdateVertexDisplays(); + // updates VHACD preview + SetVHACDNeedsUpdate(true); + this.Repaint(); + } + + /// + /// Adds or Removes tips from CurrentTips based on whether it should be displayed or not. + /// + /// Should this tip be displayed? + /// String of tip to display. + /// + private bool UpdateTip(bool displayTip, string tip) + { + if (displayTip) + { + if (!CurrentTips.Contains(tip)) + { + CurrentTips.Add(tip); + return true; + } + return false; + } + else + { + return CurrentTips.Remove(tip); + } + } + + + /// + /// Last time the sceneview was focused and tips were updated. + /// + double SceneViewFocused = 0.0f; + /// + /// Length between last scene-view focus for the tip to be displayed. + /// + double MaxFocusTooltipTimer = 1.0; + + /// + /// A simple timer used to see if the tip that is based around having the wrong focused window should be displayed. + /// Should prevent the tip from flickering in and out. + /// + /// + public bool ShouldDisplayWindowFocus() + { + if (ECEditor.VertexSelectEnabled && (EditorWindow.focusedWindow != SceneView.lastActiveSceneView)) + { + if (EditorApplication.timeSinceStartup - SceneViewFocused > MaxFocusTooltipTimer) + { + return true; + } + } + else if (EditorWindow.focusedWindow == SceneView.lastActiveSceneView) + { + SceneViewFocused = EditorApplication.timeSinceStartup; + } + return false; + } + + /// + /// Updates all the tips in to display using CurrentTips list. + /// Dr + private void UpdateTips() + { + int preUpdateCount = CurrentTips.Count; + if (ECEditor.SelectedGameObject != null) + { + UpdateTip(ECEditor.VertexSelectEnabled && !ECEPreferences.UseMouseClickSelection, "Use the " + ECEPreferences.VertSelectKeyCode + " key to select the highlighted vertex, and the " + ECEPreferences.PointSelectKeyCode + " key to select the point under the mouse."); + UpdateTip(ECEditor.VertexSelectEnabled && !ECEPreferences.UseMouseClickSelection, EasyColliderTips.TRY_MOUSE_CONTROL); + UpdateTip(ECEditor.VertexSelectEnabled && ECEPreferences.UseMouseClickSelection, EasyColliderTips.NEW_MOUSE_CONTROL); + // this is displayed as a warning by the selected gameobject field. + // UpdateTip(ECEditor.SelectedGameObject != null && ECEditor.MeshFilters.Count == 0, EasyColliderTips.NO_MESH_FILTER_FOUND); + UpdateTip(ShouldDisplayWindowFocus(), EasyColliderTips.WRONG_FOCUSED_WINDOW); + UpdateTip(ECEditor.VertexSelectEnabled && EditorApplication.isPlayingOrWillChangePlaymode, EasyColliderTips.IN_PLAY_MODE); + // UpdateTip(ECEditor.VertexSelectEnabled && ECEPreferences.ForceFocusScene, EasyColliderTips.FORCED_FOCUSED_WINDOW); + // UpdateTip(ECEditor.VertexSelectEnabled && _EditPreferences && ECEPreferences.ForceFocusScene, EasyColliderTips.EDIT_PREFS_FORCED_FOCUSED); + // https://docs.unity3d.com/Manual/SL-ShaderCompileTargets.html, 4.5+ has compute shaders. + UpdateTip(SystemInfo.graphicsShaderLevel < 45, EasyColliderTips.COMPUTE_SHADER_TIP); + UpdateTip(ECEPreferences.PreviewColliderType == CREATE_COLLIDER_TYPE.ROTATED_BOX, EasyColliderTips.ROTATED_BOX_COLLIDER_TIP); + UpdateTip(ECEPreferences.PreviewColliderType == CREATE_COLLIDER_TYPE.ROTATED_CAPSULE, EasyColliderTips.ROTATED_CAPSULE_COLLIDER_TIP); + UpdateTip(CurrentTab == ECE_WINDOW_TAB.Creation, EasyColliderTips.COLLIDER_CREATION_SHORTCUTS_1); + UpdateTip(CurrentTab == ECE_WINDOW_TAB.AutoSkinned, EasyColliderTips.AUTO_SKILLED_CONTROL_PARAMETERS); + } + else if (CurrentTips.Count > 0) + { + // Clear tips if we dont have anything selected. + CurrentTips = new List(); + } + // Repaint the Editor window if tips have changed. + if (preUpdateCount != CurrentTips.Count) + { + Repaint(); + } + } + + + /// + /// Draws the documentation tip, which opens a link to the pdf. + /// + private void DrawDocumentationTip() + { + GUIStyle tipStyle = new GUIStyle(GUI.skin.label); + tipStyle.wordWrap = true; + tipStyle.alignment = TextAnchor.UpperLeft; + tipStyle.fontStyle = FontStyle.Normal; + tipStyle.fontSize = 12; + tipStyle.alignment = TextAnchor.MiddleCenter; + tipStyle.richText = true; + + //link color for dark mode. + string linkColor = EditorGUIUtility.isProSkin ? "#0388fc" : "blue"; + if (GUILayout.Button("Be sure to check out the Quick Start Guide in the documentation.", tipStyle)) + { + UnityEngine.Object doc = FindDocumentation(); + if (doc != null) + { + AssetDatabase.OpenAsset(doc); + } + } + } + + private UnityEngine.Object FindDocumentation() + { + string[] doc = AssetDatabase.FindAssets("EasyColliderEditorDocumentation"); + if (doc.Length > 0) + { + string acp = AssetDatabase.GUIDToAssetPath(doc[0]); + UnityEngine.Object o = AssetDatabase.LoadMainAssetAtPath(acp); + return o; + } + + return null; + } + + /// + /// Draws colliders that are currently hovered or selected. + /// + private void UpdateColliderDisplays() + { + // draw selected colliders. + foreach (Collider col in ECEditor.SelectedColliders) + { + EasyColliderDraw.DrawCollider(col, ECEPreferences.SelectedVertColour); + } + + // draw hovered as either the overlap or hover color. + if (ECEditor.IsColliderSelected(_CurrentHoveredCollider)) + { + EasyColliderDraw.DrawCollider(_CurrentHoveredCollider, ECEPreferences.OverlapSelectedVertColour); + } + else + { + EasyColliderDraw.DrawCollider(_CurrentHoveredCollider, ECEPreferences.HoverVertColour); + } + SceneView.RepaintAll(); + } + + /// + /// Tells the previewer that an update may be needed. + /// + /// if true, forces the previewer to update even + private void UpdatePreview(bool force = false) + { + if (ECEPreferences.PreviewEnabled) + { + ECEPreferences.CurrentWindowTab = CurrentTab; + if (CurrentTab == ECE_WINDOW_TAB.Creation) + { + // modified so that preview aligns correctly with button availability. + if (HasMinimumVerticesForCollider(ECEPreferences.PreviewColliderType)) + { + ECPreviewer.UpdatePreview(ECEditor, ECEPreferences, force); + } + else + { + ECPreviewer.ClearPreview(); + } + } + else + { + ECPreviewer.UpdatePreview(ECEditor, ECEPreferences, force); + } + + } + else + { + ECPreviewer.ClearPreview(); + + } + // sometimes not updating? + if (force) { SceneView.RepaintAll(); } + } + + + /// + /// Updates the gizmos or shaders selected, hover, and overlap vertices. + /// + private void UpdateVertexDisplays() + { + // Update Gizmos + if (ECEditor.Gizmos != null) + { + ECEditor.Gizmos.SetSelectedVertices(ECEditor.GetWorldVertices()); + ECEditor.Gizmos.HoveredVertexPositions = CurrentHoveredVertices; + if (ECEPreferences.DisplayAllVertices) + { + ECEditor.Gizmos.DisplayVertexPositions = ECEditor.GetAllWorldMeshVertices(); + } + } + // Update Compute / Shader script. + if (ECEditor.Compute != null) + { + ECEditor.Compute.UpdateSelectedBuffer(ECEditor.GetWorldVertices()); + ECEditor.Compute.UpdateOverlapHoveredBuffer(CurrentHoveredVertices); + //fixes issue where display all vertices was disappearing after undo + // (The compute buffer would become invalid after an undo, so needs to be recreated) + if (ECEPreferences.DisplayAllVertices) + { + ECEditor.Compute.SetDisplayAllBuffer(ECEditor.GetAllWorldMeshVertices()); + } + } + SceneView.RepaintAll(); + } + + /// + /// Updates just the hovered vertices + /// + private void UpdateVertexDisplaysHovered() + { + if (ECEditor.Gizmos != null) + { + ECEditor.Gizmos.HoveredVertexPositions = CurrentHoveredVertices; + } + // Update Compute / Shader script. + if (ECEditor.Compute != null) + { + ECEditor.Compute.UpdateOverlapHoveredBuffer(CurrentHoveredVertices); + } + SceneView.RepaintAll(); + } + + /// + /// Updates the world space, local space, and screen space vertex lists from the valid selectable vertices. + /// + private void UpdateWorldScreenLocalSpaceVertexLists() + { + // Create lists if null + if (WorldSpaceVertices == null) { WorldSpaceVertices = new List>(); } + if (ScreenSpaceVertices == null) { ScreenSpaceVertices = new List>(); } + if (LocalSpaceVertices == null) { LocalSpaceVertices = new List>(); } + // clear the lists + WorldSpaceVertices.Clear(); + ScreenSpaceVertices.Clear(); + LocalSpaceVertices.Clear(); + Vector3[] verts = new Vector3[0]; + Transform t; + Vector3 transformedPoint; + HashSet currentSelSet = new HashSet(ECEditor.SelectedVerticesSet); + for (int i = 0; i < ECEditor.MeshFilters.Count; i++) + { + + // Create a list for each mesh filter (before checking for null, otherwise i is wrong) + WorldSpaceVertices.Add(new List()); + ScreenSpaceVertices.Add(new List()); + LocalSpaceVertices.Add(new List()); + if (ECEditor.MeshFilters[i] == null || ECEditor.MeshFilters[i].sharedMesh == null) continue; + + if (Camera.current != null) // is called from OnGUI as well when selected gameobject changes. + { + // go through all the points + verts = ECEditor.MeshFilters[i].sharedMesh.vertices; + t = ECEditor.MeshFilters[i].transform; + for (int j = 0; j < verts.Length; j++) + { + + // transform and add to the list + transformedPoint = t.TransformPoint(verts[j]); + WorldSpaceVertices[i].Add(transformedPoint); + LocalSpaceVertices[i].Add(verts[j]); + ScreenSpaceVertices[i].Add(Camera.current.WorldToScreenPoint(transformedPoint)); + } + // go through the selected points as well, (this includes arbitrary non-vertex points) + // TODO: keep track of arbitrary selected points seperately so this isn't needed...... + HashSet toRemoveSet = new HashSet(); + foreach (EasyColliderVertex ecv in currentSelSet) + { + if (ecv.T == t) + { + transformedPoint = t.TransformPoint(ecv.LocalPosition); + WorldSpaceVertices[i].Add(transformedPoint); + LocalSpaceVertices[i].Add(ecv.LocalPosition); + ScreenSpaceVertices[i].Add(Camera.current.WorldToScreenPoint(transformedPoint)); + toRemoveSet.Add(ecv); + } + } + currentSelSet.ExceptWith(toRemoveSet); + } + } + } + + float savedLabelWidth; + void SetLabelWidth(float width, bool saveCurrent = true) + { + if (saveCurrent) + { + savedLabelWidth = EditorGUIUtility.labelWidth; + } + EditorGUIUtility.labelWidth = width; + } + void RestoreLabelWidth() + { + EditorGUIUtility.labelWidth = savedLabelWidth; + } + } +} +#endif \ No newline at end of file diff --git a/Assets/EasyColliderEditor/Scripts/EasyColliderWindow.cs.meta b/Assets/EasyColliderEditor/Scripts/EasyColliderWindow.cs.meta new file mode 100644 index 00000000..a96c8d76 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/EasyColliderWindow.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 8aba5803d0eee804792b9405ce7990cf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/EasyColliderWindow.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/Interfaces.meta b/Assets/EasyColliderEditor/Scripts/Interfaces.meta new file mode 100644 index 00000000..0f9d197b --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Interfaces.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ac9df4839a2693149a285fc07e42c21f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/EasyColliderEditor/Scripts/Interfaces/IEasyColliderPostProcessor.cs b/Assets/EasyColliderEditor/Scripts/Interfaces/IEasyColliderPostProcessor.cs new file mode 100644 index 00000000..c6d447a6 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Interfaces/IEasyColliderPostProcessor.cs @@ -0,0 +1,39 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +namespace ECE +{ + public interface IEasyColliderPostProcessor + { + /// + /// post processes a box collider, properties orientation indiciates if it's a rotated collider. + /// + /// + /// + void PostProcessCollider(BoxCollider boxCollider, EasyColliderProperties properties); + + /// + /// post processes a capsule collider, properties orientation indiciates if it's a rotated collider. + /// + /// + /// + void PostProcessCollider(CapsuleCollider capsuleCollider, EasyColliderProperties properties); + + /// + /// post processes a mesh collider. cylinder colliders are mesh colliders as well. + /// + /// + /// + void PostProcessCollider(MeshCollider meshCollider, EasyColliderProperties properties); + + + /// + /// post processes a sphere collider + /// + /// + /// + void PostProcessCollider(SphereCollider sphereCollider, EasyColliderProperties properties); + + + } +} \ No newline at end of file diff --git a/Assets/EasyColliderEditor/Scripts/Interfaces/IEasyColliderPostProcessor.cs.meta b/Assets/EasyColliderEditor/Scripts/Interfaces/IEasyColliderPostProcessor.cs.meta new file mode 100644 index 00000000..d6fd8f34 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Interfaces/IEasyColliderPostProcessor.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: b6cf4bc0f3d036f46a48313c8a796bed +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/Interfaces/IEasyColliderPostProcessor.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/Plugins.meta b/Assets/EasyColliderEditor/Scripts/Plugins.meta new file mode 100644 index 00000000..ccefa975 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Plugins.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: a8a38e163b87eb748aebb1b989f8503b +folderAsset: yes +timeCreated: 1591980098 +licenseType: Store +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/EasyColliderEditor/Scripts/Plugins/ECE_VHACD.dll b/Assets/EasyColliderEditor/Scripts/Plugins/ECE_VHACD.dll new file mode 100644 index 00000000..58e15077 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Plugins/ECE_VHACD.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5cae8eed3355d6749c3d3e18157fefcc6e07b055c3b93179b0c439c112f17cb2 +size 163328 diff --git a/Assets/EasyColliderEditor/Scripts/Plugins/ECE_VHACD.dll.meta b/Assets/EasyColliderEditor/Scripts/Plugins/ECE_VHACD.dll.meta new file mode 100644 index 00000000..da1d0867 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Plugins/ECE_VHACD.dll.meta @@ -0,0 +1,96 @@ +fileFormatVersion: 2 +guid: 68a88bb9c40937341bf6536bbe774f0d +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux: 1 + Exclude Linux64: 1 + Exclude LinuxUniversal: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: Windows + - first: + Facebook: Win + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + Facebook: Win64 + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + Standalone: Linux + second: + enabled: 0 + settings: + CPU: x86 + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + Standalone: LinuxUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/Plugins/ECE_VHACD.dll + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/Plugins/Linux.meta b/Assets/EasyColliderEditor/Scripts/Plugins/Linux.meta new file mode 100644 index 00000000..586de8e6 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Plugins/Linux.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d834607d4f50eef45918d26ac1d3e340 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/EasyColliderEditor/Scripts/Plugins/Linux/ECE_VHACD.so b/Assets/EasyColliderEditor/Scripts/Plugins/Linux/ECE_VHACD.so new file mode 100644 index 00000000..cc85857b Binary files /dev/null and b/Assets/EasyColliderEditor/Scripts/Plugins/Linux/ECE_VHACD.so differ diff --git a/Assets/EasyColliderEditor/Scripts/Plugins/Linux/ECE_VHACD.so.meta b/Assets/EasyColliderEditor/Scripts/Plugins/Linux/ECE_VHACD.so.meta new file mode 100644 index 00000000..3fa35547 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Plugins/Linux/ECE_VHACD.so.meta @@ -0,0 +1,70 @@ +fileFormatVersion: 2 +guid: 253ee65f4209bc24ab1c970ff13bf752 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux64: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: Linux + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: None + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/Plugins/Linux/ECE_VHACD.so + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/Plugins/OSX.meta b/Assets/EasyColliderEditor/Scripts/Plugins/OSX.meta new file mode 100644 index 00000000..1854282e --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Plugins/OSX.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 88f8f3ea0a40624418d9c27ab616b7ea +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle.meta b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle.meta new file mode 100644 index 00000000..0f0eed5d --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle.meta @@ -0,0 +1,74 @@ +fileFormatVersion: 2 +guid: a9491b77ec748994585680ed2c3066b5 +folderAsset: yes +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux64: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + - first: + : OSXIntel + second: + enabled: 1 + settings: {} + - first: + : OSXIntel64 + second: + enabled: 1 + settings: {} + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: OSX + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: None + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents.meta b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents.meta new file mode 100644 index 00000000..3317a207 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6a8585019d15a9a46bcd1c5a4672a0dc +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/Info.plist b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/Info.plist new file mode 100644 index 00000000..bd1269d6 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/Info.plist @@ -0,0 +1,48 @@ + + + + + BuildMachineOSBuild + 22G313 + CFBundleDevelopmentRegion + en + CFBundleExecutable + ECE_VHACD + CFBundleIdentifier + com.pmurph0305.vhacd + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ECE_VHACD + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSupportedPlatforms + + MacOSX + + CFBundleVersion + 1 + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + + DTPlatformName + macosx + DTPlatformVersion + 14.0 + DTSDKBuild + 23A334 + DTSDKName + macosx14.0 + DTXcode + 1501 + DTXcodeBuild + 15A507 + LSMinimumSystemVersion + 10.14.6 + NSHumanReadableCopyright + Patrick Murphy + + diff --git a/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/MacOS.meta b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/MacOS.meta new file mode 100644 index 00000000..8b813a85 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/MacOS.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a0f15c4d5e14a654984875f8b9c256e2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/MacOS/ECE_VHACD b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/MacOS/ECE_VHACD new file mode 100644 index 00000000..11daebe6 Binary files /dev/null and b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/MacOS/ECE_VHACD differ diff --git a/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/Resources.meta b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/Resources.meta new file mode 100644 index 00000000..adf3758a --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f1b2f199639e70343a1ee88ed11361b5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/Resources/FloatMath.inl b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/Resources/FloatMath.inl new file mode 100644 index 00000000..0316061f --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/Resources/FloatMath.inl @@ -0,0 +1,5437 @@ +// a set of routines that let you do common 3d math +// operations without any vector, matrix, or quaternion +// classes or templates. +// +// a vector (or point) is a 'float *' to 3 floating point numbers. +// a matrix is a 'float *' to an array of 16 floating point numbers representing a 4x4 transformation matrix compatible +// with D3D or OGL a quaternion is a 'float *' to 4 floats representing a quaternion x,y,z,w +// + +#ifdef _MSC_VER +# pragma warning(disable : 4996) +#endif + +namespace FLOAT_MATH +{ + +void fm_inverseRT(const REAL matrix[16], const REAL pos[3], REAL t[3]) // inverse rotate translate the point. +{ + + REAL _x = pos[0] - matrix[3 * 4 + 0]; + REAL _y = pos[1] - matrix[3 * 4 + 1]; + REAL _z = pos[2] - matrix[3 * 4 + 2]; + + // Multiply inverse-translated source vector by inverted rotation transform + + t[0] = (matrix[0 * 4 + 0] * _x) + (matrix[0 * 4 + 1] * _y) + (matrix[0 * 4 + 2] * _z); + t[1] = (matrix[1 * 4 + 0] * _x) + (matrix[1 * 4 + 1] * _y) + (matrix[1 * 4 + 2] * _z); + t[2] = (matrix[2 * 4 + 0] * _x) + (matrix[2 * 4 + 1] * _y) + (matrix[2 * 4 + 2] * _z); +} + +REAL fm_getDeterminant(const REAL matrix[16]) +{ + REAL tempv[3]; + REAL p0[3]; + REAL p1[3]; + REAL p2[3]; + + + p0[0] = matrix[0 * 4 + 0]; + p0[1] = matrix[0 * 4 + 1]; + p0[2] = matrix[0 * 4 + 2]; + + p1[0] = matrix[1 * 4 + 0]; + p1[1] = matrix[1 * 4 + 1]; + p1[2] = matrix[1 * 4 + 2]; + + p2[0] = matrix[2 * 4 + 0]; + p2[1] = matrix[2 * 4 + 1]; + p2[2] = matrix[2 * 4 + 2]; + + fm_cross(tempv, p1, p2); + + return fm_dot(p0, tempv); +} + +REAL fm_squared(REAL x) +{ + return x * x; +}; + +void fm_decomposeTransform(const REAL local_transform[16], REAL trans[3], REAL rot[4], REAL scale[3]) +{ + + trans[0] = local_transform[12]; + trans[1] = local_transform[13]; + trans[2] = local_transform[14]; + + scale[0] = (REAL)sqrt(fm_squared(local_transform[0 * 4 + 0]) + fm_squared(local_transform[0 * 4 + 1]) + + fm_squared(local_transform[0 * 4 + 2])); + scale[1] = (REAL)sqrt(fm_squared(local_transform[1 * 4 + 0]) + fm_squared(local_transform[1 * 4 + 1]) + + fm_squared(local_transform[1 * 4 + 2])); + scale[2] = (REAL)sqrt(fm_squared(local_transform[2 * 4 + 0]) + fm_squared(local_transform[2 * 4 + 1]) + + fm_squared(local_transform[2 * 4 + 2])); + + REAL m[16]; + memcpy(m, local_transform, sizeof(REAL) * 16); + + REAL sx = 1.0f / scale[0]; + REAL sy = 1.0f / scale[1]; + REAL sz = 1.0f / scale[2]; + + m[0 * 4 + 0] *= sx; + m[0 * 4 + 1] *= sx; + m[0 * 4 + 2] *= sx; + + m[1 * 4 + 0] *= sy; + m[1 * 4 + 1] *= sy; + m[1 * 4 + 2] *= sy; + + m[2 * 4 + 0] *= sz; + m[2 * 4 + 1] *= sz; + m[2 * 4 + 2] *= sz; + + fm_matrixToQuat(m, rot); +} + +void fm_getSubMatrix(int32_t ki, int32_t kj, REAL pDst[16], const REAL matrix[16]) +{ + int32_t row, col; + int32_t dstCol = 0, dstRow = 0; + + for (col = 0; col < 4; col++) + { + if (col == kj) + { + continue; + } + for (dstRow = 0, row = 0; row < 4; row++) + { + if (row == ki) + { + continue; + } + pDst[dstCol * 4 + dstRow] = matrix[col * 4 + row]; + dstRow++; + } + dstCol++; + } +} + +void fm_inverseTransform(const REAL matrix[16], REAL inverse_matrix[16]) +{ + REAL determinant = fm_getDeterminant(matrix); + determinant = 1.0f / determinant; + for (int32_t i = 0; i < 4; i++) + { + for (int32_t j = 0; j < 4; j++) + { + int32_t sign = 1 - ((i + j) % 2) * 2; + REAL subMat[16]; + fm_identity(subMat); + fm_getSubMatrix(i, j, subMat, matrix); + REAL subDeterminant = fm_getDeterminant(subMat); + inverse_matrix[i * 4 + j] = (subDeterminant * sign) * determinant; + } + } +} + +void fm_identity(REAL matrix[16]) // set 4x4 matrix to identity. +{ + matrix[0 * 4 + 0] = 1; + matrix[1 * 4 + 1] = 1; + matrix[2 * 4 + 2] = 1; + matrix[3 * 4 + 3] = 1; + + matrix[1 * 4 + 0] = 0; + matrix[2 * 4 + 0] = 0; + matrix[3 * 4 + 0] = 0; + + matrix[0 * 4 + 1] = 0; + matrix[2 * 4 + 1] = 0; + matrix[3 * 4 + 1] = 0; + + matrix[0 * 4 + 2] = 0; + matrix[1 * 4 + 2] = 0; + matrix[3 * 4 + 2] = 0; + + matrix[0 * 4 + 3] = 0; + matrix[1 * 4 + 3] = 0; + matrix[2 * 4 + 3] = 0; +} + +void fm_quatToEuler(const REAL quat[4], REAL& ax, REAL& ay, REAL& az) +{ + REAL x = quat[0]; + REAL y = quat[1]; + REAL z = quat[2]; + REAL w = quat[3]; + + REAL sint = (2.0f * w * y) - (2.0f * x * z); + REAL cost_temp = 1.0f - (sint * sint); + REAL cost = 0; + + if ((REAL)fabs(cost_temp) > 0.001f) + { + cost = (REAL)sqrt(cost_temp); + } + + REAL sinv, cosv, sinf, cosf; + if ((REAL)fabs(cost) > 0.001f) + { + cost = 1.0f / cost; + sinv = ((2.0f * y * z) + (2.0f * w * x)) * cost; + cosv = (1.0f - (2.0f * x * x) - (2.0f * y * y)) * cost; + sinf = ((2.0f * x * y) + (2.0f * w * z)) * cost; + cosf = (1.0f - (2.0f * y * y) - (2.0f * z * z)) * cost; + } + else + { + sinv = (2.0f * w * x) - (2.0f * y * z); + cosv = 1.0f - (2.0f * x * x) - (2.0f * z * z); + sinf = 0; + cosf = 1.0f; + } + + // compute output rotations + ax = (REAL)atan2(sinv, cosv); + ay = (REAL)atan2(sint, cost); + az = (REAL)atan2(sinf, cosf); +} + +void fm_eulerToMatrix(REAL ax, REAL ay, REAL az, REAL* matrix) // convert euler (in radians) to a dest 4x4 matrix + // (translation set to zero) +{ + REAL quat[4]; + fm_eulerToQuat(ax, ay, az, quat); + fm_quatToMatrix(quat, matrix); +} + +void fm_getAABB(uint32_t vcount, const REAL* points, uint32_t pstride, REAL* bmin, REAL* bmax) +{ + + const uint8_t* source = (const uint8_t*)points; + + bmin[0] = points[0]; + bmin[1] = points[1]; + bmin[2] = points[2]; + + bmax[0] = points[0]; + bmax[1] = points[1]; + bmax[2] = points[2]; + + + for (uint32_t i = 1; i < vcount; i++) + { + source += pstride; + const REAL* p = (const REAL*)source; + + if (p[0] < bmin[0]) + bmin[0] = p[0]; + if (p[1] < bmin[1]) + bmin[1] = p[1]; + if (p[2] < bmin[2]) + bmin[2] = p[2]; + + if (p[0] > bmax[0]) + bmax[0] = p[0]; + if (p[1] > bmax[1]) + bmax[1] = p[1]; + if (p[2] > bmax[2]) + bmax[2] = p[2]; + } +} + +void fm_eulerToQuat(const REAL* euler, REAL* quat) // convert euler angles to quaternion. +{ + fm_eulerToQuat(euler[0], euler[1], euler[2], quat); +} + +void fm_eulerToQuat(REAL roll, REAL pitch, REAL yaw, REAL* quat) // convert euler angles to quaternion. +{ + roll *= 0.5f; + pitch *= 0.5f; + yaw *= 0.5f; + + REAL cr = (REAL)cos(roll); + REAL cp = (REAL)cos(pitch); + REAL cy = (REAL)cos(yaw); + + REAL sr = (REAL)sin(roll); + REAL sp = (REAL)sin(pitch); + REAL sy = (REAL)sin(yaw); + + REAL cpcy = cp * cy; + REAL spsy = sp * sy; + REAL spcy = sp * cy; + REAL cpsy = cp * sy; + + quat[0] = (sr * cpcy - cr * spsy); + quat[1] = (cr * spcy + sr * cpsy); + quat[2] = (cr * cpsy - sr * spcy); + quat[3] = cr * cpcy + sr * spsy; +} + +void fm_quatToMatrix(const REAL* quat, REAL* matrix) // convert quaterinion rotation to matrix, zeros out the + // translation component. +{ + + REAL xx = quat[0] * quat[0]; + REAL yy = quat[1] * quat[1]; + REAL zz = quat[2] * quat[2]; + REAL xy = quat[0] * quat[1]; + REAL xz = quat[0] * quat[2]; + REAL yz = quat[1] * quat[2]; + REAL wx = quat[3] * quat[0]; + REAL wy = quat[3] * quat[1]; + REAL wz = quat[3] * quat[2]; + + matrix[0 * 4 + 0] = 1 - 2 * (yy + zz); + matrix[1 * 4 + 0] = 2 * (xy - wz); + matrix[2 * 4 + 0] = 2 * (xz + wy); + + matrix[0 * 4 + 1] = 2 * (xy + wz); + matrix[1 * 4 + 1] = 1 - 2 * (xx + zz); + matrix[2 * 4 + 1] = 2 * (yz - wx); + + matrix[0 * 4 + 2] = 2 * (xz - wy); + matrix[1 * 4 + 2] = 2 * (yz + wx); + matrix[2 * 4 + 2] = 1 - 2 * (xx + yy); + + matrix[3 * 4 + 0] = matrix[3 * 4 + 1] = matrix[3 * 4 + 2] = (REAL)0.0f; + matrix[0 * 4 + 3] = matrix[1 * 4 + 3] = matrix[2 * 4 + 3] = (REAL)0.0f; + matrix[3 * 4 + 3] = (REAL)1.0f; +} + + +void fm_quatRotate(const REAL* quat, const REAL* v, REAL* r) // rotate a vector directly by a quaternion. +{ + REAL left[4]; + + left[0] = quat[3] * v[0] + quat[1] * v[2] - v[1] * quat[2]; + left[1] = quat[3] * v[1] + quat[2] * v[0] - v[2] * quat[0]; + left[2] = quat[3] * v[2] + quat[0] * v[1] - v[0] * quat[1]; + left[3] = -quat[0] * v[0] - quat[1] * v[1] - quat[2] * v[2]; + + r[0] = (left[3] * -quat[0]) + (quat[3] * left[0]) + (left[1] * -quat[2]) - (-quat[1] * left[2]); + r[1] = (left[3] * -quat[1]) + (quat[3] * left[1]) + (left[2] * -quat[0]) - (-quat[2] * left[0]); + r[2] = (left[3] * -quat[2]) + (quat[3] * left[2]) + (left[0] * -quat[1]) - (-quat[0] * left[1]); +} + + +void fm_getTranslation(const REAL* matrix, REAL* t) +{ + t[0] = matrix[3 * 4 + 0]; + t[1] = matrix[3 * 4 + 1]; + t[2] = matrix[3 * 4 + 2]; +} + +void fm_matrixToQuat(const REAL* matrix, REAL* quat) // convert the 3x3 portion of a 4x4 matrix into a quaterion as + // x,y,z,w +{ + + REAL tr = matrix[0 * 4 + 0] + matrix[1 * 4 + 1] + matrix[2 * 4 + 2]; + + // check the diagonal + + if (tr > 0.0f) + { + REAL s = (REAL)sqrt((double)(tr + 1.0f)); + quat[3] = s * 0.5f; + s = 0.5f / s; + quat[0] = (matrix[1 * 4 + 2] - matrix[2 * 4 + 1]) * s; + quat[1] = (matrix[2 * 4 + 0] - matrix[0 * 4 + 2]) * s; + quat[2] = (matrix[0 * 4 + 1] - matrix[1 * 4 + 0]) * s; + } + else + { + // diagonal is negative + int32_t nxt[3] = { 1, 2, 0 }; + REAL qa[4]; + + int32_t i = 0; + + if (matrix[1 * 4 + 1] > matrix[0 * 4 + 0]) + i = 1; + if (matrix[2 * 4 + 2] > matrix[i * 4 + i]) + i = 2; + + int32_t j = nxt[i]; + int32_t k = nxt[j]; + + REAL s = (REAL)sqrt(((matrix[i * 4 + i] - (matrix[j * 4 + j] + matrix[k * 4 + k])) + 1.0f)); + + qa[i] = s * 0.5f; + + if (s != 0.0f) + s = 0.5f / s; + + qa[3] = (matrix[j * 4 + k] - matrix[k * 4 + j]) * s; + qa[j] = (matrix[i * 4 + j] + matrix[j * 4 + i]) * s; + qa[k] = (matrix[i * 4 + k] + matrix[k * 4 + i]) * s; + + quat[0] = qa[0]; + quat[1] = qa[1]; + quat[2] = qa[2]; + quat[3] = qa[3]; + } + // fm_normalizeQuat(quat); +} + + +REAL fm_sphereVolume(REAL radius) // return's the volume of a sphere of this radius (4/3 PI * R cubed ) +{ + return (4.0f / 3.0f) * FM_PI * radius * radius * radius; +} + + +REAL fm_cylinderVolume(REAL radius, REAL h) +{ + return FM_PI * radius * radius * h; +} + +REAL fm_capsuleVolume(REAL radius, REAL h) +{ + REAL volume = fm_sphereVolume(radius); // volume of the sphere portion. + REAL ch = h - radius * 2; // this is the cylinder length + if (ch > 0) + { + volume += fm_cylinderVolume(radius, ch); + } + return volume; +} + +void fm_transform(const REAL matrix[16], const REAL v[3], REAL t[3]) // rotate and translate this point +{ + if (matrix) + { + REAL tx = + (matrix[0 * 4 + 0] * v[0]) + (matrix[1 * 4 + 0] * v[1]) + (matrix[2 * 4 + 0] * v[2]) + matrix[3 * 4 + 0]; + REAL ty = + (matrix[0 * 4 + 1] * v[0]) + (matrix[1 * 4 + 1] * v[1]) + (matrix[2 * 4 + 1] * v[2]) + matrix[3 * 4 + 1]; + REAL tz = + (matrix[0 * 4 + 2] * v[0]) + (matrix[1 * 4 + 2] * v[1]) + (matrix[2 * 4 + 2] * v[2]) + matrix[3 * 4 + 2]; + t[0] = tx; + t[1] = ty; + t[2] = tz; + } + else + { + t[0] = v[0]; + t[1] = v[1]; + t[2] = v[2]; + } +} + +void fm_rotate(const REAL matrix[16], const REAL v[3], REAL t[3]) // rotate and translate this point +{ + if (matrix) + { + REAL tx = (matrix[0 * 4 + 0] * v[0]) + (matrix[1 * 4 + 0] * v[1]) + (matrix[2 * 4 + 0] * v[2]); + REAL ty = (matrix[0 * 4 + 1] * v[0]) + (matrix[1 * 4 + 1] * v[1]) + (matrix[2 * 4 + 1] * v[2]); + REAL tz = (matrix[0 * 4 + 2] * v[0]) + (matrix[1 * 4 + 2] * v[1]) + (matrix[2 * 4 + 2] * v[2]); + t[0] = tx; + t[1] = ty; + t[2] = tz; + } + else + { + t[0] = v[0]; + t[1] = v[1]; + t[2] = v[2]; + } +} + + +REAL fm_distance(const REAL* p1, const REAL* p2) +{ + REAL dx = p1[0] - p2[0]; + REAL dy = p1[1] - p2[1]; + REAL dz = p1[2] - p2[2]; + + return (REAL)sqrt(dx * dx + dy * dy + dz * dz); +} + +REAL fm_distanceSquared(const REAL* p1, const REAL* p2) +{ + REAL dx = p1[0] - p2[0]; + REAL dy = p1[1] - p2[1]; + REAL dz = p1[2] - p2[2]; + + return dx * dx + dy * dy + dz * dz; +} + + +REAL fm_distanceSquaredXZ(const REAL* p1, const REAL* p2) +{ + REAL dx = p1[0] - p2[0]; + REAL dz = p1[2] - p2[2]; + + return dx * dx + dz * dz; +} + + +REAL fm_computePlane(const REAL* A, const REAL* B, const REAL* C, REAL* n) // returns D +{ + REAL vx = (B[0] - C[0]); + REAL vy = (B[1] - C[1]); + REAL vz = (B[2] - C[2]); + + REAL wx = (A[0] - B[0]); + REAL wy = (A[1] - B[1]); + REAL wz = (A[2] - B[2]); + + REAL vw_x = vy * wz - vz * wy; + REAL vw_y = vz * wx - vx * wz; + REAL vw_z = vx * wy - vy * wx; + + REAL mag = (REAL)sqrt((vw_x * vw_x) + (vw_y * vw_y) + (vw_z * vw_z)); + + if (mag < 0.000001f) + { + mag = 0; + } + else + { + mag = 1.0f / mag; + } + + REAL x = vw_x * mag; + REAL y = vw_y * mag; + REAL z = vw_z * mag; + + + REAL D = 0.0f - ((x * A[0]) + (y * A[1]) + (z * A[2])); + + n[0] = x; + n[1] = y; + n[2] = z; + + return D; +} + +REAL fm_distToPlane(const REAL* plane, const REAL* p) // computes the distance of this point from the plane. +{ + return p[0] * plane[0] + p[1] * plane[1] + p[2] * plane[2] + plane[3]; +} + +REAL fm_dot(const REAL* p1, const REAL* p2) +{ + return p1[0] * p2[0] + p1[1] * p2[1] + p1[2] * p2[2]; +} + +void fm_cross(REAL* cross, const REAL* a, const REAL* b) +{ + cross[0] = a[1] * b[2] - a[2] * b[1]; + cross[1] = a[2] * b[0] - a[0] * b[2]; + cross[2] = a[0] * b[1] - a[1] * b[0]; +} + +REAL fm_computeNormalVector(REAL* n, const REAL* p1, const REAL* p2) +{ + n[0] = p2[0] - p1[0]; + n[1] = p2[1] - p1[1]; + n[2] = p2[2] - p1[2]; + return fm_normalize(n); +} + +bool fm_computeWindingOrder(const REAL* p1, const REAL* p2, const REAL* p3) // returns true if the triangle is + // clockwise. +{ + bool ret = false; + + REAL v1[3]; + REAL v2[3]; + + fm_computeNormalVector(v1, p1, p2); // p2-p1 (as vector) and then normalized + fm_computeNormalVector(v2, p1, p3); // p3-p1 (as vector) and then normalized + + REAL cross[3]; + + fm_cross(cross, v1, v2); + REAL ref[3] = { 1, 0, 0 }; + + REAL d = fm_dot(cross, ref); + + + if (d <= 0) + ret = false; + else + ret = true; + + return ret; +} + +REAL fm_normalize(REAL* n) // normalize this vector +{ + REAL dist = (REAL)sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]); + if (dist > 0.0000001f) + { + REAL mag = 1.0f / dist; + n[0] *= mag; + n[1] *= mag; + n[2] *= mag; + } + else + { + n[0] = 1; + n[1] = 0; + n[2] = 0; + } + + return dist; +} + + +void fm_matrixMultiply(const REAL* pA, const REAL* pB, REAL* pM) +{ +#if 1 + + REAL a = pA[0 * 4 + 0] * pB[0 * 4 + 0] + pA[0 * 4 + 1] * pB[1 * 4 + 0] + pA[0 * 4 + 2] * pB[2 * 4 + 0] + + pA[0 * 4 + 3] * pB[3 * 4 + 0]; + REAL b = pA[0 * 4 + 0] * pB[0 * 4 + 1] + pA[0 * 4 + 1] * pB[1 * 4 + 1] + pA[0 * 4 + 2] * pB[2 * 4 + 1] + + pA[0 * 4 + 3] * pB[3 * 4 + 1]; + REAL c = pA[0 * 4 + 0] * pB[0 * 4 + 2] + pA[0 * 4 + 1] * pB[1 * 4 + 2] + pA[0 * 4 + 2] * pB[2 * 4 + 2] + + pA[0 * 4 + 3] * pB[3 * 4 + 2]; + REAL d = pA[0 * 4 + 0] * pB[0 * 4 + 3] + pA[0 * 4 + 1] * pB[1 * 4 + 3] + pA[0 * 4 + 2] * pB[2 * 4 + 3] + + pA[0 * 4 + 3] * pB[3 * 4 + 3]; + + REAL e = pA[1 * 4 + 0] * pB[0 * 4 + 0] + pA[1 * 4 + 1] * pB[1 * 4 + 0] + pA[1 * 4 + 2] * pB[2 * 4 + 0] + + pA[1 * 4 + 3] * pB[3 * 4 + 0]; + REAL f = pA[1 * 4 + 0] * pB[0 * 4 + 1] + pA[1 * 4 + 1] * pB[1 * 4 + 1] + pA[1 * 4 + 2] * pB[2 * 4 + 1] + + pA[1 * 4 + 3] * pB[3 * 4 + 1]; + REAL g = pA[1 * 4 + 0] * pB[0 * 4 + 2] + pA[1 * 4 + 1] * pB[1 * 4 + 2] + pA[1 * 4 + 2] * pB[2 * 4 + 2] + + pA[1 * 4 + 3] * pB[3 * 4 + 2]; + REAL h = pA[1 * 4 + 0] * pB[0 * 4 + 3] + pA[1 * 4 + 1] * pB[1 * 4 + 3] + pA[1 * 4 + 2] * pB[2 * 4 + 3] + + pA[1 * 4 + 3] * pB[3 * 4 + 3]; + + REAL i = pA[2 * 4 + 0] * pB[0 * 4 + 0] + pA[2 * 4 + 1] * pB[1 * 4 + 0] + pA[2 * 4 + 2] * pB[2 * 4 + 0] + + pA[2 * 4 + 3] * pB[3 * 4 + 0]; + REAL j = pA[2 * 4 + 0] * pB[0 * 4 + 1] + pA[2 * 4 + 1] * pB[1 * 4 + 1] + pA[2 * 4 + 2] * pB[2 * 4 + 1] + + pA[2 * 4 + 3] * pB[3 * 4 + 1]; + REAL k = pA[2 * 4 + 0] * pB[0 * 4 + 2] + pA[2 * 4 + 1] * pB[1 * 4 + 2] + pA[2 * 4 + 2] * pB[2 * 4 + 2] + + pA[2 * 4 + 3] * pB[3 * 4 + 2]; + REAL l = pA[2 * 4 + 0] * pB[0 * 4 + 3] + pA[2 * 4 + 1] * pB[1 * 4 + 3] + pA[2 * 4 + 2] * pB[2 * 4 + 3] + + pA[2 * 4 + 3] * pB[3 * 4 + 3]; + + REAL m = pA[3 * 4 + 0] * pB[0 * 4 + 0] + pA[3 * 4 + 1] * pB[1 * 4 + 0] + pA[3 * 4 + 2] * pB[2 * 4 + 0] + + pA[3 * 4 + 3] * pB[3 * 4 + 0]; + REAL n = pA[3 * 4 + 0] * pB[0 * 4 + 1] + pA[3 * 4 + 1] * pB[1 * 4 + 1] + pA[3 * 4 + 2] * pB[2 * 4 + 1] + + pA[3 * 4 + 3] * pB[3 * 4 + 1]; + REAL o = pA[3 * 4 + 0] * pB[0 * 4 + 2] + pA[3 * 4 + 1] * pB[1 * 4 + 2] + pA[3 * 4 + 2] * pB[2 * 4 + 2] + + pA[3 * 4 + 3] * pB[3 * 4 + 2]; + REAL p = pA[3 * 4 + 0] * pB[0 * 4 + 3] + pA[3 * 4 + 1] * pB[1 * 4 + 3] + pA[3 * 4 + 2] * pB[2 * 4 + 3] + + pA[3 * 4 + 3] * pB[3 * 4 + 3]; + + pM[0] = a; + pM[1] = b; + pM[2] = c; + pM[3] = d; + + pM[4] = e; + pM[5] = f; + pM[6] = g; + pM[7] = h; + + pM[8] = i; + pM[9] = j; + pM[10] = k; + pM[11] = l; + + pM[12] = m; + pM[13] = n; + pM[14] = o; + pM[15] = p; + + +#else + memset(pM, 0, sizeof(REAL) * 16); + for (int32_t i = 0; i < 4; i++) + for (int32_t j = 0; j < 4; j++) + for (int32_t k = 0; k < 4; k++) + pM[4 * i + j] += pA[4 * i + k] * pB[4 * k + j]; +#endif +} + + +void fm_eulerToQuatDX(REAL x, REAL y, REAL z, REAL* quat) // convert euler angles to quaternion using the fucked up + // DirectX method +{ + REAL matrix[16]; + fm_eulerToMatrix(x, y, z, matrix); + fm_matrixToQuat(matrix, quat); +} + +// implementation copied from: http://blogs.msdn.com/mikepelton/archive/2004/10/29/249501.aspx +void fm_eulerToMatrixDX(REAL x, REAL y, REAL z, REAL* matrix) // convert euler angles to quaternion using the fucked up + // DirectX method. +{ + fm_identity(matrix); + matrix[0 * 4 + 0] = (REAL)(cos(z) * cos(y) + sin(z) * sin(x) * sin(y)); + matrix[0 * 4 + 1] = (REAL)(sin(z) * cos(x)); + matrix[0 * 4 + 2] = (REAL)(cos(z) * -sin(y) + sin(z) * sin(x) * cos(y)); + + matrix[1 * 4 + 0] = (REAL)(-sin(z) * cos(y) + cos(z) * sin(x) * sin(y)); + matrix[1 * 4 + 1] = (REAL)(cos(z) * cos(x)); + matrix[1 * 4 + 2] = (REAL)(sin(z) * sin(y) + cos(z) * sin(x) * cos(y)); + + matrix[2 * 4 + 0] = (REAL)(cos(x) * sin(y)); + matrix[2 * 4 + 1] = (REAL)(-sin(x)); + matrix[2 * 4 + 2] = (REAL)(cos(x) * cos(y)); +} + + +void fm_scale(REAL x, REAL y, REAL z, REAL* fscale) // apply scale to the matrix. +{ + fscale[0 * 4 + 0] = x; + fscale[1 * 4 + 1] = y; + fscale[2 * 4 + 2] = z; +} + + +void fm_composeTransform(const REAL* position, const REAL* quat, const REAL* scale, REAL* matrix) +{ + fm_identity(matrix); + fm_quatToMatrix(quat, matrix); + + if (scale && (scale[0] != 1 || scale[1] != 1 || scale[2] != 1)) + { + REAL work[16]; + memcpy(work, matrix, sizeof(REAL) * 16); + REAL mscale[16]; + fm_identity(mscale); + fm_scale(scale[0], scale[1], scale[2], mscale); + fm_matrixMultiply(work, mscale, matrix); + } + + matrix[12] = position[0]; + matrix[13] = position[1]; + matrix[14] = position[2]; +} + + +void fm_setTranslation(const REAL* translation, REAL* matrix) +{ + matrix[12] = translation[0]; + matrix[13] = translation[1]; + matrix[14] = translation[2]; +} + +static REAL enorm0_3d(REAL x0, REAL y0, REAL z0, REAL x1, REAL y1, REAL z1) + +/**********************************************************************/ + +/* +Purpose: + +ENORM0_3D computes the Euclidean norm of (P1-P0) in 3D. + +Modified: + +18 April 1999 + +Author: + +John Burkardt + +Parameters: + +Input, REAL X0, Y0, Z0, X1, Y1, Z1, the coordinates of the points +P0 and P1. + +Output, REAL ENORM0_3D, the Euclidean norm of (P1-P0). +*/ +{ + REAL value; + + value = (REAL)sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0) + (z1 - z0) * (z1 - z0)); + + return value; +} + + +static REAL triangle_area_3d(REAL x1, REAL y1, REAL z1, REAL x2, REAL y2, REAL z2, REAL x3, REAL y3, REAL z3) + +/**********************************************************************/ + +/* +Purpose: + +TRIANGLE_AREA_3D computes the area of a triangle in 3D. + +Modified: + +22 April 1999 + +Author: + +John Burkardt + +Parameters: + +Input, REAL X1, Y1, Z1, X2, Y2, Z2, X3, Y3, Z3, the (X,Y,Z) +coordinates of the corners of the triangle. + +Output, REAL TRIANGLE_AREA_3D, the area of the triangle. +*/ +{ + REAL a; + REAL alpha; + REAL area; + REAL b; + REAL base; + REAL c; + REAL dot; + REAL height; + /* + Find the projection of (P3-P1) onto (P2-P1). + */ + dot = (x2 - x1) * (x3 - x1) + (y2 - y1) * (y3 - y1) + (z2 - z1) * (z3 - z1); + + base = enorm0_3d(x1, y1, z1, x2, y2, z2); + /* + The height of the triangle is the length of (P3-P1) after its + projection onto (P2-P1) has been subtracted. + */ + if (base == 0.0) + { + + height = 0.0; + } + else + { + + alpha = dot / (base * base); + + a = x3 - x1 - alpha * (x2 - x1); + b = y3 - y1 - alpha * (y2 - y1); + c = z3 - z1 - alpha * (z2 - z1); + + height = (REAL)sqrt(a * a + b * b + c * c); + } + + area = 0.5f * base * height; + + return area; +} + + +REAL fm_computeArea(const REAL* p1, const REAL* p2, const REAL* p3) +{ + REAL ret = 0; + + ret = triangle_area_3d(p1[0], p1[1], p1[2], p2[0], p2[1], p2[2], p3[0], p3[1], p3[2]); + + return ret; +} + + +void fm_lerp(const REAL* p1, const REAL* p2, REAL* dest, REAL lerpValue) +{ + dest[0] = ((p2[0] - p1[0]) * lerpValue) + p1[0]; + dest[1] = ((p2[1] - p1[1]) * lerpValue) + p1[1]; + dest[2] = ((p2[2] - p1[2]) * lerpValue) + p1[2]; +} + +bool fm_pointTestXZ(const REAL* p, const REAL* i, const REAL* j) +{ + bool ret = false; + + if ((((i[2] <= p[2]) && (p[2] < j[2])) || ((j[2] <= p[2]) && (p[2] < i[2]))) && + (p[0] < (j[0] - i[0]) * (p[2] - i[2]) / (j[2] - i[2]) + i[0])) + ret = true; + + return ret; +}; + + +bool fm_insideTriangleXZ(const REAL* p, const REAL* p1, const REAL* p2, const REAL* p3) +{ + bool ret = false; + + int32_t c = 0; + if (fm_pointTestXZ(p, p1, p2)) + c = !c; + if (fm_pointTestXZ(p, p2, p3)) + c = !c; + if (fm_pointTestXZ(p, p3, p1)) + c = !c; + if (c) + ret = true; + + return ret; +} + +bool fm_insideAABB(const REAL* pos, const REAL* bmin, const REAL* bmax) +{ + bool ret = false; + + if (pos[0] >= bmin[0] && pos[0] <= bmax[0] && pos[1] >= bmin[1] && pos[1] <= bmax[1] && pos[2] >= bmin[2] && + pos[2] <= bmax[2]) + ret = true; + + return ret; +} + + +uint32_t fm_clipTestPoint(const REAL* bmin, const REAL* bmax, const REAL* pos) +{ + uint32_t ret = 0; + + if (pos[0] < bmin[0]) + ret |= FMCS_XMIN; + else if (pos[0] > bmax[0]) + ret |= FMCS_XMAX; + + if (pos[1] < bmin[1]) + ret |= FMCS_YMIN; + else if (pos[1] > bmax[1]) + ret |= FMCS_YMAX; + + if (pos[2] < bmin[2]) + ret |= FMCS_ZMIN; + else if (pos[2] > bmax[2]) + ret |= FMCS_ZMAX; + + return ret; +} + +uint32_t fm_clipTestPointXZ(const REAL* bmin, const REAL* bmax, const REAL* pos) // only tests X and Z, not Y +{ + uint32_t ret = 0; + + if (pos[0] < bmin[0]) + ret |= FMCS_XMIN; + else if (pos[0] > bmax[0]) + ret |= FMCS_XMAX; + + if (pos[2] < bmin[2]) + ret |= FMCS_ZMIN; + else if (pos[2] > bmax[2]) + ret |= FMCS_ZMAX; + + return ret; +} + +uint32_t fm_clipTestAABB(const REAL* bmin, const REAL* bmax, const REAL* p1, const REAL* p2, const REAL* p3, uint32_t& andCode) +{ + uint32_t orCode = 0; + + andCode = FMCS_XMIN | FMCS_XMAX | FMCS_YMIN | FMCS_YMAX | FMCS_ZMIN | FMCS_ZMAX; + + uint32_t c = fm_clipTestPoint(bmin, bmax, p1); + orCode |= c; + andCode &= c; + + c = fm_clipTestPoint(bmin, bmax, p2); + orCode |= c; + andCode &= c; + + c = fm_clipTestPoint(bmin, bmax, p3); + orCode |= c; + andCode &= c; + + return orCode; +} + +bool intersect(const REAL* si, const REAL* ei, const REAL* bmin, const REAL* bmax, REAL* time) +{ + REAL st, et, fst = 0, fet = 1; + + for (int32_t i = 0; i < 3; i++) + { + if (*si < *ei) + { + if (*si > *bmax || *ei < *bmin) + return false; + REAL di = *ei - *si; + st = (*si < *bmin) ? (*bmin - *si) / di : 0; + et = (*ei > *bmax) ? (*bmax - *si) / di : 1; + } + else + { + if (*ei > *bmax || *si < *bmin) + return false; + REAL di = *ei - *si; + st = (*si > *bmax) ? (*bmax - *si) / di : 0; + et = (*ei < *bmin) ? (*bmin - *si) / di : 1; + } + + if (st > fst) + fst = st; + if (et < fet) + fet = et; + if (fet < fst) + return false; + bmin++; + bmax++; + si++; + ei++; + } + + *time = fst; + return true; +} + + +bool fm_lineTestAABB(const REAL* p1, const REAL* p2, const REAL* bmin, const REAL* bmax, REAL& time) +{ + bool sect = intersect(p1, p2, bmin, bmax, &time); + return sect; +} + + +bool fm_lineTestAABBXZ(const REAL* p1, const REAL* p2, const REAL* bmin, const REAL* bmax, REAL& time) +{ + REAL _bmin[3]; + REAL _bmax[3]; + + _bmin[0] = bmin[0]; + _bmin[1] = -1e9; + _bmin[2] = bmin[2]; + + _bmax[0] = bmax[0]; + _bmax[1] = 1e9; + _bmax[2] = bmax[2]; + + bool sect = intersect(p1, p2, _bmin, _bmax, &time); + + return sect; +} + +void fm_minmax(const REAL* p, REAL* bmin, REAL* bmax) // accmulate to a min-max value +{ + + if (p[0] < bmin[0]) + bmin[0] = p[0]; + if (p[1] < bmin[1]) + bmin[1] = p[1]; + if (p[2] < bmin[2]) + bmin[2] = p[2]; + + if (p[0] > bmax[0]) + bmax[0] = p[0]; + if (p[1] > bmax[1]) + bmax[1] = p[1]; + if (p[2] > bmax[2]) + bmax[2] = p[2]; +} + +REAL fm_solveX(const REAL* plane, REAL y, REAL z) // solve for X given this plane equation and the other two components. +{ + REAL x = (y * plane[1] + z * plane[2] + plane[3]) / -plane[0]; + return x; +} + +REAL fm_solveY(const REAL* plane, REAL x, REAL z) // solve for Y given this plane equation and the other two components. +{ + REAL y = (x * plane[0] + z * plane[2] + plane[3]) / -plane[1]; + return y; +} + + +REAL fm_solveZ(const REAL* plane, REAL x, REAL y) // solve for Y given this plane equation and the other two components. +{ + REAL z = (x * plane[0] + y * plane[1] + plane[3]) / -plane[2]; + return z; +} + + +void fm_getAABBCenter(const REAL* bmin, const REAL* bmax, REAL* center) +{ + center[0] = (bmax[0] - bmin[0]) * 0.5f + bmin[0]; + center[1] = (bmax[1] - bmin[1]) * 0.5f + bmin[1]; + center[2] = (bmax[2] - bmin[2]) * 0.5f + bmin[2]; +} + +FM_Axis fm_getDominantAxis(const REAL normal[3]) +{ + FM_Axis ret = FM_XAXIS; + + REAL x = (REAL)fabs(normal[0]); + REAL y = (REAL)fabs(normal[1]); + REAL z = (REAL)fabs(normal[2]); + + if (y > x && y > z) + ret = FM_YAXIS; + else if (z > x && z > y) + ret = FM_ZAXIS; + + return ret; +} + + +bool fm_lineSphereIntersect(const REAL* center, REAL radius, const REAL* p1, const REAL* p2, REAL* intersect) +{ + bool ret = false; + + REAL dir[3]; + + dir[0] = p2[0] - p1[0]; + dir[1] = p2[1] - p1[1]; + dir[2] = p2[2] - p1[2]; + + REAL distance = (REAL)sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]); + + if (distance > 0) + { + REAL recip = 1.0f / distance; + dir[0] *= recip; + dir[1] *= recip; + dir[2] *= recip; + ret = fm_raySphereIntersect(center, radius, p1, dir, distance, intersect); + } + else + { + dir[0] = center[0] - p1[0]; + dir[1] = center[1] - p1[1]; + dir[2] = center[2] - p1[2]; + REAL d2 = dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]; + REAL r2 = radius * radius; + if (d2 < r2) + { + ret = true; + if (intersect) + { + intersect[0] = p1[0]; + intersect[1] = p1[1]; + intersect[2] = p1[2]; + } + } + } + return ret; +} + +#define DOT(p1, p2) (p1[0] * p2[0] + p1[1] * p2[1] + p1[2] * p2[2]) + +bool fm_raySphereIntersect(const REAL* center, REAL radius, const REAL* pos, const REAL* dir, REAL distance, REAL* intersect) +{ + bool ret = false; + + REAL E0[3]; + + E0[0] = center[0] - pos[0]; + E0[1] = center[1] - pos[1]; + E0[2] = center[2] - pos[2]; + + REAL V[3]; + + V[0] = dir[0]; + V[1] = dir[1]; + V[2] = dir[2]; + + + REAL dist2 = E0[0] * E0[0] + E0[1] * E0[1] + E0[2] * E0[2]; + REAL radius2 = radius * radius; // radius squared.. + + // Bug Fix For Gem, if origin is *inside* the sphere, invert the + // direction vector so that we get a valid intersection location. + if (dist2 < radius2) + { + V[0] *= -1; + V[1] *= -1; + V[2] *= -1; + } + + + REAL v = DOT(E0, V); + + REAL disc = radius2 - (dist2 - v * v); + + if (disc > 0.0f) + { + if (intersect) + { + REAL d = (REAL)sqrt(disc); + REAL diff = v - d; + if (diff < distance) + { + intersect[0] = pos[0] + V[0] * diff; + intersect[1] = pos[1] + V[1] * diff; + intersect[2] = pos[2] + V[2] * diff; + ret = true; + } + } + } + + return ret; +} + + +void fm_catmullRom(REAL* out_vector, const REAL* p1, const REAL* p2, const REAL* p3, const REAL* p4, const REAL s) +{ + REAL s_squared = s * s; + REAL s_cubed = s_squared * s; + + REAL coefficient_p1 = -s_cubed + 2 * s_squared - s; + REAL coefficient_p2 = 3 * s_cubed - 5 * s_squared + 2; + REAL coefficient_p3 = -3 * s_cubed + 4 * s_squared + s; + REAL coefficient_p4 = s_cubed - s_squared; + + out_vector[0] = + (coefficient_p1 * p1[0] + coefficient_p2 * p2[0] + coefficient_p3 * p3[0] + coefficient_p4 * p4[0]) * 0.5f; + out_vector[1] = + (coefficient_p1 * p1[1] + coefficient_p2 * p2[1] + coefficient_p3 * p3[1] + coefficient_p4 * p4[1]) * 0.5f; + out_vector[2] = + (coefficient_p1 * p1[2] + coefficient_p2 * p2[2] + coefficient_p3 * p3[2] + coefficient_p4 * p4[2]) * 0.5f; +} + +bool fm_intersectAABB(const REAL* bmin1, const REAL* bmax1, const REAL* bmin2, const REAL* bmax2) +{ + if ((bmin1[0] > bmax2[0]) || (bmin2[0] > bmax1[0])) + return false; + if ((bmin1[1] > bmax2[1]) || (bmin2[1] > bmax1[1])) + return false; + if ((bmin1[2] > bmax2[2]) || (bmin2[2] > bmax1[2])) + return false; + return true; +} + +bool fm_insideAABB(const REAL* obmin, const REAL* obmax, const REAL* tbmin, const REAL* tbmax) // test if bounding box + // tbmin/tmbax is fully + // inside obmin/obmax +{ + bool ret = false; + + if (tbmax[0] <= obmax[0] && tbmax[1] <= obmax[1] && tbmax[2] <= obmax[2] && tbmin[0] >= obmin[0] && + tbmin[1] >= obmin[1] && tbmin[2] >= obmin[2]) + ret = true; + + return ret; +} + + +// Reference, from Stan Melax in Game Gems I +// Quaternion q; +// vector3 c = CrossProduct(v0,v1); +// REAL d = DotProduct(v0,v1); +// REAL s = (REAL)sqrt((1+d)*2); +// q.x = c.x / s; +// q.y = c.y / s; +// q.z = c.z / s; +// q.w = s /2.0f; +// return q; +void fm_rotationArc(const REAL* v0, const REAL* v1, REAL* quat) +{ + REAL cross[3]; + + fm_cross(cross, v0, v1); + REAL d = fm_dot(v0, v1); + + if (d <= -0.99999f) // 180 about x axis + { + if (fabsf((float)v0[0]) < 0.1f) + { + quat[0] = 0; + quat[1] = v0[2]; + quat[2] = -v0[1]; + quat[3] = 0; + } + else + { + quat[0] = v0[1]; + quat[1] = -v0[0]; + quat[2] = 0; + quat[3] = 0; + } + REAL magnitudeSquared = quat[0] * quat[0] + quat[1] * quat[1] + quat[2] * quat[2] + quat[3] * quat[3]; + REAL magnitude = sqrtf((float)magnitudeSquared); + REAL recip = 1.0f / magnitude; + quat[0] *= recip; + quat[1] *= recip; + quat[2] *= recip; + quat[3] *= recip; + } + else + { + REAL s = (REAL)sqrt((1 + d) * 2); + REAL recip = 1.0f / s; + + quat[0] = cross[0] * recip; + quat[1] = cross[1] * recip; + quat[2] = cross[2] * recip; + quat[3] = s * 0.5f; + } +} + + +REAL fm_distancePointLineSegment( + const REAL* Point, const REAL* LineStart, const REAL* LineEnd, REAL* intersection, LineSegmentType& type, REAL epsilon) +{ + REAL ret; + + REAL LineMag = fm_distance(LineEnd, LineStart); + + if (LineMag > 0) + { + REAL U = (((Point[0] - LineStart[0]) * (LineEnd[0] - LineStart[0])) + + ((Point[1] - LineStart[1]) * (LineEnd[1] - LineStart[1])) + + ((Point[2] - LineStart[2]) * (LineEnd[2] - LineStart[2]))) / + (LineMag * LineMag); + if (U < 0.0f || U > 1.0f) + { + REAL d1 = fm_distanceSquared(Point, LineStart); + REAL d2 = fm_distanceSquared(Point, LineEnd); + if (d1 <= d2) + { + ret = (REAL)sqrt(d1); + intersection[0] = LineStart[0]; + intersection[1] = LineStart[1]; + intersection[2] = LineStart[2]; + type = LS_START; + } + else + { + ret = (REAL)sqrt(d2); + intersection[0] = LineEnd[0]; + intersection[1] = LineEnd[1]; + intersection[2] = LineEnd[2]; + type = LS_END; + } + } + else + { + intersection[0] = LineStart[0] + U * (LineEnd[0] - LineStart[0]); + intersection[1] = LineStart[1] + U * (LineEnd[1] - LineStart[1]); + intersection[2] = LineStart[2] + U * (LineEnd[2] - LineStart[2]); + + ret = fm_distance(Point, intersection); + + REAL d1 = fm_distanceSquared(intersection, LineStart); + REAL d2 = fm_distanceSquared(intersection, LineEnd); + REAL mag = (epsilon * 2) * (epsilon * 2); + + if (d1 < mag) // if less than 1/100th the total distance, treat is as the 'start' + { + type = LS_START; + } + else if (d2 < mag) + { + type = LS_END; + } + else + { + type = LS_MIDDLE; + } + } + } + else + { + ret = LineMag; + intersection[0] = LineEnd[0]; + intersection[1] = LineEnd[1]; + intersection[2] = LineEnd[2]; + type = LS_END; + } + + return ret; +} + + +#ifndef BEST_FIT_PLANE_H + +# define BEST_FIT_PLANE_H + +template +class Eigen +{ +public: + void DecrSortEigenStuff(void) + { + Tridiagonal(); // diagonalize the matrix. + QLAlgorithm(); // + DecreasingSort(); + GuaranteeRotation(); + } + + void Tridiagonal(void) + { + Type fM00 = mElement[0][0]; + Type fM01 = mElement[0][1]; + Type fM02 = mElement[0][2]; + Type fM11 = mElement[1][1]; + Type fM12 = mElement[1][2]; + Type fM22 = mElement[2][2]; + + m_afDiag[0] = fM00; + m_afSubd[2] = 0; + if (fM02 != (Type)0.0) + { + Type fLength = (REAL)sqrt(fM01 * fM01 + fM02 * fM02); + Type fInvLength = ((Type)1.0) / fLength; + fM01 *= fInvLength; + fM02 *= fInvLength; + Type fQ = ((Type)2.0) * fM01 * fM12 + fM02 * (fM22 - fM11); + m_afDiag[1] = fM11 + fM02 * fQ; + m_afDiag[2] = fM22 - fM02 * fQ; + m_afSubd[0] = fLength; + m_afSubd[1] = fM12 - fM01 * fQ; + mElement[0][0] = (Type)1.0; + mElement[0][1] = (Type)0.0; + mElement[0][2] = (Type)0.0; + mElement[1][0] = (Type)0.0; + mElement[1][1] = fM01; + mElement[1][2] = fM02; + mElement[2][0] = (Type)0.0; + mElement[2][1] = fM02; + mElement[2][2] = -fM01; + m_bIsRotation = false; + } + else + { + m_afDiag[1] = fM11; + m_afDiag[2] = fM22; + m_afSubd[0] = fM01; + m_afSubd[1] = fM12; + mElement[0][0] = (Type)1.0; + mElement[0][1] = (Type)0.0; + mElement[0][2] = (Type)0.0; + mElement[1][0] = (Type)0.0; + mElement[1][1] = (Type)1.0; + mElement[1][2] = (Type)0.0; + mElement[2][0] = (Type)0.0; + mElement[2][1] = (Type)0.0; + mElement[2][2] = (Type)1.0; + m_bIsRotation = true; + } + } + + bool QLAlgorithm(void) + { + const int32_t iMaxIter = 32; + + for (int32_t i0 = 0; i0 < 3; i0++) + { + int32_t i1; + for (i1 = 0; i1 < iMaxIter; i1++) + { + int32_t i2; + for (i2 = i0; i2 <= (3 - 2); i2++) + { + Type fTmp = Type(fabs(m_afDiag[i2]) + fabs(m_afDiag[i2 + 1])); + if (fabs(m_afSubd[i2]) + fTmp == fTmp) + break; + } + if (i2 == i0) + { + break; + } + + Type fG = (m_afDiag[i0 + 1] - m_afDiag[i0]) / (((Type)2.0) * m_afSubd[i0]); + Type fR = (REAL)sqrt(fG * fG + (Type)1.0); + if (fG < (Type)0.0) + { + fG = m_afDiag[i2] - m_afDiag[i0] + m_afSubd[i0] / (fG - fR); + } + else + { + fG = m_afDiag[i2] - m_afDiag[i0] + m_afSubd[i0] / (fG + fR); + } + Type fSin = (Type)1.0, fCos = (Type)1.0, fP = (Type)0.0; + for (int32_t i3 = i2 - 1; i3 >= i0; i3--) + { + Type fF = fSin * m_afSubd[i3]; + Type fB = fCos * m_afSubd[i3]; + if (fabs(fF) >= fabs(fG)) + { + fCos = fG / fF; + fR = (REAL)sqrt(fCos * fCos + (Type)1.0); + m_afSubd[i3 + 1] = fF * fR; + fSin = ((Type)1.0) / fR; + fCos *= fSin; + } + else + { + fSin = fF / fG; + fR = (REAL)sqrt(fSin * fSin + (Type)1.0); + m_afSubd[i3 + 1] = fG * fR; + fCos = ((Type)1.0) / fR; + fSin *= fCos; + } + fG = m_afDiag[i3 + 1] - fP; + fR = (m_afDiag[i3] - fG) * fSin + ((Type)2.0) * fB * fCos; + fP = fSin * fR; + m_afDiag[i3 + 1] = fG + fP; + fG = fCos * fR - fB; + for (int32_t i4 = 0; i4 < 3; i4++) + { + fF = mElement[i4][i3 + 1]; + mElement[i4][i3 + 1] = fSin * mElement[i4][i3] + fCos * fF; + mElement[i4][i3] = fCos * mElement[i4][i3] - fSin * fF; + } + } + m_afDiag[i0] -= fP; + m_afSubd[i0] = fG; + m_afSubd[i2] = (Type)0.0; + } + if (i1 == iMaxIter) + { + return false; + } + } + return true; + } + + void DecreasingSort(void) + { + // sort eigenvalues in decreasing order, e[0] >= ... >= e[iSize-1] + for (int32_t i0 = 0, i1; i0 <= 3 - 2; i0++) + { + // locate maximum eigenvalue + i1 = i0; + Type fMax = m_afDiag[i1]; + int32_t i2; + for (i2 = i0 + 1; i2 < 3; i2++) + { + if (m_afDiag[i2] > fMax) + { + i1 = i2; + fMax = m_afDiag[i1]; + } + } + + if (i1 != i0) + { + // swap eigenvalues + m_afDiag[i1] = m_afDiag[i0]; + m_afDiag[i0] = fMax; + // swap eigenvectors + for (i2 = 0; i2 < 3; i2++) + { + Type fTmp = mElement[i2][i0]; + mElement[i2][i0] = mElement[i2][i1]; + mElement[i2][i1] = fTmp; + m_bIsRotation = !m_bIsRotation; + } + } + } + } + + + void GuaranteeRotation(void) + { + if (!m_bIsRotation) + { + // change sign on the first column + for (int32_t iRow = 0; iRow < 3; iRow++) + { + mElement[iRow][0] = -mElement[iRow][0]; + } + } + } + + Type mElement[3][3]; + Type m_afDiag[3]; + Type m_afSubd[3]; + bool m_bIsRotation; +}; + +#endif + +bool fm_computeBestFitPlane( + uint32_t vcount, const REAL* points, uint32_t vstride, const REAL* weights, uint32_t wstride, REAL* plane, REAL* center) +{ + bool ret = false; + + REAL kOrigin[3] = { 0, 0, 0 }; + + REAL wtotal = 0; + + { + const char* source = (const char*)points; + const char* wsource = (const char*)weights; + + for (uint32_t i = 0; i < vcount; i++) + { + + const REAL* p = (const REAL*)source; + + REAL w = 1; + + if (wsource) + { + const REAL* ws = (const REAL*)wsource; + w = *ws; // + wsource += wstride; + } + + kOrigin[0] += p[0] * w; + kOrigin[1] += p[1] * w; + kOrigin[2] += p[2] * w; + + wtotal += w; + + source += vstride; + } + } + + REAL recip = 1.0f / wtotal; // reciprocol of total weighting + + kOrigin[0] *= recip; + kOrigin[1] *= recip; + kOrigin[2] *= recip; + + center[0] = kOrigin[0]; + center[1] = kOrigin[1]; + center[2] = kOrigin[2]; + + + REAL fSumXX = 0; + REAL fSumXY = 0; + REAL fSumXZ = 0; + + REAL fSumYY = 0; + REAL fSumYZ = 0; + REAL fSumZZ = 0; + + + { + const char* source = (const char*)points; + const char* wsource = (const char*)weights; + + for (uint32_t i = 0; i < vcount; i++) + { + + const REAL* p = (const REAL*)source; + + REAL w = 1; + + if (wsource) + { + const REAL* ws = (const REAL*)wsource; + w = *ws; // + wsource += wstride; + } + + REAL kDiff[3]; + + kDiff[0] = w * (p[0] - kOrigin[0]); // apply vertex weighting! + kDiff[1] = w * (p[1] - kOrigin[1]); + kDiff[2] = w * (p[2] - kOrigin[2]); + + fSumXX += kDiff[0] * kDiff[0]; // sume of the squares of the differences. + fSumXY += kDiff[0] * kDiff[1]; // sume of the squares of the differences. + fSumXZ += kDiff[0] * kDiff[2]; // sume of the squares of the differences. + + fSumYY += kDiff[1] * kDiff[1]; + fSumYZ += kDiff[1] * kDiff[2]; + fSumZZ += kDiff[2] * kDiff[2]; + + + source += vstride; + } + } + + fSumXX *= recip; + fSumXY *= recip; + fSumXZ *= recip; + fSumYY *= recip; + fSumYZ *= recip; + fSumZZ *= recip; + + // setup the eigensolver + Eigen kES; + + kES.mElement[0][0] = fSumXX; + kES.mElement[0][1] = fSumXY; + kES.mElement[0][2] = fSumXZ; + + kES.mElement[1][0] = fSumXY; + kES.mElement[1][1] = fSumYY; + kES.mElement[1][2] = fSumYZ; + + kES.mElement[2][0] = fSumXZ; + kES.mElement[2][1] = fSumYZ; + kES.mElement[2][2] = fSumZZ; + + // compute eigenstuff, smallest eigenvalue is in last position + kES.DecrSortEigenStuff(); + + REAL kNormal[3]; + + kNormal[0] = kES.mElement[0][2]; + kNormal[1] = kES.mElement[1][2]; + kNormal[2] = kES.mElement[2][2]; + + // the minimum energy + plane[0] = kNormal[0]; + plane[1] = kNormal[1]; + plane[2] = kNormal[2]; + + plane[3] = 0 - fm_dot(kNormal, kOrigin); + + ret = true; + + return ret; +} + + +bool fm_colinear(const REAL a1[3], const REAL a2[3], const REAL b1[3], const REAL b2[3], REAL epsilon) // true if these + // two line + // segments are + // co-linear. +{ + bool ret = false; + + REAL dir1[3]; + REAL dir2[3]; + + dir1[0] = (a2[0] - a1[0]); + dir1[1] = (a2[1] - a1[1]); + dir1[2] = (a2[2] - a1[2]); + + dir2[0] = (b2[0] - a1[0]) - (b1[0] - a1[0]); + dir2[1] = (b2[1] - a1[1]) - (b1[1] - a1[1]); + dir2[2] = (b2[2] - a2[2]) - (b1[2] - a2[2]); + + fm_normalize(dir1); + fm_normalize(dir2); + + REAL dot = fm_dot(dir1, dir2); + + if (dot >= epsilon) + { + ret = true; + } + + + return ret; +} + +bool fm_colinear(const REAL* p1, const REAL* p2, const REAL* p3, REAL epsilon) +{ + bool ret = false; + + REAL dir1[3]; + REAL dir2[3]; + + dir1[0] = p2[0] - p1[0]; + dir1[1] = p2[1] - p1[1]; + dir1[2] = p2[2] - p1[2]; + + dir2[0] = p3[0] - p2[0]; + dir2[1] = p3[1] - p2[1]; + dir2[2] = p3[2] - p2[2]; + + fm_normalize(dir1); + fm_normalize(dir2); + + REAL dot = fm_dot(dir1, dir2); + + if (dot >= epsilon) + { + ret = true; + } + + + return ret; +} + +void fm_initMinMax(const REAL* p, REAL* bmin, REAL* bmax) +{ + bmax[0] = bmin[0] = p[0]; + bmax[1] = bmin[1] = p[1]; + bmax[2] = bmin[2] = p[2]; +} + +IntersectResult fm_intersectLineSegments2d(const REAL* a1, const REAL* a2, const REAL* b1, const REAL* b2, REAL* intersection) +{ + IntersectResult ret; + + REAL denom = ((b2[1] - b1[1]) * (a2[0] - a1[0])) - ((b2[0] - b1[0]) * (a2[1] - a1[1])); + REAL nume_a = ((b2[0] - b1[0]) * (a1[1] - b1[1])) - ((b2[1] - b1[1]) * (a1[0] - b1[0])); + REAL nume_b = ((a2[0] - a1[0]) * (a1[1] - b1[1])) - ((a2[1] - a1[1]) * (a1[0] - b1[0])); + if (denom == 0) + { + if (nume_a == 0 && nume_b == 0) + { + ret = IR_COINCIDENT; + } + else + { + ret = IR_PARALLEL; + } + } + else + { + + REAL recip = 1 / denom; + REAL ua = nume_a * recip; + REAL ub = nume_b * recip; + + if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) + { + // Get the intersection point. + intersection[0] = a1[0] + ua * (a2[0] - a1[0]); + intersection[1] = a1[1] + ua * (a2[1] - a1[1]); + ret = IR_DO_INTERSECT; + } + else + { + ret = IR_DONT_INTERSECT; + } + } + return ret; +} + +IntersectResult fm_intersectLineSegments2dTime( + const REAL* a1, const REAL* a2, const REAL* b1, const REAL* b2, REAL& t1, REAL& t2) +{ + IntersectResult ret; + + REAL denom = ((b2[1] - b1[1]) * (a2[0] - a1[0])) - ((b2[0] - b1[0]) * (a2[1] - a1[1])); + REAL nume_a = ((b2[0] - b1[0]) * (a1[1] - b1[1])) - ((b2[1] - b1[1]) * (a1[0] - b1[0])); + REAL nume_b = ((a2[0] - a1[0]) * (a1[1] - b1[1])) - ((a2[1] - a1[1]) * (a1[0] - b1[0])); + if (denom == 0) + { + if (nume_a == 0 && nume_b == 0) + { + ret = IR_COINCIDENT; + } + else + { + ret = IR_PARALLEL; + } + } + else + { + + REAL recip = 1 / denom; + REAL ua = nume_a * recip; + REAL ub = nume_b * recip; + + if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) + { + t1 = ua; + t2 = ub; + ret = IR_DO_INTERSECT; + } + else + { + ret = IR_DONT_INTERSECT; + } + } + return ret; +} + +//**** Plane Triangle Intersection + + +// assumes that the points are on opposite sides of the plane! +bool fm_intersectPointPlane(const REAL* p1, const REAL* p2, REAL* split, const REAL* plane) +{ + + REAL dp1 = fm_distToPlane(plane, p1); + REAL dp2 = fm_distToPlane(plane, p2); + if (dp1 <= 0 && dp2 <= 0) + { + return false; + } + if (dp1 >= 0 && dp2 >= 0) + { + return false; + } + + REAL dir[3]; + + dir[0] = p2[0] - p1[0]; + dir[1] = p2[1] - p1[1]; + dir[2] = p2[2] - p1[2]; + + REAL dot1 = dir[0] * plane[0] + dir[1] * plane[1] + dir[2] * plane[2]; + REAL dot2 = dp1 - plane[3]; + + REAL t = -(plane[3] + dot2) / dot1; + + split[0] = (dir[0] * t) + p1[0]; + split[1] = (dir[1] * t) + p1[1]; + split[2] = (dir[2] * t) + p1[2]; + + return true; +} + +PlaneTriResult fm_getSidePlane(const REAL* p, const REAL* plane, REAL epsilon) +{ + PlaneTriResult ret = PTR_ON_PLANE; + + REAL d = fm_distToPlane(plane, p); + + if (d < -epsilon || d > epsilon) + { + if (d > 0) + ret = PTR_FRONT; // it is 'in front' within the provided epsilon value. + else + ret = PTR_BACK; + } + + return ret; +} + + +#ifndef PLANE_TRIANGLE_INTERSECTION_H + +# define PLANE_TRIANGLE_INTERSECTION_H + +# define MAXPTS 256 + +template +class point +{ +public: + void set(const Type* p) + { + x = p[0]; + y = p[1]; + z = p[2]; + } + + Type x; + Type y; + Type z; +}; + +template +class plane +{ +public: + plane(const Type* p) + { + normal.x = p[0]; + normal.y = p[1]; + normal.z = p[2]; + D = p[3]; + } + + Type Classify_Point(const point& p) + { + return p.x * normal.x + p.y * normal.y + p.z * normal.z + D; + } + + point normal; + Type D; +}; + +template +class polygon +{ +public: + polygon(void) + { + mVcount = 0; + } + + polygon(const Type* p1, const Type* p2, const Type* p3) + { + mVcount = 3; + mVertices[0].set(p1); + mVertices[1].set(p2); + mVertices[2].set(p3); + } + + + int32_t NumVertices(void) const + { + return mVcount; + }; + + const point& Vertex(int32_t index) + { + if (index < 0) + index += mVcount; + return mVertices[index]; + }; + + + void set(const point* pts, int32_t count) + { + for (int32_t i = 0; i < count; i++) + { + mVertices[i] = pts[i]; + } + mVcount = count; + } + + + void Split_Polygon(polygon* poly, plane* part, polygon& front, polygon& back) + { + int32_t count = poly->NumVertices(); + int32_t out_c = 0, in_c = 0; + point ptA, ptB, outpts[MAXPTS], inpts[MAXPTS]; + Type sideA, sideB; + ptA = poly->Vertex(count - 1); + sideA = part->Classify_Point(ptA); + for (int32_t i = -1; ++i < count;) + { + ptB = poly->Vertex(i); + sideB = part->Classify_Point(ptB); + if (sideB > 0) + { + if (sideA < 0) + { + point v; + fm_intersectPointPlane(&ptB.x, &ptA.x, &v.x, &part->normal.x); + outpts[out_c++] = inpts[in_c++] = v; + } + outpts[out_c++] = ptB; + } + else if (sideB < 0) + { + if (sideA > 0) + { + point v; + fm_intersectPointPlane(&ptB.x, &ptA.x, &v.x, &part->normal.x); + outpts[out_c++] = inpts[in_c++] = v; + } + inpts[in_c++] = ptB; + } + else + outpts[out_c++] = inpts[in_c++] = ptB; + ptA = ptB; + sideA = sideB; + } + + front.set(&outpts[0], out_c); + back.set(&inpts[0], in_c); + } + + int32_t mVcount; + point mVertices[MAXPTS]; +}; + + +#endif + +static inline void add(const REAL* p, REAL* dest, uint32_t tstride, uint32_t& pcount) +{ + char* d = (char*)dest; + d = d + pcount * tstride; + dest = (REAL*)d; + dest[0] = p[0]; + dest[1] = p[1]; + dest[2] = p[2]; + pcount++; + assert(pcount <= 4); +} + + +PlaneTriResult fm_planeTriIntersection(const REAL* _plane, // the plane equation in Ax+By+Cz+D format + const REAL* triangle, // the source triangle. + uint32_t tstride, // stride in bytes of the input and output *vertices* + REAL epsilon, // the co-planar epsilon value. + REAL* front, // the triangle in front of the + uint32_t& fcount, // number of vertices in the 'front' triangle + REAL* back, // the triangle in back of the plane + uint32_t& bcount) // the number of vertices in the 'back' triangle. +{ + + fcount = 0; + bcount = 0; + + const char* tsource = (const char*)triangle; + + // get the three vertices of the triangle. + const REAL* p1 = (const REAL*)(tsource); + const REAL* p2 = (const REAL*)(tsource + tstride); + const REAL* p3 = (const REAL*)(tsource + tstride * 2); + + + PlaneTriResult r1 = fm_getSidePlane(p1, _plane, epsilon); // compute the side of the plane each vertex is on + PlaneTriResult r2 = fm_getSidePlane(p2, _plane, epsilon); + PlaneTriResult r3 = fm_getSidePlane(p3, _plane, epsilon); + + // If any of the points lay right *on* the plane.... + if (r1 == PTR_ON_PLANE || r2 == PTR_ON_PLANE || r3 == PTR_ON_PLANE) + { + // If the triangle is completely co-planar, then just treat it as 'front' and return! + if (r1 == PTR_ON_PLANE && r2 == PTR_ON_PLANE && r3 == PTR_ON_PLANE) + { + add(p1, front, tstride, fcount); + add(p2, front, tstride, fcount); + add(p3, front, tstride, fcount); + return PTR_FRONT; + } + // Decide to place the co-planar points on the same side as the co-planar point. + PlaneTriResult r = PTR_ON_PLANE; + if (r1 != PTR_ON_PLANE) + r = r1; + else if (r2 != PTR_ON_PLANE) + r = r2; + else if (r3 != PTR_ON_PLANE) + r = r3; + + if (r1 == PTR_ON_PLANE) + r1 = r; + if (r2 == PTR_ON_PLANE) + r2 = r; + if (r3 == PTR_ON_PLANE) + r3 = r; + } + + if (r1 == r2 && r1 == r3) // if all three vertices are on the same side of the plane. + { + if (r1 == PTR_FRONT) // if all three are in front of the plane, then copy to the 'front' output triangle. + { + add(p1, front, tstride, fcount); + add(p2, front, tstride, fcount); + add(p3, front, tstride, fcount); + } + else + { + add(p1, back, tstride, bcount); // if all three are in 'back' then copy to the 'back' output triangle. + add(p2, back, tstride, bcount); + add(p3, back, tstride, bcount); + } + return r1; // if all three points are on the same side of the plane return result + } + + + polygon pi(p1, p2, p3); + polygon pfront, pback; + + plane part(_plane); + + pi.Split_Polygon(&pi, &part, pfront, pback); + + for (int32_t i = 0; i < pfront.mVcount; i++) + { + add(&pfront.mVertices[i].x, front, tstride, fcount); + } + + for (int32_t i = 0; i < pback.mVcount; i++) + { + add(&pback.mVertices[i].x, back, tstride, bcount); + } + + PlaneTriResult ret = PTR_SPLIT; + + if (fcount < 3) + fcount = 0; + if (bcount < 3) + bcount = 0; + + if (fcount == 0 && bcount) + ret = PTR_BACK; + + if (bcount == 0 && fcount) + ret = PTR_FRONT; + + + return ret; +} + +// computes the OBB for this set of points relative to this transform matrix. +void computeOBB(uint32_t vcount, const REAL* points, uint32_t pstride, REAL* sides, REAL* matrix) +{ + const char* src = (const char*)points; + + REAL bmin[3] = { 1e9, 1e9, 1e9 }; + REAL bmax[3] = { -1e9, -1e9, -1e9 }; + + for (uint32_t i = 0; i < vcount; i++) + { + const REAL* p = (const REAL*)src; + REAL t[3]; + + fm_inverseRT(matrix, p, t); // inverse rotate translate + + if (t[0] < bmin[0]) + bmin[0] = t[0]; + if (t[1] < bmin[1]) + bmin[1] = t[1]; + if (t[2] < bmin[2]) + bmin[2] = t[2]; + + if (t[0] > bmax[0]) + bmax[0] = t[0]; + if (t[1] > bmax[1]) + bmax[1] = t[1]; + if (t[2] > bmax[2]) + bmax[2] = t[2]; + + src += pstride; + } + + REAL center[3]; + + sides[0] = bmax[0] - bmin[0]; + sides[1] = bmax[1] - bmin[1]; + sides[2] = bmax[2] - bmin[2]; + + center[0] = sides[0] * 0.5f + bmin[0]; + center[1] = sides[1] * 0.5f + bmin[1]; + center[2] = sides[2] * 0.5f + bmin[2]; + + REAL ocenter[3]; + + fm_rotate(matrix, center, ocenter); + + matrix[12] += ocenter[0]; + matrix[13] += ocenter[1]; + matrix[14] += ocenter[2]; +} + +void fm_computeBestFitOBB(uint32_t vcount, const REAL* points, uint32_t pstride, REAL* sides, REAL* matrix, bool bruteForce) +{ + REAL plane[4]; + REAL center[3]; + fm_computeBestFitPlane(vcount, points, pstride, 0, 0, plane, center); + fm_planeToMatrix(plane, matrix); + computeOBB(vcount, points, pstride, sides, matrix); + + REAL refmatrix[16]; + memcpy(refmatrix, matrix, 16 * sizeof(REAL)); + + REAL volume = sides[0] * sides[1] * sides[2]; + if (bruteForce) + { + for (REAL a = 10; a < 180; a += 10) + { + REAL quat[4]; + fm_eulerToQuat(0, a * FM_DEG_TO_RAD, 0, quat); + REAL temp[16]; + REAL pmatrix[16]; + fm_quatToMatrix(quat, temp); + fm_matrixMultiply(temp, refmatrix, pmatrix); + REAL psides[3]; + computeOBB(vcount, points, pstride, psides, pmatrix); + REAL v = psides[0] * psides[1] * psides[2]; + if (v < volume) + { + volume = v; + memcpy(matrix, pmatrix, sizeof(REAL) * 16); + sides[0] = psides[0]; + sides[1] = psides[1]; + sides[2] = psides[2]; + } + } + } +} + +void fm_computeBestFitOBB( + uint32_t vcount, const REAL* points, uint32_t pstride, REAL* sides, REAL* pos, REAL* quat, bool bruteForce) +{ + REAL matrix[16]; + fm_computeBestFitOBB(vcount, points, pstride, sides, matrix, bruteForce); + fm_getTranslation(matrix, pos); + fm_matrixToQuat(matrix, quat); +} + +void fm_computeBestFitABB(uint32_t vcount, const REAL* points, uint32_t pstride, REAL* sides, REAL* pos) +{ + REAL bmin[3]; + REAL bmax[3]; + + bmin[0] = points[0]; + bmin[1] = points[1]; + bmin[2] = points[2]; + + bmax[0] = points[0]; + bmax[1] = points[1]; + bmax[2] = points[2]; + + const char* cp = (const char*)points; + for (uint32_t i = 0; i < vcount; i++) + { + const REAL* p = (const REAL*)cp; + + if (p[0] < bmin[0]) + bmin[0] = p[0]; + if (p[1] < bmin[1]) + bmin[1] = p[1]; + if (p[2] < bmin[2]) + bmin[2] = p[2]; + + if (p[0] > bmax[0]) + bmax[0] = p[0]; + if (p[1] > bmax[1]) + bmax[1] = p[1]; + if (p[2] > bmax[2]) + bmax[2] = p[2]; + + cp += pstride; + } + + + sides[0] = bmax[0] - bmin[0]; + sides[1] = bmax[1] - bmin[1]; + sides[2] = bmax[2] - bmin[2]; + + pos[0] = bmin[0] + sides[0] * 0.5f; + pos[1] = bmin[1] + sides[1] * 0.5f; + pos[2] = bmin[2] + sides[2] * 0.5f; +} + + +void fm_planeToMatrix(const REAL* plane, REAL* matrix) // convert a plane equation to a 4x4 rotation matrix +{ + REAL ref[3] = { 0, 1, 0 }; + REAL quat[4]; + fm_rotationArc(ref, plane, quat); + fm_quatToMatrix(quat, matrix); + REAL origin[3] = { 0, -plane[3], 0 }; + REAL center[3]; + fm_transform(matrix, origin, center); + fm_setTranslation(center, matrix); +} + +void fm_planeToQuat(const REAL* plane, REAL* quat, REAL* pos) // convert a plane equation to a quaternion and + // translation +{ + REAL ref[3] = { 0, 1, 0 }; + REAL matrix[16]; + fm_rotationArc(ref, plane, quat); + fm_quatToMatrix(quat, matrix); + REAL origin[3] = { 0, plane[3], 0 }; + fm_transform(matrix, origin, pos); +} + +void fm_eulerMatrix(REAL ax, REAL ay, REAL az, REAL* matrix) // convert euler (in radians) to a dest 4x4 matrix + // (translation set to zero) +{ + REAL quat[4]; + fm_eulerToQuat(ax, ay, az, quat); + fm_quatToMatrix(quat, matrix); +} + + +//********************************************************** +//********************************************************** +//**** Vertex Welding +//********************************************************** +//********************************************************** + +#ifndef VERTEX_INDEX_H + +# define VERTEX_INDEX_H + +namespace VERTEX_INDEX +{ + +class KdTreeNode; + +typedef std::vector KdTreeNodeVector; + +enum Axes +{ + X_AXIS = 0, + Y_AXIS = 1, + Z_AXIS = 2 +}; + +class KdTreeFindNode +{ +public: + KdTreeFindNode(void) + { + mNode = 0; + mDistance = 0; + } + KdTreeNode* mNode; + double mDistance; +}; + +class KdTreeInterface +{ +public: + virtual const double* getPositionDouble(uint32_t index) const = 0; + virtual const float* getPositionFloat(uint32_t index) const = 0; +}; + +class KdTreeNode +{ +public: + KdTreeNode(void) + { + mIndex = 0; + mLeft = 0; + mRight = 0; + } + + KdTreeNode(uint32_t index) + { + mIndex = index; + mLeft = 0; + mRight = 0; + }; + + ~KdTreeNode(void) + { + } + + + void addDouble(KdTreeNode* node, Axes dim, const KdTreeInterface* iface) + { + const double* nodePosition = iface->getPositionDouble(node->mIndex); + const double* position = iface->getPositionDouble(mIndex); + switch (dim) + { + case X_AXIS: + if (nodePosition[0] <= position[0]) + { + if (mLeft) + mLeft->addDouble(node, Y_AXIS, iface); + else + mLeft = node; + } + else + { + if (mRight) + mRight->addDouble(node, Y_AXIS, iface); + else + mRight = node; + } + break; + case Y_AXIS: + if (nodePosition[1] <= position[1]) + { + if (mLeft) + mLeft->addDouble(node, Z_AXIS, iface); + else + mLeft = node; + } + else + { + if (mRight) + mRight->addDouble(node, Z_AXIS, iface); + else + mRight = node; + } + break; + case Z_AXIS: + if (nodePosition[2] <= position[2]) + { + if (mLeft) + mLeft->addDouble(node, X_AXIS, iface); + else + mLeft = node; + } + else + { + if (mRight) + mRight->addDouble(node, X_AXIS, iface); + else + mRight = node; + } + break; + } + } + + + void addFloat(KdTreeNode* node, Axes dim, const KdTreeInterface* iface) + { + const float* nodePosition = iface->getPositionFloat(node->mIndex); + const float* position = iface->getPositionFloat(mIndex); + switch (dim) + { + case X_AXIS: + if (nodePosition[0] <= position[0]) + { + if (mLeft) + mLeft->addFloat(node, Y_AXIS, iface); + else + mLeft = node; + } + else + { + if (mRight) + mRight->addFloat(node, Y_AXIS, iface); + else + mRight = node; + } + break; + case Y_AXIS: + if (nodePosition[1] <= position[1]) + { + if (mLeft) + mLeft->addFloat(node, Z_AXIS, iface); + else + mLeft = node; + } + else + { + if (mRight) + mRight->addFloat(node, Z_AXIS, iface); + else + mRight = node; + } + break; + case Z_AXIS: + if (nodePosition[2] <= position[2]) + { + if (mLeft) + mLeft->addFloat(node, X_AXIS, iface); + else + mLeft = node; + } + else + { + if (mRight) + mRight->addFloat(node, X_AXIS, iface); + else + mRight = node; + } + break; + } + } + + + uint32_t getIndex(void) const + { + return mIndex; + }; + + void search(Axes axis, + const double* pos, + double radius, + uint32_t& count, + uint32_t maxObjects, + KdTreeFindNode* found, + const KdTreeInterface* iface) + { + + const double* position = iface->getPositionDouble(mIndex); + + double dx = pos[0] - position[0]; + double dy = pos[1] - position[1]; + double dz = pos[2] - position[2]; + + KdTreeNode* search1 = 0; + KdTreeNode* search2 = 0; + + switch (axis) + { + case X_AXIS: + if (dx <= 0) // JWR if we are to the left + { + search1 = mLeft; // JWR then search to the left + if (-dx < radius) // JWR if distance to the right is less than our search radius, continue on the right + // as well. + search2 = mRight; + } + else + { + search1 = mRight; // JWR ok, we go down the left tree + if (dx < radius) // JWR if the distance from the right is less than our search radius + search2 = mLeft; + } + axis = Y_AXIS; + break; + case Y_AXIS: + if (dy <= 0) + { + search1 = mLeft; + if (-dy < radius) + search2 = mRight; + } + else + { + search1 = mRight; + if (dy < radius) + search2 = mLeft; + } + axis = Z_AXIS; + break; + case Z_AXIS: + if (dz <= 0) + { + search1 = mLeft; + if (-dz < radius) + search2 = mRight; + } + else + { + search1 = mRight; + if (dz < radius) + search2 = mLeft; + } + axis = X_AXIS; + break; + } + + double r2 = radius * radius; + double m = dx * dx + dy * dy + dz * dz; + + if (m < r2) + { + switch (count) + { + case 0: + found[count].mNode = this; + found[count].mDistance = m; + break; + case 1: + if (m < found[0].mDistance) + { + if (maxObjects == 1) + { + found[0].mNode = this; + found[0].mDistance = m; + } + else + { + found[1] = found[0]; + found[0].mNode = this; + found[0].mDistance = m; + } + } + else if (maxObjects > 1) + { + found[1].mNode = this; + found[1].mDistance = m; + } + break; + default: + { + bool inserted = false; + + for (uint32_t i = 0; i < count; i++) + { + if (m < found[i].mDistance) // if this one is closer than a pre-existing one... + { + // insertion sort... + uint32_t scan = count; + if (scan >= maxObjects) + scan = maxObjects - 1; + for (uint32_t j = scan; j > i; j--) + { + found[j] = found[j - 1]; + } + found[i].mNode = this; + found[i].mDistance = m; + inserted = true; + break; + } + } + + if (!inserted && count < maxObjects) + { + found[count].mNode = this; + found[count].mDistance = m; + } + } + break; + } + count++; + if (count > maxObjects) + { + count = maxObjects; + } + } + + + if (search1) + search1->search(axis, pos, radius, count, maxObjects, found, iface); + + if (search2) + search2->search(axis, pos, radius, count, maxObjects, found, iface); + } + + void search(Axes axis, + const float* pos, + float radius, + uint32_t& count, + uint32_t maxObjects, + KdTreeFindNode* found, + const KdTreeInterface* iface) + { + + const float* position = iface->getPositionFloat(mIndex); + + float dx = pos[0] - position[0]; + float dy = pos[1] - position[1]; + float dz = pos[2] - position[2]; + + KdTreeNode* search1 = 0; + KdTreeNode* search2 = 0; + + switch (axis) + { + case X_AXIS: + if (dx <= 0) // JWR if we are to the left + { + search1 = mLeft; // JWR then search to the left + if (-dx < radius) // JWR if distance to the right is less than our search radius, continue on the right + // as well. + search2 = mRight; + } + else + { + search1 = mRight; // JWR ok, we go down the left tree + if (dx < radius) // JWR if the distance from the right is less than our search radius + search2 = mLeft; + } + axis = Y_AXIS; + break; + case Y_AXIS: + if (dy <= 0) + { + search1 = mLeft; + if (-dy < radius) + search2 = mRight; + } + else + { + search1 = mRight; + if (dy < radius) + search2 = mLeft; + } + axis = Z_AXIS; + break; + case Z_AXIS: + if (dz <= 0) + { + search1 = mLeft; + if (-dz < radius) + search2 = mRight; + } + else + { + search1 = mRight; + if (dz < radius) + search2 = mLeft; + } + axis = X_AXIS; + break; + } + + float r2 = radius * radius; + float m = dx * dx + dy * dy + dz * dz; + + if (m < r2) + { + switch (count) + { + case 0: + found[count].mNode = this; + found[count].mDistance = m; + break; + case 1: + if (m < found[0].mDistance) + { + if (maxObjects == 1) + { + found[0].mNode = this; + found[0].mDistance = m; + } + else + { + found[1] = found[0]; + found[0].mNode = this; + found[0].mDistance = m; + } + } + else if (maxObjects > 1) + { + found[1].mNode = this; + found[1].mDistance = m; + } + break; + default: + { + bool inserted = false; + + for (uint32_t i = 0; i < count; i++) + { + if (m < found[i].mDistance) // if this one is closer than a pre-existing one... + { + // insertion sort... + uint32_t scan = count; + if (scan >= maxObjects) + scan = maxObjects - 1; + for (uint32_t j = scan; j > i; j--) + { + found[j] = found[j - 1]; + } + found[i].mNode = this; + found[i].mDistance = m; + inserted = true; + break; + } + } + + if (!inserted && count < maxObjects) + { + found[count].mNode = this; + found[count].mDistance = m; + } + } + break; + } + count++; + if (count > maxObjects) + { + count = maxObjects; + } + } + + + if (search1) + search1->search(axis, pos, radius, count, maxObjects, found, iface); + + if (search2) + search2->search(axis, pos, radius, count, maxObjects, found, iface); + } + +private: + void setLeft(KdTreeNode* left) + { + mLeft = left; + }; + void setRight(KdTreeNode* right) + { + mRight = right; + }; + + KdTreeNode* getLeft(void) + { + return mLeft; + } + KdTreeNode* getRight(void) + { + return mRight; + } + + uint32_t mIndex; + KdTreeNode* mLeft; + KdTreeNode* mRight; +}; + + +# define MAX_BUNDLE_SIZE \ + 1024 // 1024 nodes at a time, to minimize memory allocation and guarantee that pointers are persistent. + +class KdTreeNodeBundle +{ +public: + KdTreeNodeBundle(void) + { + mNext = 0; + mIndex = 0; + } + + bool isFull(void) const + { + return (bool)(mIndex == MAX_BUNDLE_SIZE); + } + + KdTreeNode* getNextNode(void) + { + assert(mIndex < MAX_BUNDLE_SIZE); + KdTreeNode* ret = &mNodes[mIndex]; + mIndex++; + return ret; + } + + KdTreeNodeBundle* mNext; + uint32_t mIndex; + KdTreeNode mNodes[MAX_BUNDLE_SIZE]; +}; + + +typedef std::vector DoubleVector; +typedef std::vector FloatVector; + +class KdTree : public KdTreeInterface +{ +public: + KdTree(void) + { + mRoot = 0; + mBundle = 0; + mVcount = 0; + mUseDouble = false; + } + + virtual ~KdTree(void) + { + reset(); + } + + const double* getPositionDouble(uint32_t index) const + { + assert(mUseDouble); + assert(index < mVcount); + return &mVerticesDouble[index * 3]; + } + + const float* getPositionFloat(uint32_t index) const + { + assert(!mUseDouble); + assert(index < mVcount); + return &mVerticesFloat[index * 3]; + } + + uint32_t search(const double* pos, double radius, uint32_t maxObjects, KdTreeFindNode* found) const + { + assert(mUseDouble); + if (!mRoot) + return 0; + uint32_t count = 0; + mRoot->search(X_AXIS, pos, radius, count, maxObjects, found, this); + return count; + } + + uint32_t search(const float* pos, float radius, uint32_t maxObjects, KdTreeFindNode* found) const + { + assert(!mUseDouble); + if (!mRoot) + return 0; + uint32_t count = 0; + mRoot->search(X_AXIS, pos, radius, count, maxObjects, found, this); + return count; + } + + void reset(void) + { + mRoot = 0; + mVerticesDouble.clear(); + mVerticesFloat.clear(); + KdTreeNodeBundle* bundle = mBundle; + while (bundle) + { + KdTreeNodeBundle* next = bundle->mNext; + delete bundle; + bundle = next; + } + mBundle = 0; + mVcount = 0; + } + + uint32_t add(double x, double y, double z) + { + assert(mUseDouble); + uint32_t ret = mVcount; + mVerticesDouble.push_back(x); + mVerticesDouble.push_back(y); + mVerticesDouble.push_back(z); + mVcount++; + KdTreeNode* node = getNewNode(ret); + if (mRoot) + { + mRoot->addDouble(node, X_AXIS, this); + } + else + { + mRoot = node; + } + return ret; + } + + uint32_t add(float x, float y, float z) + { + assert(!mUseDouble); + uint32_t ret = mVcount; + mVerticesFloat.push_back(x); + mVerticesFloat.push_back(y); + mVerticesFloat.push_back(z); + mVcount++; + KdTreeNode* node = getNewNode(ret); + if (mRoot) + { + mRoot->addFloat(node, X_AXIS, this); + } + else + { + mRoot = node; + } + return ret; + } + + KdTreeNode* getNewNode(uint32_t index) + { + if (mBundle == 0) + { + mBundle = new KdTreeNodeBundle; + } + if (mBundle->isFull()) + { + KdTreeNodeBundle* bundle = new KdTreeNodeBundle; + mBundle->mNext = bundle; + mBundle = bundle; + } + KdTreeNode* node = mBundle->getNextNode(); + new (node) KdTreeNode(index); + return node; + } + + uint32_t getNearest(const double* pos, double radius, bool& _found) const // returns the nearest possible neighbor's + // index. + { + assert(mUseDouble); + uint32_t ret = 0; + + _found = false; + KdTreeFindNode found[1]; + uint32_t count = search(pos, radius, 1, found); + if (count) + { + KdTreeNode* node = found[0].mNode; + ret = node->getIndex(); + _found = true; + } + return ret; + } + + uint32_t getNearest(const float* pos, float radius, bool& _found) const // returns the nearest possible neighbor's + // index. + { + assert(!mUseDouble); + uint32_t ret = 0; + + _found = false; + KdTreeFindNode found[1]; + uint32_t count = search(pos, radius, 1, found); + if (count) + { + KdTreeNode* node = found[0].mNode; + ret = node->getIndex(); + _found = true; + } + return ret; + } + + const double* getVerticesDouble(void) const + { + assert(mUseDouble); + const double* ret = 0; + if (!mVerticesDouble.empty()) + { + ret = &mVerticesDouble[0]; + } + return ret; + } + + const float* getVerticesFloat(void) const + { + assert(!mUseDouble); + const float* ret = 0; + if (!mVerticesFloat.empty()) + { + ret = &mVerticesFloat[0]; + } + return ret; + } + + uint32_t getVcount(void) const + { + return mVcount; + }; + + void setUseDouble(bool useDouble) + { + mUseDouble = useDouble; + } + +private: + bool mUseDouble; + KdTreeNode* mRoot; + KdTreeNodeBundle* mBundle; + uint32_t mVcount; + DoubleVector mVerticesDouble; + FloatVector mVerticesFloat; +}; + +}; // end of namespace VERTEX_INDEX + +class MyVertexIndex : public fm_VertexIndex +{ +public: + MyVertexIndex(double granularity, bool snapToGrid) + { + mDoubleGranularity = granularity; + mFloatGranularity = (float)granularity; + mSnapToGrid = snapToGrid; + mUseDouble = true; + mKdTree.setUseDouble(true); + } + + MyVertexIndex(float granularity, bool snapToGrid) + { + mDoubleGranularity = granularity; + mFloatGranularity = (float)granularity; + mSnapToGrid = snapToGrid; + mUseDouble = false; + mKdTree.setUseDouble(false); + } + + virtual ~MyVertexIndex(void) + { + } + + + double snapToGrid(double p) + { + double m = fmod(p, mDoubleGranularity); + p -= m; + return p; + } + + float snapToGrid(float p) + { + float m = fmodf(p, mFloatGranularity); + p -= m; + return p; + } + + uint32_t getIndex(const float* _p, bool& newPos) // get index for a vector float + { + uint32_t ret; + + if (mUseDouble) + { + double p[3]; + p[0] = _p[0]; + p[1] = _p[1]; + p[2] = _p[2]; + return getIndex(p, newPos); + } + + newPos = false; + + float p[3]; + + if (mSnapToGrid) + { + p[0] = snapToGrid(_p[0]); + p[1] = snapToGrid(_p[1]); + p[2] = snapToGrid(_p[2]); + } + else + { + p[0] = _p[0]; + p[1] = _p[1]; + p[2] = _p[2]; + } + + bool found; + ret = mKdTree.getNearest(p, mFloatGranularity, found); + if (!found) + { + newPos = true; + ret = mKdTree.add(p[0], p[1], p[2]); + } + + + return ret; + } + + uint32_t getIndex(const double* _p, bool& newPos) // get index for a vector double + { + uint32_t ret; + + if (!mUseDouble) + { + float p[3]; + p[0] = (float)_p[0]; + p[1] = (float)_p[1]; + p[2] = (float)_p[2]; + return getIndex(p, newPos); + } + + newPos = false; + + double p[3]; + + if (mSnapToGrid) + { + p[0] = snapToGrid(_p[0]); + p[1] = snapToGrid(_p[1]); + p[2] = snapToGrid(_p[2]); + } + else + { + p[0] = _p[0]; + p[1] = _p[1]; + p[2] = _p[2]; + } + + bool found; + ret = mKdTree.getNearest(p, mDoubleGranularity, found); + if (!found) + { + newPos = true; + ret = mKdTree.add(p[0], p[1], p[2]); + } + + + return ret; + } + + const float* getVerticesFloat(void) const + { + const float* ret = 0; + + assert(!mUseDouble); + + ret = mKdTree.getVerticesFloat(); + + return ret; + } + + const double* getVerticesDouble(void) const + { + const double* ret = 0; + + assert(mUseDouble); + + ret = mKdTree.getVerticesDouble(); + + return ret; + } + + const float* getVertexFloat(uint32_t index) const + { + const float* ret = 0; + assert(!mUseDouble); +# ifdef _DEBUG + uint32_t vcount = mKdTree.getVcount(); + assert(index < vcount); +# endif + ret = mKdTree.getVerticesFloat(); + ret = &ret[index * 3]; + return ret; + } + + const double* getVertexDouble(uint32_t index) const + { + const double* ret = 0; + assert(mUseDouble); +# ifdef _DEBUG + uint32_t vcount = mKdTree.getVcount(); + assert(index < vcount); +# endif + ret = mKdTree.getVerticesDouble(); + ret = &ret[index * 3]; + + return ret; + } + + uint32_t getVcount(void) const + { + return mKdTree.getVcount(); + } + + bool isDouble(void) const + { + return mUseDouble; + } + + + bool saveAsObj(const char* fname, uint32_t tcount, uint32_t* indices) + { + bool ret = false; + + + FILE* fph = fopen(fname, "wb"); + if (fph) + { + ret = true; + + uint32_t vcount = getVcount(); + if (mUseDouble) + { + const double* v = getVerticesDouble(); + for (uint32_t i = 0; i < vcount; i++) + { + fprintf(fph, "v %0.9f %0.9f %0.9f\r\n", (float)v[0], (float)v[1], (float)v[2]); + v += 3; + } + } + else + { + const float* v = getVerticesFloat(); + for (uint32_t i = 0; i < vcount; i++) + { + fprintf(fph, "v %0.9f %0.9f %0.9f\r\n", v[0], v[1], v[2]); + v += 3; + } + } + + for (uint32_t i = 0; i < tcount; i++) + { + uint32_t i1 = *indices++; + uint32_t i2 = *indices++; + uint32_t i3 = *indices++; + fprintf(fph, "f %d %d %d\r\n", i1 + 1, i2 + 1, i3 + 1); + } + fclose(fph); + } + + return ret; + } + +private: + bool mUseDouble : 1; + bool mSnapToGrid : 1; + double mDoubleGranularity; + float mFloatGranularity; + VERTEX_INDEX::KdTree mKdTree; +}; + +fm_VertexIndex* fm_createVertexIndex(double granularity, bool snapToGrid) // create an indexed vertex system for doubles +{ + MyVertexIndex* ret = new MyVertexIndex(granularity, snapToGrid); + return static_cast(ret); +} + +fm_VertexIndex* fm_createVertexIndex(float granularity, bool snapToGrid) // create an indexed vertext system for floats +{ + MyVertexIndex* ret = new MyVertexIndex(granularity, snapToGrid); + return static_cast(ret); +} + +void fm_releaseVertexIndex(fm_VertexIndex* vindex) +{ + MyVertexIndex* m = static_cast(vindex); + delete m; +} + +#endif // END OF VERTEX WELDING CODE + + +REAL fm_computeBestFitAABB(uint32_t vcount, const REAL* points, uint32_t pstride, REAL* bmin, REAL* bmax) // returns the + // diagonal + // distance +{ + + const uint8_t* source = (const uint8_t*)points; + + bmin[0] = points[0]; + bmin[1] = points[1]; + bmin[2] = points[2]; + + bmax[0] = points[0]; + bmax[1] = points[1]; + bmax[2] = points[2]; + + + for (uint32_t i = 1; i < vcount; i++) + { + source += pstride; + const REAL* p = (const REAL*)source; + + if (p[0] < bmin[0]) + bmin[0] = p[0]; + if (p[1] < bmin[1]) + bmin[1] = p[1]; + if (p[2] < bmin[2]) + bmin[2] = p[2]; + + if (p[0] > bmax[0]) + bmax[0] = p[0]; + if (p[1] > bmax[1]) + bmax[1] = p[1]; + if (p[2] > bmax[2]) + bmax[2] = p[2]; + } + + REAL dx = bmax[0] - bmin[0]; + REAL dy = bmax[1] - bmin[1]; + REAL dz = bmax[2] - bmin[2]; + + return (REAL)sqrt(dx * dx + dy * dy + dz * dz); +} + + +/* a = b - c */ +#define vector(a, b, c) \ + (a)[0] = (b)[0] - (c)[0]; \ + (a)[1] = (b)[1] - (c)[1]; \ + (a)[2] = (b)[2] - (c)[2]; + + +#define innerProduct(v, q) ((v)[0] * (q)[0] + (v)[1] * (q)[1] + (v)[2] * (q)[2]) + +#define crossProduct(a, b, c) \ + (a)[0] = (b)[1] * (c)[2] - (c)[1] * (b)[2]; \ + (a)[1] = (b)[2] * (c)[0] - (c)[2] * (b)[0]; \ + (a)[2] = (b)[0] * (c)[1] - (c)[0] * (b)[1]; + + +bool fm_lineIntersectsTriangle( + const REAL* rayStart, const REAL* rayEnd, const REAL* p1, const REAL* p2, const REAL* p3, REAL* sect) +{ + REAL dir[3]; + + dir[0] = rayEnd[0] - rayStart[0]; + dir[1] = rayEnd[1] - rayStart[1]; + dir[2] = rayEnd[2] - rayStart[2]; + + REAL d = (REAL)sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]); + REAL r = 1.0f / d; + + dir[0] *= r; + dir[1] *= r; + dir[2] *= r; + + + REAL t; + + bool ret = fm_rayIntersectsTriangle(rayStart, dir, p1, p2, p3, t); + + if (ret) + { + if (t > d) + { + sect[0] = rayStart[0] + dir[0] * t; + sect[1] = rayStart[1] + dir[1] * t; + sect[2] = rayStart[2] + dir[2] * t; + } + else + { + ret = false; + } + } + + return ret; +} + + +bool fm_rayIntersectsTriangle(const REAL* p, const REAL* d, const REAL* v0, const REAL* v1, const REAL* v2, REAL& t) +{ + REAL e1[3], e2[3], h[3], s[3], q[3]; + REAL a, f, u, v; + + vector(e1, v1, v0); + vector(e2, v2, v0); + crossProduct(h, d, e2); + a = innerProduct(e1, h); + + if (a > -0.00001 && a < 0.00001) + return (false); + + f = 1 / a; + vector(s, p, v0); + u = f * (innerProduct(s, h)); + + if (u < 0.0 || u > 1.0) + return (false); + + crossProduct(q, s, e1); + v = f * innerProduct(d, q); + if (v < 0.0 || u + v > 1.0) + return (false); + // at this stage we can compute t to find out where + // the intersection point is on the line + t = f * innerProduct(e2, q); + if (t > 0) // ray intersection + return (true); + else // this means that there is a line intersection + // but not a ray intersection + return (false); +} + + +inline REAL det(const REAL* p1, const REAL* p2, const REAL* p3) +{ + return p1[0] * p2[1] * p3[2] + p2[0] * p3[1] * p1[2] + p3[0] * p1[1] * p2[2] - p1[0] * p3[1] * p2[2] - + p2[0] * p1[1] * p3[2] - p3[0] * p2[1] * p1[2]; +} + + +REAL fm_computeMeshVolume(const REAL* vertices, uint32_t tcount, const uint32_t* indices) +{ + REAL volume = 0; + + for (uint32_t i = 0; i < tcount; i++, indices += 3) + { + const REAL* p1 = &vertices[indices[0] * 3]; + const REAL* p2 = &vertices[indices[1] * 3]; + const REAL* p3 = &vertices[indices[2] * 3]; + volume += det(p1, p2, p3); // compute the volume of the tetrahedran relative to the origin. + } + + volume *= (1.0f / 6.0f); + if (volume < 0) + volume *= -1; + return volume; +} + + +const REAL* fm_getPoint(const REAL* points, uint32_t pstride, uint32_t index) +{ + const uint8_t* scan = (const uint8_t*)points; + scan += (index * pstride); + return (REAL*)scan; +} + + +bool fm_insideTriangle(REAL Ax, REAL Ay, REAL Bx, REAL By, REAL Cx, REAL Cy, REAL Px, REAL Py) + +{ + REAL ax, ay, bx, by, cx, cy, apx, apy, bpx, bpy, cpx, cpy; + REAL cCROSSap, bCROSScp, aCROSSbp; + + ax = Cx - Bx; + ay = Cy - By; + bx = Ax - Cx; + by = Ay - Cy; + cx = Bx - Ax; + cy = By - Ay; + apx = Px - Ax; + apy = Py - Ay; + bpx = Px - Bx; + bpy = Py - By; + cpx = Px - Cx; + cpy = Py - Cy; + + aCROSSbp = ax * bpy - ay * bpx; + cCROSSap = cx * apy - cy * apx; + bCROSScp = bx * cpy - by * cpx; + + return ((aCROSSbp >= 0.0f) && (bCROSScp >= 0.0f) && (cCROSSap >= 0.0f)); +} + + +REAL fm_areaPolygon2d(uint32_t pcount, const REAL* points, uint32_t pstride) +{ + int32_t n = (int32_t)pcount; + + REAL A = 0.0f; + for (int32_t p = n - 1, q = 0; q < n; p = q++) + { + const REAL* p1 = fm_getPoint(points, pstride, p); + const REAL* p2 = fm_getPoint(points, pstride, q); + A += p1[0] * p2[1] - p2[0] * p1[1]; + } + return A * 0.5f; +} + + +bool fm_pointInsidePolygon2d( + uint32_t pcount, const REAL* points, uint32_t pstride, const REAL* point, uint32_t xindex, uint32_t yindex) +{ + uint32_t j = pcount - 1; + int32_t oddNodes = 0; + + REAL x = point[xindex]; + REAL y = point[yindex]; + + for (uint32_t i = 0; i < pcount; i++) + { + const REAL* p1 = fm_getPoint(points, pstride, i); + const REAL* p2 = fm_getPoint(points, pstride, j); + + REAL x1 = p1[xindex]; + REAL y1 = p1[yindex]; + + REAL x2 = p2[xindex]; + REAL y2 = p2[yindex]; + + if ((y1 < y && y2 >= y) || (y2 < y && y1 >= y)) + { + if (x1 + (y - y1) / (y2 - y1) * (x2 - x1) < x) + { + oddNodes = 1 - oddNodes; + } + } + j = i; + } + + return oddNodes ? true : false; +} + + +uint32_t fm_consolidatePolygon(uint32_t pcount, const REAL* points, uint32_t pstride, REAL* _dest, REAL epsilon) // collapses + // co-linear + // edges. +{ + uint32_t ret = 0; + + + if (pcount >= 3) + { + const REAL* prev = fm_getPoint(points, pstride, pcount - 1); + const REAL* current = points; + const REAL* next = fm_getPoint(points, pstride, 1); + REAL* dest = _dest; + + for (uint32_t i = 0; i < pcount; i++) + { + + next = (i + 1) == pcount ? points : next; + + if (!fm_colinear(prev, current, next, epsilon)) + { + dest[0] = current[0]; + dest[1] = current[1]; + dest[2] = current[2]; + + dest += 3; + ret++; + } + + prev = current; + current += 3; + next += 3; + } + } + + return ret; +} + + +#ifndef RECT3D_TEMPLATE + +# define RECT3D_TEMPLATE + +template +class Rect3d +{ +public: + Rect3d(void){}; + + Rect3d(const T* bmin, const T* bmax) + { + + mMin[0] = bmin[0]; + mMin[1] = bmin[1]; + mMin[2] = bmin[2]; + + mMax[0] = bmax[0]; + mMax[1] = bmax[1]; + mMax[2] = bmax[2]; + } + + void SetMin(const T* bmin) + { + mMin[0] = bmin[0]; + mMin[1] = bmin[1]; + mMin[2] = bmin[2]; + } + + void SetMax(const T* bmax) + { + mMax[0] = bmax[0]; + mMax[1] = bmax[1]; + mMax[2] = bmax[2]; + } + + void SetMin(T x, T y, T z) + { + mMin[0] = x; + mMin[1] = y; + mMin[2] = z; + } + + void SetMax(T x, T y, T z) + { + mMax[0] = x; + mMax[1] = y; + mMax[2] = z; + } + + T mMin[3]; + T mMax[3]; +}; + +#endif + +void splitRect(uint32_t axis, const Rect3d& source, Rect3d& b1, Rect3d& b2, const REAL* midpoint) +{ + switch (axis) + { + case 0: + b1.SetMin(source.mMin); + b1.SetMax(midpoint[0], source.mMax[1], source.mMax[2]); + + b2.SetMin(midpoint[0], source.mMin[1], source.mMin[2]); + b2.SetMax(source.mMax); + + break; + case 1: + b1.SetMin(source.mMin); + b1.SetMax(source.mMax[0], midpoint[1], source.mMax[2]); + + b2.SetMin(source.mMin[0], midpoint[1], source.mMin[2]); + b2.SetMax(source.mMax); + + break; + case 2: + b1.SetMin(source.mMin); + b1.SetMax(source.mMax[0], source.mMax[1], midpoint[2]); + + b2.SetMin(source.mMin[0], source.mMin[1], midpoint[2]); + b2.SetMax(source.mMax); + + break; + } +} + +bool fm_computeSplitPlane( + uint32_t vcount, const REAL* vertices, uint32_t /* tcount */, const uint32_t* /* indices */, REAL* plane) +{ + + REAL sides[3]; + REAL matrix[16]; + + fm_computeBestFitOBB(vcount, vertices, sizeof(REAL) * 3, sides, matrix); + + REAL bmax[3]; + REAL bmin[3]; + + bmax[0] = sides[0] * 0.5f; + bmax[1] = sides[1] * 0.5f; + bmax[2] = sides[2] * 0.5f; + + bmin[0] = -bmax[0]; + bmin[1] = -bmax[1]; + bmin[2] = -bmax[2]; + + + REAL dx = sides[0]; + REAL dy = sides[1]; + REAL dz = sides[2]; + + + uint32_t axis = 0; + + if (dy > dx) + { + axis = 1; + } + + if (dz > dx && dz > dy) + { + axis = 2; + } + + REAL p1[3]; + REAL p2[3]; + REAL p3[3]; + + p3[0] = p2[0] = p1[0] = bmin[0] + dx * 0.5f; + p3[1] = p2[1] = p1[1] = bmin[1] + dy * 0.5f; + p3[2] = p2[2] = p1[2] = bmin[2] + dz * 0.5f; + + Rect3d b(bmin, bmax); + + Rect3d b1, b2; + + splitRect(axis, b, b1, b2, p1); + + + switch (axis) + { + case 0: + p2[1] = bmin[1]; + p2[2] = bmin[2]; + + if (dz > dy) + { + p3[1] = bmax[1]; + p3[2] = bmin[2]; + } + else + { + p3[1] = bmin[1]; + p3[2] = bmax[2]; + } + + break; + case 1: + p2[0] = bmin[0]; + p2[2] = bmin[2]; + + if (dx > dz) + { + p3[0] = bmax[0]; + p3[2] = bmin[2]; + } + else + { + p3[0] = bmin[0]; + p3[2] = bmax[2]; + } + + break; + case 2: + p2[0] = bmin[0]; + p2[1] = bmin[1]; + + if (dx > dy) + { + p3[0] = bmax[0]; + p3[1] = bmin[1]; + } + else + { + p3[0] = bmin[0]; + p3[1] = bmax[1]; + } + + break; + } + + REAL tp1[3]; + REAL tp2[3]; + REAL tp3[3]; + + fm_transform(matrix, p1, tp1); + fm_transform(matrix, p2, tp2); + fm_transform(matrix, p3, tp3); + + plane[3] = fm_computePlane(tp1, tp2, tp3, plane); + + return true; +} + +#pragma warning(disable : 4100) + +void fm_nearestPointInTriangle( + const REAL* /*nearestPoint*/, const REAL* /*p1*/, const REAL* /*p2*/, const REAL* /*p3*/, REAL* /*nearest*/) +{ +} + +static REAL Partial(const REAL* a, const REAL* p) +{ + return (a[0] * p[1]) - (p[0] * a[1]); +} + +REAL fm_areaTriangle(const REAL* p0, const REAL* p1, const REAL* p2) +{ + REAL A = Partial(p0, p1); + A += Partial(p1, p2); + A += Partial(p2, p0); + return A * 0.5f; +} + +void fm_subtract(const REAL* A, const REAL* B, REAL* diff) // compute A-B and store the result in 'diff' +{ + diff[0] = A[0] - B[0]; + diff[1] = A[1] - B[1]; + diff[2] = A[2] - B[2]; +} + + +void fm_multiplyTransform(const REAL* pA, const REAL* pB, REAL* pM) +{ + + REAL a = pA[0 * 4 + 0] * pB[0 * 4 + 0] + pA[0 * 4 + 1] * pB[1 * 4 + 0] + pA[0 * 4 + 2] * pB[2 * 4 + 0] + + pA[0 * 4 + 3] * pB[3 * 4 + 0]; + REAL b = pA[0 * 4 + 0] * pB[0 * 4 + 1] + pA[0 * 4 + 1] * pB[1 * 4 + 1] + pA[0 * 4 + 2] * pB[2 * 4 + 1] + + pA[0 * 4 + 3] * pB[3 * 4 + 1]; + REAL c = pA[0 * 4 + 0] * pB[0 * 4 + 2] + pA[0 * 4 + 1] * pB[1 * 4 + 2] + pA[0 * 4 + 2] * pB[2 * 4 + 2] + + pA[0 * 4 + 3] * pB[3 * 4 + 2]; + REAL d = pA[0 * 4 + 0] * pB[0 * 4 + 3] + pA[0 * 4 + 1] * pB[1 * 4 + 3] + pA[0 * 4 + 2] * pB[2 * 4 + 3] + + pA[0 * 4 + 3] * pB[3 * 4 + 3]; + + REAL e = pA[1 * 4 + 0] * pB[0 * 4 + 0] + pA[1 * 4 + 1] * pB[1 * 4 + 0] + pA[1 * 4 + 2] * pB[2 * 4 + 0] + + pA[1 * 4 + 3] * pB[3 * 4 + 0]; + REAL f = pA[1 * 4 + 0] * pB[0 * 4 + 1] + pA[1 * 4 + 1] * pB[1 * 4 + 1] + pA[1 * 4 + 2] * pB[2 * 4 + 1] + + pA[1 * 4 + 3] * pB[3 * 4 + 1]; + REAL g = pA[1 * 4 + 0] * pB[0 * 4 + 2] + pA[1 * 4 + 1] * pB[1 * 4 + 2] + pA[1 * 4 + 2] * pB[2 * 4 + 2] + + pA[1 * 4 + 3] * pB[3 * 4 + 2]; + REAL h = pA[1 * 4 + 0] * pB[0 * 4 + 3] + pA[1 * 4 + 1] * pB[1 * 4 + 3] + pA[1 * 4 + 2] * pB[2 * 4 + 3] + + pA[1 * 4 + 3] * pB[3 * 4 + 3]; + + REAL i = pA[2 * 4 + 0] * pB[0 * 4 + 0] + pA[2 * 4 + 1] * pB[1 * 4 + 0] + pA[2 * 4 + 2] * pB[2 * 4 + 0] + + pA[2 * 4 + 3] * pB[3 * 4 + 0]; + REAL j = pA[2 * 4 + 0] * pB[0 * 4 + 1] + pA[2 * 4 + 1] * pB[1 * 4 + 1] + pA[2 * 4 + 2] * pB[2 * 4 + 1] + + pA[2 * 4 + 3] * pB[3 * 4 + 1]; + REAL k = pA[2 * 4 + 0] * pB[0 * 4 + 2] + pA[2 * 4 + 1] * pB[1 * 4 + 2] + pA[2 * 4 + 2] * pB[2 * 4 + 2] + + pA[2 * 4 + 3] * pB[3 * 4 + 2]; + REAL l = pA[2 * 4 + 0] * pB[0 * 4 + 3] + pA[2 * 4 + 1] * pB[1 * 4 + 3] + pA[2 * 4 + 2] * pB[2 * 4 + 3] + + pA[2 * 4 + 3] * pB[3 * 4 + 3]; + + REAL m = pA[3 * 4 + 0] * pB[0 * 4 + 0] + pA[3 * 4 + 1] * pB[1 * 4 + 0] + pA[3 * 4 + 2] * pB[2 * 4 + 0] + + pA[3 * 4 + 3] * pB[3 * 4 + 0]; + REAL n = pA[3 * 4 + 0] * pB[0 * 4 + 1] + pA[3 * 4 + 1] * pB[1 * 4 + 1] + pA[3 * 4 + 2] * pB[2 * 4 + 1] + + pA[3 * 4 + 3] * pB[3 * 4 + 1]; + REAL o = pA[3 * 4 + 0] * pB[0 * 4 + 2] + pA[3 * 4 + 1] * pB[1 * 4 + 2] + pA[3 * 4 + 2] * pB[2 * 4 + 2] + + pA[3 * 4 + 3] * pB[3 * 4 + 2]; + REAL p = pA[3 * 4 + 0] * pB[0 * 4 + 3] + pA[3 * 4 + 1] * pB[1 * 4 + 3] + pA[3 * 4 + 2] * pB[2 * 4 + 3] + + pA[3 * 4 + 3] * pB[3 * 4 + 3]; + + pM[0] = a; + pM[1] = b; + pM[2] = c; + pM[3] = d; + + pM[4] = e; + pM[5] = f; + pM[6] = g; + pM[7] = h; + + pM[8] = i; + pM[9] = j; + pM[10] = k; + pM[11] = l; + + pM[12] = m; + pM[13] = n; + pM[14] = o; + pM[15] = p; +} + +void fm_multiply(REAL* A, REAL scaler) +{ + A[0] *= scaler; + A[1] *= scaler; + A[2] *= scaler; +} + +void fm_add(const REAL* A, const REAL* B, REAL* sum) +{ + sum[0] = A[0] + B[0]; + sum[1] = A[1] + B[1]; + sum[2] = A[2] + B[2]; +} + +void fm_copy3(const REAL* source, REAL* dest) +{ + dest[0] = source[0]; + dest[1] = source[1]; + dest[2] = source[2]; +} + + +uint32_t fm_copyUniqueVertices(uint32_t vcount, + const REAL* input_vertices, + REAL* output_vertices, + uint32_t tcount, + const uint32_t* input_indices, + uint32_t* output_indices) +{ + uint32_t ret = 0; + + REAL* vertices = (REAL*)malloc(sizeof(REAL) * vcount * 3); + memcpy(vertices, input_vertices, sizeof(REAL) * vcount * 3); + REAL* dest = output_vertices; + + uint32_t* reindex = (uint32_t*)malloc(sizeof(uint32_t) * vcount); + memset(reindex, 0xFF, sizeof(uint32_t) * vcount); + + uint32_t icount = tcount * 3; + + for (uint32_t i = 0; i < icount; i++) + { + uint32_t index = *input_indices++; + + assert(index < vcount); + + if (reindex[index] == 0xFFFFFFFF) + { + *output_indices++ = ret; + reindex[index] = ret; + const REAL* pos = &vertices[index * 3]; + dest[0] = pos[0]; + dest[1] = pos[1]; + dest[2] = pos[2]; + dest += 3; + ret++; + } + else + { + *output_indices++ = reindex[index]; + } + } + free(vertices); + free(reindex); + return ret; +} + +bool fm_isMeshCoplanar(uint32_t tcount, const uint32_t* indices, const REAL* vertices, bool doubleSided) // returns true + // if this + // collection + // of indexed + // triangles + // are + // co-planar! +{ + bool ret = true; + + if (tcount > 0) + { + uint32_t i1 = indices[0]; + uint32_t i2 = indices[1]; + uint32_t i3 = indices[2]; + const REAL* p1 = &vertices[i1 * 3]; + const REAL* p2 = &vertices[i2 * 3]; + const REAL* p3 = &vertices[i3 * 3]; + REAL plane[4]; + plane[3] = fm_computePlane(p1, p2, p3, plane); + const uint32_t* scan = &indices[3]; + for (uint32_t i = 1; i < tcount; i++) + { + i1 = *scan++; + i2 = *scan++; + i3 = *scan++; + p1 = &vertices[i1 * 3]; + p2 = &vertices[i2 * 3]; + p3 = &vertices[i3 * 3]; + REAL _plane[4]; + _plane[3] = fm_computePlane(p1, p2, p3, _plane); + if (!fm_samePlane(plane, _plane, 0.01f, 0.001f, doubleSided)) + { + ret = false; + break; + } + } + } + return ret; +} + + +bool fm_samePlane(const REAL p1[4], const REAL p2[4], REAL normalEpsilon, REAL dEpsilon, bool doubleSided) +{ + bool ret = false; + +#if 0 + if (p1[0] == p2[0] && + p1[1] == p2[1] && + p1[2] == p2[2] && + p1[3] == p2[3]) + { + ret = true; + } +#else + REAL diff = (REAL)fabs(p1[3] - p2[3]); + if (diff < dEpsilon) // if the plane -d co-efficient is within our epsilon + { + REAL dot = fm_dot(p1, p2); // compute the dot-product of the vector normals. + if (doubleSided) + dot = (REAL)fabs(dot); + REAL dmin = 1 - normalEpsilon; + REAL dmax = 1 + normalEpsilon; + if (dot >= dmin && dot <= dmax) + { + ret = true; // then the plane equation is for practical purposes identical. + } + } +#endif + return ret; +} + + +void fm_initMinMax(REAL bmin[3], REAL bmax[3]) +{ + bmin[0] = FLT_MAX; + bmin[1] = FLT_MAX; + bmin[2] = FLT_MAX; + + bmax[0] = -FLT_MAX; + bmax[1] = -FLT_MAX; + bmax[2] = -FLT_MAX; +} + +void fm_inflateMinMax(REAL bmin[3], REAL bmax[3], REAL ratio) +{ + REAL inflate = fm_distance(bmin, bmax) * 0.5f * ratio; + + bmin[0] -= inflate; + bmin[1] -= inflate; + bmin[2] -= inflate; + + bmax[0] += inflate; + bmax[1] += inflate; + bmax[2] += inflate; +} + +#ifndef TESSELATE_H + +# define TESSELATE_H + +typedef std::vector UintVector; + +class Myfm_Tesselate : public fm_Tesselate +{ +public: + virtual ~Myfm_Tesselate(void) + { + } + + const uint32_t* tesselate(fm_VertexIndex* vindex, + uint32_t tcount, + const uint32_t* indices, + float longEdge, + uint32_t maxDepth, + uint32_t& outcount) + { + const uint32_t* ret = 0; + + mMaxDepth = maxDepth; + mLongEdge = longEdge * longEdge; + mLongEdgeD = mLongEdge; + mVertices = vindex; + + if (mVertices->isDouble()) + { + uint32_t vcount = mVertices->getVcount(); + double* vertices = (double*)malloc(sizeof(double) * vcount * 3); + memcpy(vertices, mVertices->getVerticesDouble(), sizeof(double) * vcount * 3); + + for (uint32_t i = 0; i < tcount; i++) + { + uint32_t i1 = *indices++; + uint32_t i2 = *indices++; + uint32_t i3 = *indices++; + + const double* p1 = &vertices[i1 * 3]; + const double* p2 = &vertices[i2 * 3]; + const double* p3 = &vertices[i3 * 3]; + + tesselate(p1, p2, p3, 0); + } + free(vertices); + } + else + { + uint32_t vcount = mVertices->getVcount(); + float* vertices = (float*)malloc(sizeof(float) * vcount * 3); + memcpy(vertices, mVertices->getVerticesFloat(), sizeof(float) * vcount * 3); + + + for (uint32_t i = 0; i < tcount; i++) + { + uint32_t i1 = *indices++; + uint32_t i2 = *indices++; + uint32_t i3 = *indices++; + + const float* p1 = &vertices[i1 * 3]; + const float* p2 = &vertices[i2 * 3]; + const float* p3 = &vertices[i3 * 3]; + + tesselate(p1, p2, p3, 0); + } + free(vertices); + } + + outcount = (uint32_t)(mIndices.size() / 3); + ret = &mIndices[0]; + + + return ret; + } + + void tesselate(const float* p1, const float* p2, const float* p3, uint32_t recurse) + { + bool split = false; + float l1, l2, l3; + + l1 = l2 = l3 = 0; + + if (recurse < mMaxDepth) + { + l1 = fm_distanceSquared(p1, p2); + l2 = fm_distanceSquared(p2, p3); + l3 = fm_distanceSquared(p3, p1); + + if (l1 > mLongEdge || l2 > mLongEdge || l3 > mLongEdge) + split = true; + } + + if (split) + { + uint32_t edge; + + if (l1 >= l2 && l1 >= l3) + edge = 0; + else if (l2 >= l1 && l2 >= l3) + edge = 1; + else + edge = 2; + + float splits[3]; + + switch (edge) + { + case 0: + { + fm_lerp(p1, p2, splits, 0.5f); + tesselate(p1, splits, p3, recurse + 1); + tesselate(splits, p2, p3, recurse + 1); + } + break; + case 1: + { + fm_lerp(p2, p3, splits, 0.5f); + tesselate(p1, p2, splits, recurse + 1); + tesselate(p1, splits, p3, recurse + 1); + } + break; + case 2: + { + fm_lerp(p3, p1, splits, 0.5f); + tesselate(p1, p2, splits, recurse + 1); + tesselate(splits, p2, p3, recurse + 1); + } + break; + } + } + else + { + bool newp; + + uint32_t i1 = mVertices->getIndex(p1, newp); + uint32_t i2 = mVertices->getIndex(p2, newp); + uint32_t i3 = mVertices->getIndex(p3, newp); + + mIndices.push_back(i1); + mIndices.push_back(i2); + mIndices.push_back(i3); + } + } + + void tesselate(const double* p1, const double* p2, const double* p3, uint32_t recurse) + { + bool split = false; + double l1, l2, l3; + + l1 = l2 = l3 = 0; + + if (recurse < mMaxDepth) + { + l1 = fm_distanceSquared(p1, p2); + l2 = fm_distanceSquared(p2, p3); + l3 = fm_distanceSquared(p3, p1); + + if (l1 > mLongEdgeD || l2 > mLongEdgeD || l3 > mLongEdgeD) + split = true; + } + + if (split) + { + uint32_t edge; + + if (l1 >= l2 && l1 >= l3) + edge = 0; + else if (l2 >= l1 && l2 >= l3) + edge = 1; + else + edge = 2; + + double splits[3]; + + switch (edge) + { + case 0: + { + fm_lerp(p1, p2, splits, 0.5); + tesselate(p1, splits, p3, recurse + 1); + tesselate(splits, p2, p3, recurse + 1); + } + break; + case 1: + { + fm_lerp(p2, p3, splits, 0.5); + tesselate(p1, p2, splits, recurse + 1); + tesselate(p1, splits, p3, recurse + 1); + } + break; + case 2: + { + fm_lerp(p3, p1, splits, 0.5); + tesselate(p1, p2, splits, recurse + 1); + tesselate(splits, p2, p3, recurse + 1); + } + break; + } + } + else + { + bool newp; + + uint32_t i1 = mVertices->getIndex(p1, newp); + uint32_t i2 = mVertices->getIndex(p2, newp); + uint32_t i3 = mVertices->getIndex(p3, newp); + + mIndices.push_back(i1); + mIndices.push_back(i2); + mIndices.push_back(i3); + } + } + +private: + float mLongEdge; + double mLongEdgeD; + fm_VertexIndex* mVertices; + UintVector mIndices; + uint32_t mMaxDepth; +}; + +fm_Tesselate* fm_createTesselate(void) +{ + Myfm_Tesselate* m = new Myfm_Tesselate; + return static_cast(m); +} + +void fm_releaseTesselate(fm_Tesselate* t) +{ + Myfm_Tesselate* m = static_cast(t); + delete m; +} + +#endif + + +#ifndef RAY_ABB_INTERSECT + +# define RAY_ABB_INTERSECT + +//! Integer representation of a floating-point value. +# define IR(x) ((uint32_t&)x) + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * A method to compute a ray-AABB intersection. + * Original code by Andrew Woo, from "Graphics Gems", Academic Press, 1990 + * Optimized code by Pierre Terdiman, 2000 (~20-30% faster on my Celeron 500) + * Epsilon value added by Klaus Hartmann. (discarding it saves a few cycles only) + * + * Hence this version is faster as well as more robust than the original one. + * + * Should work provided: + * 1) the integer representation of 0.0f is 0x00000000 + * 2) the sign bit of the float is the most significant one + * + * Report bugs: p.terdiman@codercorner.com + * + * \param aabb [in] the axis-aligned bounding box + * \param origin [in] ray origin + * \param dir [in] ray direction + * \param coord [out] impact coordinates + * \return true if ray intersects AABB + */ +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +# define RAYAABB_EPSILON 0.00001f +bool fm_intersectRayAABB(const float MinB[3], const float MaxB[3], const float origin[3], const float dir[3], float coord[3]) +{ + bool Inside = true; + float MaxT[3]; + MaxT[0] = MaxT[1] = MaxT[2] = -1.0f; + + // Find candidate planes. + for (uint32_t i = 0; i < 3; i++) + { + if (origin[i] < MinB[i]) + { + coord[i] = MinB[i]; + Inside = false; + + // Calculate T distances to candidate planes + if (IR(dir[i])) + MaxT[i] = (MinB[i] - origin[i]) / dir[i]; + } + else if (origin[i] > MaxB[i]) + { + coord[i] = MaxB[i]; + Inside = false; + + // Calculate T distances to candidate planes + if (IR(dir[i])) + MaxT[i] = (MaxB[i] - origin[i]) / dir[i]; + } + } + + // Ray origin inside bounding box + if (Inside) + { + coord[0] = origin[0]; + coord[1] = origin[1]; + coord[2] = origin[2]; + return true; + } + + // Get largest of the maxT's for final choice of intersection + uint32_t WhichPlane = 0; + if (MaxT[1] > MaxT[WhichPlane]) + WhichPlane = 1; + if (MaxT[2] > MaxT[WhichPlane]) + WhichPlane = 2; + + // Check final candidate actually inside box + if (IR(MaxT[WhichPlane]) & 0x80000000) + return false; + + for (uint32_t i = 0; i < 3; i++) + { + if (i != WhichPlane) + { + coord[i] = origin[i] + MaxT[WhichPlane] * dir[i]; +# ifdef RAYAABB_EPSILON + if (coord[i] < MinB[i] - RAYAABB_EPSILON || coord[i] > MaxB[i] + RAYAABB_EPSILON) + return false; +# else + if (coord[i] < MinB[i] || coord[i] > MaxB[i]) + return false; +# endif + } + } + return true; // ray hits box +} + +bool fm_intersectLineSegmentAABB( + const float bmin[3], const float bmax[3], const float p1[3], const float p2[3], float intersect[3]) +{ + bool ret = false; + + float dir[3]; + dir[0] = p2[0] - p1[0]; + dir[1] = p2[1] - p1[1]; + dir[2] = p2[2] - p1[2]; + float dist = fm_normalize(dir); + if (dist > RAYAABB_EPSILON) + { + ret = fm_intersectRayAABB(bmin, bmax, p1, dir, intersect); + if (ret) + { + float d = fm_distanceSquared(p1, intersect); + if (d > (dist * dist)) + { + ret = false; + } + } + } + return ret; +} + +#endif + +#ifndef OBB_TO_AABB + +# define OBB_TO_AABB + +# pragma warning(disable : 4100) + +void fm_OBBtoAABB(const float /*obmin*/[3], + const float /*obmax*/[3], + const float /*matrix*/[16], + float /*abmin*/[3], + float /*abmax*/[3]) +{ + assert(0); // not yet implemented. +} + + +const REAL* computePos(uint32_t index, const REAL* vertices, uint32_t vstride) +{ + const char* tmp = (const char*)vertices; + tmp += (index * vstride); + return (const REAL*)tmp; +} + +void computeNormal(uint32_t index, REAL* normals, uint32_t nstride, const REAL* normal) +{ + char* tmp = (char*)normals; + tmp += (index * nstride); + REAL* dest = (REAL*)tmp; + dest[0] += normal[0]; + dest[1] += normal[1]; + dest[2] += normal[2]; +} + +void fm_computeMeanNormals(uint32_t vcount, // the number of vertices + const REAL* vertices, // the base address of the vertex position data. + uint32_t vstride, // the stride between position data. + REAL* normals, // the base address of the destination for mean vector normals + uint32_t nstride, // the stride between normals + uint32_t tcount, // the number of triangles + const uint32_t* indices) // the triangle indices +{ + + // Step #1 : Zero out the vertex normals + char* dest = (char*)normals; + for (uint32_t i = 0; i < vcount; i++) + { + REAL* n = (REAL*)dest; + n[0] = 0; + n[1] = 0; + n[2] = 0; + dest += nstride; + } + + // Step #2 : Compute the face normals and accumulate them + const uint32_t* scan = indices; + for (uint32_t i = 0; i < tcount; i++) + { + + uint32_t i1 = *scan++; + uint32_t i2 = *scan++; + uint32_t i3 = *scan++; + + const REAL* p1 = computePos(i1, vertices, vstride); + const REAL* p2 = computePos(i2, vertices, vstride); + const REAL* p3 = computePos(i3, vertices, vstride); + + REAL normal[3]; + fm_computePlane(p3, p2, p1, normal); + + computeNormal(i1, normals, nstride, normal); + computeNormal(i2, normals, nstride, normal); + computeNormal(i3, normals, nstride, normal); + } + + + // Normalize the accumulated normals + dest = (char*)normals; + for (uint32_t i = 0; i < vcount; i++) + { + REAL* n = (REAL*)dest; + fm_normalize(n); + dest += nstride; + } +} + +#endif + + +#define BIGNUMBER 100000000.0 /* hundred million */ + +static inline void Set(REAL* n, REAL x, REAL y, REAL z) +{ + n[0] = x; + n[1] = y; + n[2] = z; +}; + +static inline void Copy(REAL* dest, const REAL* source) +{ + dest[0] = source[0]; + dest[1] = source[1]; + dest[2] = source[2]; +} + + +REAL fm_computeBestFitSphere(uint32_t vcount, const REAL* points, uint32_t pstride, REAL* center) +{ + REAL radius; + REAL radius2; + + REAL xmin[3]; + REAL xmax[3]; + REAL ymin[3]; + REAL ymax[3]; + REAL zmin[3]; + REAL zmax[3]; + REAL dia1[3]; + REAL dia2[3]; + + /* FIRST PASS: find 6 minima/maxima points */ + Set(xmin, BIGNUMBER, BIGNUMBER, BIGNUMBER); + Set(xmax, -BIGNUMBER, -BIGNUMBER, -BIGNUMBER); + Set(ymin, BIGNUMBER, BIGNUMBER, BIGNUMBER); + Set(ymax, -BIGNUMBER, -BIGNUMBER, -BIGNUMBER); + Set(zmin, BIGNUMBER, BIGNUMBER, BIGNUMBER); + Set(zmax, -BIGNUMBER, -BIGNUMBER, -BIGNUMBER); + + { + const char* scan = (const char*)points; + for (uint32_t i = 0; i < vcount; i++) + { + const REAL* caller_p = (const REAL*)scan; + if (caller_p[0] < xmin[0]) + Copy(xmin, caller_p); /* New xminimum point */ + if (caller_p[0] > xmax[0]) + Copy(xmax, caller_p); + if (caller_p[1] < ymin[1]) + Copy(ymin, caller_p); + if (caller_p[1] > ymax[1]) + Copy(ymax, caller_p); + if (caller_p[2] < zmin[2]) + Copy(zmin, caller_p); + if (caller_p[2] > zmax[2]) + Copy(zmax, caller_p); + scan += pstride; + } + } + + /* Set xspan = distance between the 2 points xmin & xmax (squared) */ + REAL dx = xmax[0] - xmin[0]; + REAL dy = xmax[1] - xmin[1]; + REAL dz = xmax[2] - xmin[2]; + REAL xspan = dx * dx + dy * dy + dz * dz; + + /* Same for y & z spans */ + dx = ymax[0] - ymin[0]; + dy = ymax[1] - ymin[1]; + dz = ymax[2] - ymin[2]; + REAL yspan = dx * dx + dy * dy + dz * dz; + + dx = zmax[0] - zmin[0]; + dy = zmax[1] - zmin[1]; + dz = zmax[2] - zmin[2]; + REAL zspan = dx * dx + dy * dy + dz * dz; + + /* Set points dia1 & dia2 to the maximally separated pair */ + Copy(dia1, xmin); + Copy(dia2, xmax); /* assume xspan biggest */ + REAL maxspan = xspan; + + if (yspan > maxspan) + { + maxspan = yspan; + Copy(dia1, ymin); + Copy(dia2, ymax); + } + + if (zspan > maxspan) + { + maxspan = zspan; + Copy(dia1, zmin); + Copy(dia2, zmax); + } + + + /* dia1,dia2 is a diameter of initial sphere */ + /* calc initial center */ + center[0] = (dia1[0] + dia2[0]) * 0.5f; + center[1] = (dia1[1] + dia2[1]) * 0.5f; + center[2] = (dia1[2] + dia2[2]) * 0.5f; + + /* calculate initial radius**2 and radius */ + + dx = dia2[0] - center[0]; /* x component of radius vector */ + dy = dia2[1] - center[1]; /* y component of radius vector */ + dz = dia2[2] - center[2]; /* z component of radius vector */ + + radius2 = dx * dx + dy * dy + dz * dz; + radius = REAL(sqrt(radius2)); + + /* SECOND PASS: increment current sphere */ + { + const char* scan = (const char*)points; + for (uint32_t i = 0; i < vcount; i++) + { + const REAL* caller_p = (const REAL*)scan; + dx = caller_p[0] - center[0]; + dy = caller_p[1] - center[1]; + dz = caller_p[2] - center[2]; + REAL old_to_p_sq = dx * dx + dy * dy + dz * dz; + if (old_to_p_sq > radius2) /* do r**2 test first */ + { /* this point is outside of current sphere */ + REAL old_to_p = REAL(sqrt(old_to_p_sq)); + /* calc radius of new sphere */ + radius = (radius + old_to_p) * 0.5f; + radius2 = radius * radius; /* for next r**2 compare */ + REAL old_to_new = old_to_p - radius; + /* calc center of new sphere */ + REAL recip = 1.0f / old_to_p; + REAL cx = (radius * center[0] + old_to_new * caller_p[0]) * recip; + REAL cy = (radius * center[1] + old_to_new * caller_p[1]) * recip; + REAL cz = (radius * center[2] + old_to_new * caller_p[2]) * recip; + Set(center, cx, cy, cz); + scan += pstride; + } + } + } + return radius; +} + + +void fm_computeBestFitCapsule( + uint32_t vcount, const REAL* points, uint32_t pstride, REAL& radius, REAL& height, REAL matrix[16], bool bruteForce) +{ + REAL sides[3]; + REAL omatrix[16]; + fm_computeBestFitOBB(vcount, points, pstride, sides, omatrix, bruteForce); + + int32_t axis = 0; + if (sides[0] > sides[1] && sides[0] > sides[2]) + axis = 0; + else if (sides[1] > sides[0] && sides[1] > sides[2]) + axis = 1; + else + axis = 2; + + REAL localTransform[16]; + + REAL maxDist = 0; + REAL maxLen = 0; + + switch (axis) + { + case 0: + { + fm_eulerMatrix(0, 0, FM_PI / 2, localTransform); + fm_matrixMultiply(localTransform, omatrix, matrix); + + const uint8_t* scan = (const uint8_t*)points; + for (uint32_t i = 0; i < vcount; i++) + { + const REAL* p = (const REAL*)scan; + REAL t[3]; + fm_inverseRT(omatrix, p, t); + REAL dist = t[1] * t[1] + t[2] * t[2]; + if (dist > maxDist) + { + maxDist = dist; + } + REAL l = (REAL)fabs(t[0]); + if (l > maxLen) + { + maxLen = l; + } + scan += pstride; + } + } + height = sides[0]; + break; + case 1: + { + fm_eulerMatrix(0, FM_PI / 2, 0, localTransform); + fm_matrixMultiply(localTransform, omatrix, matrix); + + const uint8_t* scan = (const uint8_t*)points; + for (uint32_t i = 0; i < vcount; i++) + { + const REAL* p = (const REAL*)scan; + REAL t[3]; + fm_inverseRT(omatrix, p, t); + REAL dist = t[0] * t[0] + t[2] * t[2]; + if (dist > maxDist) + { + maxDist = dist; + } + REAL l = (REAL)fabs(t[1]); + if (l > maxLen) + { + maxLen = l; + } + scan += pstride; + } + } + height = sides[1]; + break; + case 2: + { + fm_eulerMatrix(FM_PI / 2, 0, 0, localTransform); + fm_matrixMultiply(localTransform, omatrix, matrix); + + const uint8_t* scan = (const uint8_t*)points; + for (uint32_t i = 0; i < vcount; i++) + { + const REAL* p = (const REAL*)scan; + REAL t[3]; + fm_inverseRT(omatrix, p, t); + REAL dist = t[0] * t[0] + t[1] * t[1]; + if (dist > maxDist) + { + maxDist = dist; + } + REAL l = (REAL)fabs(t[2]); + if (l > maxLen) + { + maxLen = l; + } + scan += pstride; + } + } + height = sides[2]; + break; + } + radius = (REAL)sqrt(maxDist); + height = (maxLen * 2) - (radius * 2); +} + + +//************* Triangulation + +#ifndef TRIANGULATE_H + +# define TRIANGULATE_H + +typedef uint32_t TU32; + +class TVec +{ +public: + TVec(double _x, double _y, double _z) + { + x = _x; + y = _y; + z = _z; + }; + TVec(void){}; + + double x; + double y; + double z; +}; + +typedef std::vector TVecVector; +typedef std::vector TU32Vector; + +class CTriangulator +{ +public: + /// Default constructor + CTriangulator(); + + /// Default destructor + virtual ~CTriangulator(); + + /// Triangulates the contour + void triangulate(TU32Vector& indices); + + /// Returns the given point in the triangulator array + inline TVec get(const TU32 id) + { + return mPoints[id]; + } + + virtual void reset(void) + { + mInputPoints.clear(); + mPoints.clear(); + mIndices.clear(); + } + + virtual void addPoint(double x, double y, double z) + { + TVec v(x, y, z); + // update bounding box... + if (mInputPoints.empty()) + { + mMin = v; + mMax = v; + } + else + { + if (x < mMin.x) + mMin.x = x; + if (y < mMin.y) + mMin.y = y; + if (z < mMin.z) + mMin.z = z; + + if (x > mMax.x) + mMax.x = x; + if (y > mMax.y) + mMax.y = y; + if (z > mMax.z) + mMax.z = z; + } + mInputPoints.push_back(v); + } + + // Triangulation happens in 2d. We could inverse transform the polygon around the normal direction, or we just use + // the two most signficant axes Here we find the two longest axes and use them to triangulate. Inverse transforming + // them would introduce more doubleing point error and isn't worth it. + virtual uint32_t* triangulate(uint32_t& tcount, double epsilon) + { + uint32_t* ret = 0; + tcount = 0; + mEpsilon = epsilon; + + if (!mInputPoints.empty()) + { + mPoints.clear(); + + double dx = mMax.x - mMin.x; // locate the first, second and third longest edges and store them in i1, i2, + // i3 + double dy = mMax.y - mMin.y; + double dz = mMax.z - mMin.z; + + uint32_t i1, i2, i3; + + if (dx > dy && dx > dz) + { + i1 = 0; + if (dy > dz) + { + i2 = 1; + i3 = 2; + } + else + { + i2 = 2; + i3 = 1; + } + } + else if (dy > dx && dy > dz) + { + i1 = 1; + if (dx > dz) + { + i2 = 0; + i3 = 2; + } + else + { + i2 = 2; + i3 = 0; + } + } + else + { + i1 = 2; + if (dx > dy) + { + i2 = 0; + i3 = 1; + } + else + { + i2 = 1; + i3 = 0; + } + } + + uint32_t pcount = (uint32_t)mInputPoints.size(); + const double* points = &mInputPoints[0].x; + for (uint32_t i = 0; i < pcount; i++) + { + TVec v(points[i1], points[i2], points[i3]); + mPoints.push_back(v); + points += 3; + } + + mIndices.clear(); + triangulate(mIndices); + tcount = (uint32_t)mIndices.size() / 3; + if (tcount) + { + ret = &mIndices[0]; + } + } + return ret; + } + + virtual const double* getPoint(uint32_t index) + { + return &mInputPoints[index].x; + } + + +private: + double mEpsilon; + TVec mMin; + TVec mMax; + TVecVector mInputPoints; + TVecVector mPoints; + TU32Vector mIndices; + + /// Tests if a point is inside the given triangle + bool _insideTriangle(const TVec& A, const TVec& B, const TVec& C, const TVec& P); + + /// Returns the area of the contour + double _area(); + + bool _snip(int32_t u, int32_t v, int32_t w, int32_t n, int32_t* V); + + /// Processes the triangulation + void _process(TU32Vector& indices); +}; + +/// Default constructor +CTriangulator::CTriangulator(void) +{ +} + +/// Default destructor +CTriangulator::~CTriangulator() +{ +} + +/// Triangulates the contour +void CTriangulator::triangulate(TU32Vector& indices) +{ + _process(indices); +} + +/// Processes the triangulation +void CTriangulator::_process(TU32Vector& indices) +{ + const int32_t n = (const int32_t)mPoints.size(); + if (n < 3) + return; + int32_t* V = (int32_t*)malloc(sizeof(int32_t) * n); + + bool flipped = false; + + if (0.0f < _area()) + { + for (int32_t v = 0; v < n; v++) + V[v] = v; + } + else + { + flipped = true; + for (int32_t v = 0; v < n; v++) + V[v] = (n - 1) - v; + } + + int32_t nv = n; + int32_t count = 2 * nv; + for (int32_t m = 0, v = nv - 1; nv > 2;) + { + if (0 >= (count--)) + return; + + int32_t u = v; + if (nv <= u) + u = 0; + v = u + 1; + if (nv <= v) + v = 0; + int32_t w = v + 1; + if (nv <= w) + w = 0; + + if (_snip(u, v, w, nv, V)) + { + int32_t a, b, c, s, t; + a = V[u]; + b = V[v]; + c = V[w]; + if (flipped) + { + indices.push_back(a); + indices.push_back(b); + indices.push_back(c); + } + else + { + indices.push_back(c); + indices.push_back(b); + indices.push_back(a); + } + m++; + for (s = v, t = v + 1; t < nv; s++, t++) + V[s] = V[t]; + nv--; + count = 2 * nv; + } + } + + free(V); +} + +/// Returns the area of the contour +double CTriangulator::_area() +{ + int32_t n = (uint32_t)mPoints.size(); + double A = 0.0f; + for (int32_t p = n - 1, q = 0; q < n; p = q++) + { + const TVec& pval = mPoints[p]; + const TVec& qval = mPoints[q]; + A += pval.x * qval.y - qval.x * pval.y; + } + A *= 0.5f; + return A; +} + +bool CTriangulator::_snip(int32_t u, int32_t v, int32_t w, int32_t n, int32_t* V) +{ + int32_t p; + + const TVec& A = mPoints[V[u]]; + const TVec& B = mPoints[V[v]]; + const TVec& C = mPoints[V[w]]; + + if (mEpsilon > (((B.x - A.x) * (C.y - A.y)) - ((B.y - A.y) * (C.x - A.x)))) + return false; + + for (p = 0; p < n; p++) + { + if ((p == u) || (p == v) || (p == w)) + continue; + const TVec& P = mPoints[V[p]]; + if (_insideTriangle(A, B, C, P)) + return false; + } + return true; +} + +/// Tests if a point is inside the given triangle +bool CTriangulator::_insideTriangle(const TVec& A, const TVec& B, const TVec& C, const TVec& P) +{ + double ax, ay, bx, by, cx, cy, apx, apy, bpx, bpy, cpx, cpy; + double cCROSSap, bCROSScp, aCROSSbp; + + ax = C.x - B.x; + ay = C.y - B.y; + bx = A.x - C.x; + by = A.y - C.y; + cx = B.x - A.x; + cy = B.y - A.y; + apx = P.x - A.x; + apy = P.y - A.y; + bpx = P.x - B.x; + bpy = P.y - B.y; + cpx = P.x - C.x; + cpy = P.y - C.y; + + aCROSSbp = ax * bpy - ay * bpx; + cCROSSap = cx * apy - cy * apx; + bCROSScp = bx * cpy - by * cpx; + + return ((aCROSSbp >= 0.0f) && (bCROSScp >= 0.0f) && (cCROSSap >= 0.0f)); +} + +class Triangulate : public fm_Triangulate +{ +public: + Triangulate(void) + { + mPointsFloat = 0; + mPointsDouble = 0; + } + + virtual ~Triangulate(void) + { + reset(); + } + void reset(void) + { + free(mPointsFloat); + free(mPointsDouble); + mPointsFloat = 0; + mPointsDouble = 0; + } + + virtual const double* triangulate3d( + uint32_t pcount, const double* _points, uint32_t vstride, uint32_t& tcount, bool consolidate, double epsilon) + { + reset(); + + double* points = (double*)malloc(sizeof(double) * pcount * 3); + if (consolidate) + { + pcount = fm_consolidatePolygon(pcount, _points, vstride, points, 1 - epsilon); + } + else + { + double* dest = points; + for (uint32_t i = 0; i < pcount; i++) + { + const double* src = fm_getPoint(_points, vstride, i); + dest[0] = src[0]; + dest[1] = src[1]; + dest[2] = src[2]; + dest += 3; + } + vstride = sizeof(double) * 3; + } + + if (pcount >= 3) + { + CTriangulator ct; + for (uint32_t i = 0; i < pcount; i++) + { + const double* src = fm_getPoint(points, vstride, i); + ct.addPoint(src[0], src[1], src[2]); + } + uint32_t _tcount; + uint32_t* indices = ct.triangulate(_tcount, epsilon); + if (indices) + { + tcount = _tcount; + mPointsDouble = (double*)malloc(sizeof(double) * tcount * 3 * 3); + double* dest = mPointsDouble; + for (uint32_t i = 0; i < tcount; i++) + { + uint32_t i1 = indices[i * 3 + 0]; + uint32_t i2 = indices[i * 3 + 1]; + uint32_t i3 = indices[i * 3 + 2]; + const double* p1 = ct.getPoint(i1); + const double* p2 = ct.getPoint(i2); + const double* p3 = ct.getPoint(i3); + + dest[0] = p1[0]; + dest[1] = p1[1]; + dest[2] = p1[2]; + + dest[3] = p2[0]; + dest[4] = p2[1]; + dest[5] = p2[2]; + + dest[6] = p3[0]; + dest[7] = p3[1]; + dest[8] = p3[2]; + dest += 9; + } + } + } + free(points); + + return mPointsDouble; + } + + virtual const float* triangulate3d( + uint32_t pcount, const float* points, uint32_t vstride, uint32_t& tcount, bool consolidate, float epsilon) + { + reset(); + + double* temp = (double*)malloc(sizeof(double) * pcount * 3); + double* dest = temp; + for (uint32_t i = 0; i < pcount; i++) + { + const float* p = fm_getPoint(points, vstride, i); + dest[0] = p[0]; + dest[1] = p[1]; + dest[2] = p[2]; + dest += 3; + } + const double* results = triangulate3d(pcount, temp, sizeof(double) * 3, tcount, consolidate, epsilon); + if (results) + { + uint32_t fcount = tcount * 3 * 3; + mPointsFloat = (float*)malloc(sizeof(float) * tcount * 3 * 3); + for (uint32_t i = 0; i < fcount; i++) + { + mPointsFloat[i] = (float)results[i]; + } + free(mPointsDouble); + mPointsDouble = 0; + } + free(temp); + + return mPointsFloat; + } + +private: + float* mPointsFloat; + double* mPointsDouble; +}; + +fm_Triangulate* fm_createTriangulate(void) +{ + Triangulate* t = new Triangulate; + return static_cast(t); +} + +void fm_releaseTriangulate(fm_Triangulate* t) +{ + Triangulate* tt = static_cast(t); + delete tt; +} + +#endif + +bool validDistance(const REAL* p1, const REAL* p2, REAL epsilon) +{ + bool ret = true; + + REAL dx = p1[0] - p2[0]; + REAL dy = p1[1] - p2[1]; + REAL dz = p1[2] - p2[2]; + REAL dist = dx * dx + dy * dy + dz * dz; + if (dist < (epsilon * epsilon)) + { + ret = false; + } + return ret; +} + +bool fm_isValidTriangle(const REAL* p1, const REAL* p2, const REAL* p3, REAL epsilon) +{ + bool ret = false; + + if (validDistance(p1, p2, epsilon) && validDistance(p1, p3, epsilon) && validDistance(p2, p3, epsilon)) + { + + REAL area = fm_computeArea(p1, p2, p3); + if (area > epsilon) + { + REAL _vertices[3 * 3], vertices[64 * 3]; + + _vertices[0] = p1[0]; + _vertices[1] = p1[1]; + _vertices[2] = p1[2]; + + _vertices[3] = p2[0]; + _vertices[4] = p2[1]; + _vertices[5] = p2[2]; + + _vertices[6] = p3[0]; + _vertices[7] = p3[1]; + _vertices[8] = p3[2]; + + uint32_t pcount = fm_consolidatePolygon(3, _vertices, sizeof(REAL) * 3, vertices, 1 - epsilon); + if (pcount == 3) + { + ret = true; + } + } + } + return ret; +} + + +void fm_multiplyQuat(const REAL* left, const REAL* right, REAL* quat) +{ + REAL a, b, c, d; + + a = left[3] * right[3] - left[0] * right[0] - left[1] * right[1] - left[2] * right[2]; + b = left[3] * right[0] + right[3] * left[0] + left[1] * right[2] - right[1] * left[2]; + c = left[3] * right[1] + right[3] * left[1] + left[2] * right[0] - right[2] * left[0]; + d = left[3] * right[2] + right[3] * left[2] + left[0] * right[1] - right[0] * left[1]; + + quat[3] = a; + quat[0] = b; + quat[1] = c; + quat[2] = d; +} + +bool fm_computeCentroid(uint32_t vcount, // number of input data points + const REAL* points, // starting address of points array. + REAL* center) + +{ + bool ret = false; + if (vcount) + { + center[0] = 0; + center[1] = 0; + center[2] = 0; + const REAL* p = points; + for (uint32_t i = 0; i < vcount; i++) + { + center[0] += p[0]; + center[1] += p[1]; + center[2] += p[2]; + p += 3; + } + REAL recip = 1.0f / (REAL)vcount; + center[0] *= recip; + center[1] *= recip; + center[2] *= recip; + ret = true; + } + return ret; +} + +bool fm_computeCentroid(uint32_t vcount, // number of input data points + const REAL* points, // starting address of points array. + uint32_t triCount, + const uint32_t* indices, + REAL* center) + +{ + bool ret = false; + if (vcount) + { + center[0] = 0; + center[1] = 0; + center[2] = 0; + + REAL numerator[3] = { 0, 0, 0 }; + REAL denomintaor = 0; + + for (uint32_t i = 0; i < triCount; i++) + { + uint32_t i1 = indices[i * 3 + 0]; + uint32_t i2 = indices[i * 3 + 1]; + uint32_t i3 = indices[i * 3 + 2]; + + const REAL* p1 = &points[i1 * 3]; + const REAL* p2 = &points[i2 * 3]; + const REAL* p3 = &points[i3 * 3]; + + // Compute the sum of the three positions + REAL sum[3]; + sum[0] = p1[0] + p2[0] + p3[0]; + sum[1] = p1[1] + p2[1] + p3[1]; + sum[2] = p1[2] + p2[2] + p3[2]; + + // Compute the average of the three positions + sum[0] = sum[0] / 3; + sum[1] = sum[1] / 3; + sum[2] = sum[2] / 3; + + // Compute the area of this triangle + REAL area = fm_computeArea(p1, p2, p3); + + numerator[0] += (sum[0] * area); + numerator[1] += (sum[1] * area); + numerator[2] += (sum[2] * area); + + denomintaor += area; + } + REAL recip = 1 / denomintaor; + center[0] = numerator[0] * recip; + center[1] = numerator[1] * recip; + center[2] = numerator[2] * recip; + ret = true; + } + return ret; +} + + +#ifndef TEMPLATE_VEC3 +# define TEMPLATE_VEC3 +template +class Vec3 +{ +public: + Vec3(void) + { + } + Vec3(Type _x, Type _y, Type _z) + { + x = _x; + y = _y; + z = _z; + } + Type x; + Type y; + Type z; +}; +#endif + +void fm_transformAABB(const REAL bmin[3], const REAL bmax[3], const REAL matrix[16], REAL tbmin[3], REAL tbmax[3]) +{ + Vec3 box[8]; + box[0] = Vec3(bmin[0], bmin[1], bmin[2]); + box[1] = Vec3(bmax[0], bmin[1], bmin[2]); + box[2] = Vec3(bmax[0], bmax[1], bmin[2]); + box[3] = Vec3(bmin[0], bmax[1], bmin[2]); + box[4] = Vec3(bmin[0], bmin[1], bmax[2]); + box[5] = Vec3(bmax[0], bmin[1], bmax[2]); + box[6] = Vec3(bmax[0], bmax[1], bmax[2]); + box[7] = Vec3(bmin[0], bmax[1], bmax[2]); + // transform all 8 corners of the box and then recompute a new AABB + for (unsigned int i = 0; i < 8; i++) + { + Vec3& p = box[i]; + fm_transform(matrix, &p.x, &p.x); + if (i == 0) + { + tbmin[0] = tbmax[0] = p.x; + tbmin[1] = tbmax[1] = p.y; + tbmin[2] = tbmax[2] = p.z; + } + else + { + if (p.x < tbmin[0]) + tbmin[0] = p.x; + if (p.y < tbmin[1]) + tbmin[1] = p.y; + if (p.z < tbmin[2]) + tbmin[2] = p.z; + if (p.x > tbmax[0]) + tbmax[0] = p.x; + if (p.y > tbmax[1]) + tbmax[1] = p.y; + if (p.z > tbmax[2]) + tbmax[2] = p.z; + } + } +} + +REAL fm_normalizeQuat(REAL n[4]) // normalize this quat +{ + REAL dx = n[0] * n[0]; + REAL dy = n[1] * n[1]; + REAL dz = n[2] * n[2]; + REAL dw = n[3] * n[3]; + + REAL dist = dx * dx + dy * dy + dz * dz + dw * dw; + + dist = (REAL)sqrt(dist); + + REAL recip = 1.0f / dist; + + n[0] *= recip; + n[1] *= recip; + n[2] *= recip; + n[3] *= recip; + + return dist; +} + + +}; // namespace FLOAT_MATH diff --git a/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/Resources/vhacdCircularList.inl b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/Resources/vhacdCircularList.inl new file mode 100644 index 00000000..2dbbf3bd --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/Resources/vhacdCircularList.inl @@ -0,0 +1,159 @@ +#pragma once +#ifndef HACD_CIRCULAR_LIST_INL +# define HACD_CIRCULAR_LIST_INL +namespace VHACD +{ +template +inline bool CircularList::Delete(CircularListElement* element) +{ + if (!element) + { + return false; + } + if (m_size > 1) + { + CircularListElement* next = element->GetNext(); + CircularListElement* prev = element->GetPrev(); + delete element; + m_size--; + if (element == m_head) + { + m_head = next; + } + next->GetPrev() = prev; + prev->GetNext() = next; + return true; + } + else if (m_size == 1) + { + delete m_head; + m_size--; + m_head = 0; + return true; + } + else + { + return false; + } +} + +template +inline bool CircularList::Delete() +{ + if (m_size > 1) + { + CircularListElement* next = m_head->GetNext(); + CircularListElement* prev = m_head->GetPrev(); + delete m_head; + m_size--; + m_head = next; + next->GetPrev() = prev; + prev->GetNext() = next; + return true; + } + else if (m_size == 1) + { + delete m_head; + m_size--; + m_head = 0; + return true; + } + else + { + return false; + } +} +template +inline CircularListElement* CircularList::Add(const T* data) +{ + if (m_size == 0) + { + if (data) + { + m_head = new CircularListElement(*data); + } + else + { + m_head = new CircularListElement(); + } + m_head->GetNext() = m_head->GetPrev() = m_head; + } + else + { + CircularListElement* next = m_head->GetNext(); + CircularListElement* element = m_head; + if (data) + { + m_head = new CircularListElement(*data); + } + else + { + m_head = new CircularListElement(); + } + m_head->GetNext() = next; + m_head->GetPrev() = element; + element->GetNext() = m_head; + next->GetPrev() = m_head; + } + m_size++; + return m_head; +} +template +inline CircularListElement* CircularList::Add(const T& data) +{ + const T* pData = &data; + return Add(pData); +} +template +inline bool CircularList::Next() +{ + if (m_size == 0) + { + return false; + } + m_head = m_head->GetNext(); + return true; +} +template +inline bool CircularList::Prev() +{ + if (m_size == 0) + { + return false; + } + m_head = m_head->GetPrev(); + return true; +} +template +inline CircularList::CircularList(const CircularList& rhs) +{ + if (rhs.m_size > 0) + { + CircularListElement* current = rhs.m_head; + do + { + current = current->GetNext(); + Add(current->GetData()); + } while (current != rhs.m_head); + } +} +template +inline const CircularList& CircularList::operator=(const CircularList& rhs) +{ + if (&rhs != this) + { + Clear(); + if (rhs.m_size > 0) + { + CircularListElement* current = rhs.m_head; + do + { + current = current->GetNext(); + Add(current->GetData()); + } while (current != rhs.m_head); + } + } + return (*this); +} +} // namespace VHACD +#endif diff --git a/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/Resources/vhacdVector.inl b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/Resources/vhacdVector.inl new file mode 100644 index 00000000..39c9d23f --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/Resources/vhacdVector.inl @@ -0,0 +1,375 @@ +#pragma once +#ifndef VHACD_VECTOR_INL +# define VHACD_VECTOR_INL +namespace VHACD +{ +template +inline Vec3 operator*(T lhs, const Vec3& rhs) +{ + return Vec3(lhs * rhs.X(), lhs * rhs.Y(), lhs * rhs.Z()); +} +template +inline T& Vec3::X() +{ + return m_data[0]; +} +template +inline T& Vec3::Y() +{ + return m_data[1]; +} +template +inline T& Vec3::Z() +{ + return m_data[2]; +} +template +inline const T& Vec3::X() const +{ + return m_data[0]; +} +template +inline const T& Vec3::Y() const +{ + return m_data[1]; +} +template +inline const T& Vec3::Z() const +{ + return m_data[2]; +} +template +inline T Vec3::Normalize() +{ + T n = sqrt(m_data[0] * m_data[0] + m_data[1] * m_data[1] + m_data[2] * m_data[2]); + if (n != 0.0) + (*this) /= n; + return n; +} +template +inline T Vec3::GetNorm() const +{ + return sqrt(m_data[0] * m_data[0] + m_data[1] * m_data[1] + m_data[2] * m_data[2]); +} +template +inline void Vec3::operator=(const Vec3& rhs) +{ + this->m_data[0] = rhs.m_data[0]; + this->m_data[1] = rhs.m_data[1]; + this->m_data[2] = rhs.m_data[2]; +} +template +inline void Vec3::operator+=(const Vec3& rhs) +{ + this->m_data[0] += rhs.m_data[0]; + this->m_data[1] += rhs.m_data[1]; + this->m_data[2] += rhs.m_data[2]; +} +template +inline void Vec3::operator-=(const Vec3& rhs) +{ + this->m_data[0] -= rhs.m_data[0]; + this->m_data[1] -= rhs.m_data[1]; + this->m_data[2] -= rhs.m_data[2]; +} +template +inline void Vec3::operator-=(T a) +{ + this->m_data[0] -= a; + this->m_data[1] -= a; + this->m_data[2] -= a; +} +template +inline void Vec3::operator+=(T a) +{ + this->m_data[0] += a; + this->m_data[1] += a; + this->m_data[2] += a; +} +template +inline void Vec3::operator/=(T a) +{ + this->m_data[0] /= a; + this->m_data[1] /= a; + this->m_data[2] /= a; +} +template +inline void Vec3::operator*=(T a) +{ + this->m_data[0] *= a; + this->m_data[1] *= a; + this->m_data[2] *= a; +} +template +inline Vec3 Vec3::operator^(const Vec3& rhs) const +{ + return Vec3(m_data[1] * rhs.m_data[2] - m_data[2] * rhs.m_data[1], + m_data[2] * rhs.m_data[0] - m_data[0] * rhs.m_data[2], + m_data[0] * rhs.m_data[1] - m_data[1] * rhs.m_data[0]); +} +template +inline T Vec3::operator*(const Vec3& rhs) const +{ + return (m_data[0] * rhs.m_data[0] + m_data[1] * rhs.m_data[1] + m_data[2] * rhs.m_data[2]); +} +template +inline Vec3 Vec3::operator+(const Vec3& rhs) const +{ + return Vec3(m_data[0] + rhs.m_data[0], m_data[1] + rhs.m_data[1], m_data[2] + rhs.m_data[2]); +} +template +inline Vec3 Vec3::operator-(const Vec3& rhs) const +{ + return Vec3(m_data[0] - rhs.m_data[0], m_data[1] - rhs.m_data[1], m_data[2] - rhs.m_data[2]); +} +template +inline Vec3 Vec3::operator-() const +{ + return Vec3(-m_data[0], -m_data[1], -m_data[2]); +} + +template +inline Vec3 Vec3::operator*(T rhs) const +{ + return Vec3(rhs * this->m_data[0], rhs * this->m_data[1], rhs * this->m_data[2]); +} +template +inline Vec3 Vec3::operator/(T rhs) const +{ + return Vec3(m_data[0] / rhs, m_data[1] / rhs, m_data[2] / rhs); +} +template +inline Vec3::Vec3(T a) +{ + m_data[0] = m_data[1] = m_data[2] = a; +} +template +inline Vec3::Vec3(T x, T y, T z) +{ + m_data[0] = x; + m_data[1] = y; + m_data[2] = z; +} +template +inline Vec3::Vec3(const Vec3& rhs) +{ + m_data[0] = rhs.m_data[0]; + m_data[1] = rhs.m_data[1]; + m_data[2] = rhs.m_data[2]; +} +template +inline Vec3::~Vec3(void){}; + +template +inline Vec3::Vec3() +{ +} + +template +inline const bool Colinear(const Vec3& a, const Vec3& b, const Vec3& c) +{ + return ((c.Z() - a.Z()) * (b.Y() - a.Y()) - (b.Z() - a.Z()) * (c.Y() - a.Y()) == 0.0 /*EPS*/) && + ((b.Z() - a.Z()) * (c.X() - a.X()) - (b.X() - a.X()) * (c.Z() - a.Z()) == 0.0 /*EPS*/) && + ((b.X() - a.X()) * (c.Y() - a.Y()) - (b.Y() - a.Y()) * (c.X() - a.X()) == 0.0 /*EPS*/); +} + +template +inline const T ComputeVolume4(const Vec3& a, const Vec3& b, const Vec3& c, const Vec3& d) +{ + return (a - d) * ((b - d) ^ (c - d)); +} + +template +inline bool Vec3::operator<(const Vec3& rhs) const +{ + if (X() == rhs[0]) + { + if (Y() == rhs[1]) + { + return (Z() < rhs[2]); + } + return (Y() < rhs[1]); + } + return (X() < rhs[0]); +} +template +inline bool Vec3::operator>(const Vec3& rhs) const +{ + if (X() == rhs[0]) + { + if (Y() == rhs[1]) + { + return (Z() > rhs[2]); + } + return (Y() > rhs[1]); + } + return (X() > rhs[0]); +} +template +inline Vec2 operator*(T lhs, const Vec2& rhs) +{ + return Vec2(lhs * rhs.X(), lhs * rhs.Y()); +} +template +inline T& Vec2::X() +{ + return m_data[0]; +} +template +inline T& Vec2::Y() +{ + return m_data[1]; +} +template +inline const T& Vec2::X() const +{ + return m_data[0]; +} +template +inline const T& Vec2::Y() const +{ + return m_data[1]; +} +template +inline void Vec2::Normalize() +{ + T n = sqrt(m_data[0] * m_data[0] + m_data[1] * m_data[1]); + if (n != 0.0) + (*this) /= n; +} +template +inline T Vec2::GetNorm() const +{ + return sqrt(m_data[0] * m_data[0] + m_data[1] * m_data[1]); +} +template +inline void Vec2::operator=(const Vec2& rhs) +{ + this->m_data[0] = rhs.m_data[0]; + this->m_data[1] = rhs.m_data[1]; +} +template +inline void Vec2::operator+=(const Vec2& rhs) +{ + this->m_data[0] += rhs.m_data[0]; + this->m_data[1] += rhs.m_data[1]; +} +template +inline void Vec2::operator-=(const Vec2& rhs) +{ + this->m_data[0] -= rhs.m_data[0]; + this->m_data[1] -= rhs.m_data[1]; +} +template +inline void Vec2::operator-=(T a) +{ + this->m_data[0] -= a; + this->m_data[1] -= a; +} +template +inline void Vec2::operator+=(T a) +{ + this->m_data[0] += a; + this->m_data[1] += a; +} +template +inline void Vec2::operator/=(T a) +{ + this->m_data[0] /= a; + this->m_data[1] /= a; +} +template +inline void Vec2::operator*=(T a) +{ + this->m_data[0] *= a; + this->m_data[1] *= a; +} +template +inline T Vec2::operator^(const Vec2& rhs) const +{ + return m_data[0] * rhs.m_data[1] - m_data[1] * rhs.m_data[0]; +} +template +inline T Vec2::operator*(const Vec2& rhs) const +{ + return (m_data[0] * rhs.m_data[0] + m_data[1] * rhs.m_data[1]); +} +template +inline Vec2 Vec2::operator+(const Vec2& rhs) const +{ + return Vec2(m_data[0] + rhs.m_data[0], m_data[1] + rhs.m_data[1]); +} +template +inline Vec2 Vec2::operator-(const Vec2& rhs) const +{ + return Vec2(m_data[0] - rhs.m_data[0], m_data[1] - rhs.m_data[1]); +} +template +inline Vec2 Vec2::operator-() const +{ + return Vec2(-m_data[0], -m_data[1]); +} + +template +inline Vec2 Vec2::operator*(T rhs) const +{ + return Vec2(rhs * this->m_data[0], rhs * this->m_data[1]); +} +template +inline Vec2 Vec2::operator/(T rhs) const +{ + return Vec2(m_data[0] / rhs, m_data[1] / rhs); +} +template +inline Vec2::Vec2(T a) +{ + m_data[0] = m_data[1] = a; +} +template +inline Vec2::Vec2(T x, T y) +{ + m_data[0] = x; + m_data[1] = y; +} +template +inline Vec2::Vec2(const Vec2& rhs) +{ + m_data[0] = rhs.m_data[0]; + m_data[1] = rhs.m_data[1]; +} +template +inline Vec2::~Vec2(void){}; + +template +inline Vec2::Vec2() +{ +} + +/* + InsideTriangle decides if a point P is Inside of the triangle + defined by A, B, C. +*/ +template +inline const bool InsideTriangle(const Vec2& a, const Vec2& b, const Vec2& c, const Vec2& p) +{ + T ax, ay, bx, by, cx, cy, apx, apy, bpx, bpy, cpx, cpy; + T cCROSSap, bCROSScp, aCROSSbp; + ax = c.X() - b.X(); + ay = c.Y() - b.Y(); + bx = a.X() - c.X(); + by = a.Y() - c.Y(); + cx = b.X() - a.X(); + cy = b.Y() - a.Y(); + apx = p.X() - a.X(); + apy = p.Y() - a.Y(); + bpx = p.X() - b.X(); + bpy = p.Y() - b.Y(); + cpx = p.X() - c.X(); + cpy = p.Y() - c.Y(); + aCROSSbp = ax * bpy - ay * bpx; + cCROSSap = cx * apy - cy * apx; + bCROSScp = bx * cpy - by * cpx; + return ((aCROSSbp >= 0.0) && (bCROSScp >= 0.0) && (cCROSSap >= 0.0)); +} +} // namespace VHACD +#endif // VHACD_VECTOR_INL diff --git a/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/_CodeSignature.meta b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/_CodeSignature.meta new file mode 100644 index 00000000..770dffbd --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/_CodeSignature.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 053bcd82e5178b84f92ae9aad65047d0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/_CodeSignature/CodeResources b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/_CodeSignature/CodeResources new file mode 100644 index 00000000..b4537940 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/_CodeSignature/CodeResources @@ -0,0 +1,150 @@ + + + + + files + + Resources/FloatMath.inl + + FQ02laH8m+/JTJ+xcwqF4fWRY4w= + + Resources/vhacdCircularList.inl + + bg5DMaZXXwN+gpc16iPOfKbGOD4= + + Resources/vhacdVector.inl + + LRgKZE2bbNoNlvmVBByTASOiAlI= + + + files2 + + Resources/FloatMath.inl + + hash2 + + 7c/f0AfVvxJKCxxwRLRSiPEGbmEje0PvDdrLJebRkXU= + + + Resources/vhacdCircularList.inl + + hash2 + + WmykxJuTQfrpkVu/j2zvrquydZ+YDyad4TWxrxQgeIE= + + + Resources/vhacdVector.inl + + hash2 + + s1ji9RXyq8hW10f7880Nngs65lwHqd3pgFjjovRn1mM= + + + + rules + + ^Resources/ + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ + + nested + + weight + 10 + + ^.* + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^Resources/ + + weight + 20 + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^[^/]+$ + + nested + + weight + 10 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/Assets/EasyColliderEditor/Scripts/Plugins/VHACDLicense.txt b/Assets/EasyColliderEditor/Scripts/Plugins/VHACDLicense.txt new file mode 100644 index 00000000..9ada97ad --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Plugins/VHACDLicense.txt @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2011, Khaled Mamou (kmamou at gmail dot com) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Assets/EasyColliderEditor/Scripts/Plugins/VHACDLicense.txt.meta b/Assets/EasyColliderEditor/Scripts/Plugins/VHACDLicense.txt.meta new file mode 100644 index 00000000..1f31c09a --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Plugins/VHACDLicense.txt.meta @@ -0,0 +1,16 @@ +fileFormatVersion: 2 +guid: 0a79fff97a7a7294b835aea10e1cb6cf +timeCreated: 1596043168 +licenseType: Store +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/Plugins/VHACDLicense.txt + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/Shader.meta b/Assets/EasyColliderEditor/Scripts/Shader.meta new file mode 100644 index 00000000..95e58328 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Shader.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e07e2f113dc48584a95e5460e745c076 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/EasyColliderEditor/Scripts/Shader/EasyColliderCompute.cs b/Assets/EasyColliderEditor/Scripts/Shader/EasyColliderCompute.cs new file mode 100644 index 00000000..30aa4533 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Shader/EasyColliderCompute.cs @@ -0,0 +1,911 @@ +#if (UNITY_EDITOR) +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEditor; +using UnityEngine; +namespace ECE +{ + // Can't figure out how to do platform dependant compilation with attributes, and I have quite a headache, so... + // there's just two copies of the class here. all so that the warning isn't displayed when going from prefab mode to play mode + + /// + /// Not actually a compute shader. Just uses a regular shader with a structured buffer. + /// "In regular graphics shaders the compute buffer support requires minimum shader model 4.5." + /// +#if (UNITY_2018_3_OR_NEWER) + [System.Serializable, ExecuteAlways] + public class EasyColliderCompute : MonoBehaviour + { + #region preference values + private EasyColliderPreferences ECEPreferences + { + get { return EasyColliderPreferences.Preferences; } + } + public Color SelectedColor { get { return ECEPreferences.SelectedVertColour; } } + public Color HoveredColor { get { return ECEPreferences.HoverVertColour; } } + public Color OverlapColor { get { return ECEPreferences.OverlapSelectedVertColour; } } + public Color DisplayAllColor { get { return ECEPreferences.DisplayVerticesColour; } } + public float DefaultScale { get { return ECEPreferences.DefaultScale; } } + public float CommonScale { get { return ECEPreferences.CommonScalingMultiplier; } } + public float HoveredScale { get { return ECEPreferences.HoveredScaleMult; } } + public float SelectedScale { get { return ECEPreferences.SelectedScaleMult; } } + public float DisplayAllScale { get { return ECEPreferences.DisplayAllScaleMult; } } + public float OverlapScale { get { return ECEPreferences.OverlapScaleMult; } } + public bool DisplayAllVertices { get { return ECEPreferences.DisplayAllVertices; } } + #endregion + + + // shader to create materials from + public Shader GeometryShader; + + // hovered material for use with hovered buffer, color, and size. + [SerializeField] + Material _HoveredMaterial; + [SerializeField] + Material _SelectedMaterial; + [SerializeField] + Material _OverlapMaterial; + [SerializeField] + Material _DisplayAllMaterial; + + // bools to use to check if the buffer is valid (not empty), and if they are valid, to render the vertices/geometry + [SerializeField] + private bool _ValidOverlapBuffer = true; + [SerializeField] + private bool _ValidSelectedBuffer = true; + [SerializeField] + private bool _ValidHoverBuffer = true; + [SerializeField] + private bool _ValidDisplayAllBuffer = true; + + /// + /// Calculated density values. + /// + public float DensityScale = 0.0f; + + // Lists of points to be used in buffers. + [HideInInspector] + private List _SelectedWorldPoints = new List(); + [HideInInspector] + private HashSet _SelectedWorldPointsSet = new HashSet(); + [HideInInspector] + private List _OverlappedPoints = new List(); + [HideInInspector] + private List _HoveredPoints = new List(); + [HideInInspector] + private List _DisplayAllPoints = new List(); + public int SelectedPointCount { get { try { return _SelectedBuffer != null ? _SelectedWorldPoints.Count : 0; } catch { return 0; } } } + public int HoveredPointCount + { + get { try { return (_HoveredBuffer != null && _OverlapBuffer != null) ? _HoveredPoints.Count + _OverlappedPoints.Count : 0; } catch { return 0; } } + } + public int DisplayPointCount { get { try { return (_DisplayAllBuffer != null) ? _DisplayAllPoints.Count : 0; } catch { return 0; } } } + + // Compute buffers + [SerializeField] + ComputeBuffer _SelectedBuffer; + [SerializeField] + ComputeBuffer _HoveredBuffer; + [SerializeField] + ComputeBuffer _OverlapBuffer; + [SerializeField] + ComputeBuffer _DisplayAllBuffer; + + + /// + /// Called when the current editor scene is saved. + /// + /// + void OnSceneSaved(UnityEngine.SceneManagement.Scene scene) + { + SetSelectedBuffer(); + SetDisplayAllBuffer(); + } + + void Start() + { + if (EditorApplication.isPlaying) + { + Destroy(this); + } + } + + void OnEnable() + { + // we need to recreate the selected and display buffer when the scene is saved so they are displayed correctly. + UnityEditor.SceneManagement.EditorSceneManager.sceneSaved += OnSceneSaved; + // find the geometry shader + if (GeometryShader == null) + { + string[] ecp = AssetDatabase.FindAssets("EasyColliderShader t:Shader"); + if (ecp.Length > 0) + { + string assetPath = AssetDatabase.GUIDToAssetPath(ecp[0]); + GeometryShader = AssetDatabase.LoadAssetAtPath(assetPath, typeof(Shader)) as Shader; + } + if (GeometryShader == null) + { + Debug.LogError("EasyColliderEditor was unable to find the shader needed for displaying vertices. If you deleted it because your system does not support it, be sure to set Render Vertex Method to Gizmos in preferences."); + DestroyImmediate(this); + } + } + // create materials + _OverlapMaterial = new Material(GeometryShader); + _HoveredMaterial = new Material(GeometryShader); + _SelectedMaterial = new Material(GeometryShader); + _DisplayAllMaterial = new Material(GeometryShader); + // create buffers + if (_DisplayAllBuffer == null) + { + _ValidDisplayAllBuffer = false; + _DisplayAllBuffer = new ComputeBuffer(1, 12); + } + if (_HoveredBuffer == null) + { + _ValidHoverBuffer = false; + _HoveredBuffer = new ComputeBuffer(1, 12); + } + if (_SelectedBuffer == null) + { + _ValidSelectedBuffer = false; + _SelectedBuffer = new ComputeBuffer(1, 12); + } + if (_OverlapBuffer == null) + { + _ValidOverlapBuffer = false; + _OverlapBuffer = new ComputeBuffer(1, 12); + } + } + + float GetScale() + { + float scale = DefaultScale * CommonScale; + if (scale <= 0.0f) + { + scale = ECEPreferences.DefaultScale; + } + return scale; + } + + + void OnRenderObject() + { + float size = GetScale(); + // only need to re-update the selected buffer due to undo/redo operations. + if (!_ValidSelectedBuffer && _SelectedWorldPoints.Count > 0) + { + UpdateSelectedBuffer(_SelectedWorldPoints); + } +#if (UNITY_2018_1_OR_NEWER) + if (DisplayAllVertices && _DisplayAllBuffer != null && _ValidDisplayAllBuffer && _DisplayAllBuffer.IsValid()) +#else + if (DisplayAllVertices && _DisplayAllBuffer != null && _ValidDisplayAllBuffer) +#endif + { + _DisplayAllMaterial.SetColor("_Color", DisplayAllColor); + _DisplayAllMaterial.SetFloat("_Size", size * DisplayAllScale); + _DisplayAllMaterial.SetBuffer("worldPositions", _DisplayAllBuffer); + _DisplayAllMaterial.SetPass(0); +#if (UNITY_2019_1_OR_NEWER) + Graphics.DrawProceduralNow(MeshTopology.Points, _DisplayAllBuffer.count); +#else + Graphics.DrawProcedural(MeshTopology.Points, _DisplayAllBuffer.count); +#endif + //TODO: customizable / enablable wireframe drawing for the points. + // GL.wireframe = true; + // _DisplayAllMaterial.SetColor("_Color", Color.black); + // _DisplayAllMaterial.SetPass(0); + // Graphics.DrawProcedural(MeshTopology.Points, _DisplayAllBuffer.count); + // GL.wireframe = false; + } +#if (UNITY_2018_1_OR_NEWER) + if (_SelectedBuffer == null || !_SelectedBuffer.IsValid() || _SelectedBuffer.count != _SelectedWorldPoints.Count) + { + SetSelectedBuffer(); + } + if (_SelectedBuffer != null && _ValidSelectedBuffer && _SelectedBuffer.IsValid()) +#else + if (_SelectedBuffer != null && _ValidSelectedBuffer) +#endif + { + _SelectedMaterial.SetColor("_Color", SelectedColor); + _SelectedMaterial.SetFloat("_Size", size * SelectedScale); + _SelectedMaterial.SetBuffer("worldPositions", _SelectedBuffer); + _SelectedMaterial.SetPass(0); +#if (UNITY_2019_1_OR_NEWER) + Graphics.DrawProceduralNow(MeshTopology.Points, _SelectedBuffer.count); +#else + Graphics.DrawProcedural(MeshTopology.Points, _SelectedBuffer.count); +#endif + } + +#if (UNITY_2018_1_OR_NEWER) + if (_HoveredBuffer != null && _ValidHoverBuffer && _HoveredBuffer.IsValid()) +#else + if (_HoveredBuffer != null && _ValidHoverBuffer) +#endif + { + _HoveredMaterial.SetColor("_Color", HoveredColor); + _HoveredMaterial.SetFloat("_Size", size * HoveredScale); + _HoveredMaterial.SetPass(0); +#if (UNITY_2019_1_OR_NEWER) + Graphics.DrawProceduralNow(MeshTopology.Points, _HoveredBuffer.count); +#else + Graphics.DrawProcedural(MeshTopology.Points, _HoveredBuffer.count); +#endif + } +#if (UNITY_2018_1_OR_NEWER) + if (_OverlapBuffer != null && _ValidOverlapBuffer && _OverlapBuffer.IsValid()) +#else + if (_OverlapBuffer != null && _ValidOverlapBuffer) +#endif + { + _OverlapMaterial.SetColor("_Color", OverlapColor); + // scale overlap to be always larger than currently selected so they are always visible. + _OverlapMaterial.SetFloat("_Size", size * OverlapScale); + _OverlapMaterial.SetPass(0); + // draw the topology as points. the squares are drawn in the shader by triangles from the points passed in through the overlap buffer when it is updated. +#if (UNITY_2019_1_OR_NEWER) + Graphics.DrawProceduralNow(MeshTopology.Points, _OverlapBuffer.count); +#else + Graphics.DrawProcedural(MeshTopology.Points, _OverlapBuffer.count); +#endif + } + } + + + /// + /// Updates the selected world points buffer. + /// + /// New list of world points + public void UpdateSelectedBuffer(List worldPoints) + { + _SelectedWorldPointsSet.Clear(); + _SelectedWorldPoints = worldPoints; + _SelectedWorldPointsSet.UnionWith(worldPoints); + worldPoints.ForEach(point => _SelectedWorldPointsSet.Add(point)); + SetSelectedBuffer(); + } + + + /// + /// Clears the current selected buffer and resets it from _SelectedWorldPoints + /// + private void SetSelectedBuffer() + { + if (_SelectedBuffer != null) + { + _SelectedBuffer.Release(); + } + if (_SelectedWorldPoints.Count > 0) + { + _SelectedBuffer = new ComputeBuffer(_SelectedWorldPoints.Count, 12); + _SelectedBuffer.SetData(_SelectedWorldPoints.ToArray()); + _ValidSelectedBuffer = true; + } + else + { + _SelectedBuffer = new ComputeBuffer(1, 12); + _SelectedBuffer.SetCounterValue(0); + _ValidSelectedBuffer = false; + } + if (_SelectedBuffer != null && _SelectedMaterial != null) + { + _SelectedMaterial.SetBuffer("worldPositions", _SelectedBuffer); + } + else + { + _SelectedBuffer.Release(); + } + } + + /// + /// Updates both the overlap and hovered buffer + /// + /// All vertices highlighted for possible selection + public void UpdateOverlapHoveredBuffer(HashSet worldPoints) + { + _OverlappedPoints.Clear(); + _HoveredPoints.Clear(); + foreach (Vector3 p in _SelectedWorldPoints) + { + if (worldPoints.Contains(p)) + { + _OverlappedPoints.Add(p); + } + } + foreach (Vector3 p in worldPoints) + { + if (!_SelectedWorldPointsSet.Contains(p)) + { + _HoveredPoints.Add(p); + } + } + + if (_OverlapBuffer != null) + { + _OverlapBuffer.Release(); + } + if (_OverlappedPoints.Count > 0) + { + _OverlapBuffer = new ComputeBuffer(_OverlappedPoints.Count, 12); + // _OverlapBuffer.SetData(_OverlappedPoints); + _OverlapBuffer.SetData(_OverlappedPoints.ToArray()); + _ValidOverlapBuffer = true; + } + else + { + _OverlapBuffer = new ComputeBuffer(1, 12); + _OverlapBuffer.SetCounterValue(0); + _ValidOverlapBuffer = false; + } + if (_OverlapMaterial != null) + { + _OverlapMaterial.SetBuffer("worldPositions", _OverlapBuffer); + } + else + { + _OverlapBuffer.Release(); + } + + if (_HoveredBuffer != null) + { + _HoveredBuffer.Release(); + } + if (_HoveredPoints.Count > 0) + { + _HoveredBuffer = new ComputeBuffer(_HoveredPoints.Count, 12); + // _HoveredBuffer.SetData(_HoveredPoints); + _HoveredBuffer.SetData(_HoveredPoints.ToArray()); + _ValidHoverBuffer = true; + } + else + { + _HoveredBuffer = new ComputeBuffer(1, 12); + _HoveredBuffer.SetCounterValue(0); + _ValidHoverBuffer = false; + } + if (_HoveredMaterial != null) + { + _HoveredMaterial.SetBuffer("worldPositions", _HoveredBuffer); + } + else + { + _HoveredBuffer.Dispose(); + } + } + + public void SetDisplayAllBuffer(HashSet worldPoints) + { + _DisplayAllPoints.Clear(); + _DisplayAllPoints = worldPoints.ToList(); + SetDisplayAllBuffer(); + } + + /// + /// Clears the current display all buffer and updates it using _DisplayAllPoints + /// + private void SetDisplayAllBuffer() + { + if (_DisplayAllBuffer != null) + { + _DisplayAllBuffer.Release(); + } + if (_DisplayAllPoints.Count > 0) + { + _DisplayAllBuffer = new ComputeBuffer(_DisplayAllPoints.Count, 12); + _DisplayAllBuffer.SetData(_DisplayAllPoints.ToArray()); + _ValidDisplayAllBuffer = true; + } + else + { + _DisplayAllBuffer = new ComputeBuffer(1, 12); + _DisplayAllBuffer.SetCounterValue(0); + _ValidDisplayAllBuffer = false; + } + if (_DisplayAllBuffer != null && _DisplayAllMaterial != null) + { + _DisplayAllMaterial.SetBuffer("worldPositions", _DisplayAllBuffer); + } + else + { + _DisplayAllBuffer.Release(); + } + } + + void OnDestroy() + { + if (_HoveredBuffer != null) + { + _HoveredBuffer.Release(); + } + if (_SelectedBuffer != null) + { + _SelectedBuffer.Release(); + } + if (_OverlapBuffer != null) + { + _OverlapBuffer.Release(); + } + if (_DisplayAllBuffer != null) + { + _DisplayAllBuffer.Release(); + } + } + + void OnDisable() + { + if (_HoveredBuffer != null) + { + _HoveredBuffer.Release(); + } + if (_SelectedBuffer != null) + { + _SelectedBuffer.Release(); + } + if (_OverlapBuffer != null) + { + _OverlapBuffer.Release(); + } + if (_DisplayAllBuffer != null) + { + _DisplayAllBuffer.Release(); + } + // Unregister from the scene save delegate + UnityEditor.SceneManagement.EditorSceneManager.sceneSaved -= OnSceneSaved; + } + } +#else + [System.Serializable, ExecuteInEditMode] + + public class EasyColliderCompute : MonoBehaviour + { + #region preference values + private EasyColliderPreferences ECEPreferences + { + get { return EasyColliderPreferences.Preferences; } + } + public Color SelectedColor { get { return ECEPreferences.SelectedVertColour; } } + public Color HoveredColor { get { return ECEPreferences.HoverVertColour; } } + public Color OverlapColor { get { return ECEPreferences.OverlapSelectedVertColour; } } + public Color DisplayAllColor { get { return ECEPreferences.DisplayVerticesColour; } } + public float DefaultScale { get { return ECEPreferences.DefaultScale; } } + public float CommonScale { get { return ECEPreferences.CommonScalingMultiplier; } } + public float HoveredScale { get { return ECEPreferences.HoveredScaleMult; } } + public float SelectedScale { get { return ECEPreferences.SelectedScaleMult; } } + public float DisplayAllScale { get { return ECEPreferences.DisplayAllScaleMult; } } + public float OverlapScale { get { return ECEPreferences.OverlapScaleMult; } } + public bool DisplayAllVertices { get { return ECEPreferences.DisplayAllVertices; } } + #endregion + + + // shader to create materials from + public Shader GeometryShader; + + // hovered material for use with hovered buffer, color, and size. + [SerializeField] + Material _HoveredMaterial; + [SerializeField] + Material _SelectedMaterial; + [SerializeField] + Material _OverlapMaterial; + [SerializeField] + Material _DisplayAllMaterial; + + // bools to use to check if the buffer is valid (not empty), and if they are valid, to render the vertices/geometry + [SerializeField] + private bool _ValidOverlapBuffer = true; + [SerializeField] + private bool _ValidSelectedBuffer = true; + [SerializeField] + private bool _ValidHoverBuffer = true; + [SerializeField] + private bool _ValidDisplayAllBuffer = true; + + /// + /// Calculated density values. + /// + public float DensityScale = 0.0f; + + // Lists of points to be used in buffers. + [HideInInspector] + private List _SelectedWorldPoints = new List(); + [HideInInspector] + private HashSet _SelectedWorldPointsSet = new HashSet(); + [HideInInspector] + private List _OverlappedPoints = new List(); + [HideInInspector] + private List _HoveredPoints = new List(); + [HideInInspector] + private List _DisplayAllPoints = new List(); + public int SelectedPointCount { get { try { return _SelectedBuffer != null ? _SelectedWorldPoints.Count : 0; } catch { return 0; } } } + public int HoveredPointCount + { + get { try { return (_HoveredBuffer != null && _OverlapBuffer != null) ? (_HoveredPoints.Count + _OverlappedPoints.Count) : 0; } catch { return 0; } } + } + public int DisplayPointCount { get { try { return (_DisplayAllBuffer != null) ? _DisplayAllPoints.Count : 0; } catch { return 0; } } } + + // Compute buffers + [SerializeField] ComputeBuffer _SelectedBuffer; + [SerializeField] ComputeBuffer _HoveredBuffer; + [SerializeField] ComputeBuffer _OverlapBuffer; + [SerializeField] ComputeBuffer _DisplayAllBuffer; + + + /// + /// Called when the current editor scene is saved. + /// + /// + void OnSceneSaved(UnityEngine.SceneManagement.Scene scene) + { + SetSelectedBuffer(); + SetDisplayAllBuffer(); + } + + void Start() + { + if (EditorApplication.isPlaying) + { + Destroy(this); + } + } + + void OnEnable() + { + // we need to recreate the selected and display buffer when the scene is saved so they are displayed correctly. + UnityEditor.SceneManagement.EditorSceneManager.sceneSaved += OnSceneSaved; + // find the geometry shader + if (GeometryShader == null) + { + string[] ecp = AssetDatabase.FindAssets("EasyColliderShader t:Shader"); + if (ecp.Length > 0) + { + string assetPath = AssetDatabase.GUIDToAssetPath(ecp[0]); + GeometryShader = AssetDatabase.LoadAssetAtPath(assetPath, typeof(Shader)) as Shader; + } + if (GeometryShader == null) + { + Debug.LogError("EasyColliderEditor was unable to find the shader needed for displaying vertices. If you deleted it because your system does not support it, be sure to set Render Vertex Method to Gizmos in preferences."); + DestroyImmediate(this); + } + } + // create materials + _OverlapMaterial = new Material(GeometryShader); + _HoveredMaterial = new Material(GeometryShader); + _SelectedMaterial = new Material(GeometryShader); + _DisplayAllMaterial = new Material(GeometryShader); + // create buffers + if (_DisplayAllBuffer == null) + { + _ValidDisplayAllBuffer = false; + _DisplayAllBuffer = new ComputeBuffer(1, 12); + } + if (_HoveredBuffer == null) + { + _ValidHoverBuffer = false; + _HoveredBuffer = new ComputeBuffer(1, 12); + } + if (_SelectedBuffer == null) + { + _ValidSelectedBuffer = false; + _SelectedBuffer = new ComputeBuffer(1, 12); + } + if (_OverlapBuffer == null) + { + _ValidOverlapBuffer = false; + _OverlapBuffer = new ComputeBuffer(1, 12); + } + } + + float GetScale() + { + float scale = DefaultScale * CommonScale; + if (scale <= 0.0f) + { + scale = ECEPreferences.DefaultScale; + } + return scale; + } + + + void OnRenderObject() + { + float size = GetScale(); + // only need to re-update the selected buffer due to undo/redo operations. + if (!_ValidSelectedBuffer && _SelectedWorldPoints.Count > 0) + { + UpdateSelectedBuffer(_SelectedWorldPoints); + } +#if (UNITY_2018_1_OR_NEWER) + if (DisplayAllVertices && _DisplayAllBuffer != null && _ValidDisplayAllBuffer && _DisplayAllBuffer.IsValid()) +#else + if (DisplayAllVertices && _DisplayAllBuffer != null && _ValidDisplayAllBuffer) +#endif + { + _DisplayAllMaterial.SetColor("_Color", DisplayAllColor); + _DisplayAllMaterial.SetFloat("_Size", size * DisplayAllScale); + _DisplayAllMaterial.SetBuffer("worldPositions", _DisplayAllBuffer); + _DisplayAllMaterial.SetPass(0); +#if (UNITY_2019_1_OR_NEWER) + Graphics.DrawProceduralNow(MeshTopology.Points, _DisplayAllBuffer.count); +#else + Graphics.DrawProcedural(MeshTopology.Points, _DisplayAllBuffer.count); +#endif + //TODO: customizable / enablable wireframe drawing for the points. + // GL.wireframe = true; + // _DisplayAllMaterial.SetColor("_Color", Color.black); + // _DisplayAllMaterial.SetPass(0); + // Graphics.DrawProcedural(MeshTopology.Points, _DisplayAllBuffer.count); + // GL.wireframe = false; + } +#if (UNITY_2018_1_OR_NEWER) + if (_SelectedBuffer != null && _ValidSelectedBuffer && _SelectedBuffer.IsValid()) +#else + if (_SelectedBuffer != null && _ValidSelectedBuffer) +#endif + { + _SelectedMaterial.SetColor("_Color", SelectedColor); + _SelectedMaterial.SetFloat("_Size", size * SelectedScale); + _SelectedMaterial.SetBuffer("worldPositions", _SelectedBuffer); + _SelectedMaterial.SetPass(0); +#if (UNITY_2019_1_OR_NEWER) + Graphics.DrawProceduralNow(MeshTopology.Points, _SelectedBuffer.count); +#else + // because for some reason using vhacd with vertices selected, but not the use selected vertices toggle, + // causes the selected buffer to be lost. Tried to record an undo and full object undo, but that didn't do anything. + // but try/catch/finally works. + try + { + Graphics.DrawProcedural(MeshTopology.Points, _SelectedBuffer.count); + } + catch + { + SetSelectedBuffer(); + } + finally + { + Graphics.DrawProcedural(MeshTopology.Points, _SelectedBuffer.count); + } +#endif + } + +#if (UNITY_2018_1_OR_NEWER) + if (_HoveredBuffer != null && _ValidHoverBuffer && _HoveredBuffer.IsValid()) +#else + if (_HoveredBuffer != null && _ValidHoverBuffer) +#endif + { + _HoveredMaterial.SetColor("_Color", HoveredColor); + _HoveredMaterial.SetFloat("_Size", size * HoveredScale); + _HoveredMaterial.SetPass(0); +#if (UNITY_2019_1_OR_NEWER) + Graphics.DrawProceduralNow(MeshTopology.Points, _HoveredBuffer.count); +#else + Graphics.DrawProcedural(MeshTopology.Points, _HoveredBuffer.count); +#endif + } +#if (UNITY_2018_1_OR_NEWER) + if (_OverlapBuffer != null && _OverlapBuffer.IsValid()) +#else + if (_OverlapBuffer != null && _ValidOverlapBuffer) +#endif + { + _OverlapMaterial.SetColor("_Color", OverlapColor); + // scale overlap to be always larger than currently selected so they are always visible. + _OverlapMaterial.SetFloat("_Size", size * OverlapScale); + _OverlapMaterial.SetPass(0); + // draw the topology as points. the squares are drawn in the shader by triangles from the points passed in through the overlap buffer when it is updated. +#if (UNITY_2019_1_OR_NEWER) + Graphics.DrawProceduralNow(MeshTopology.Points, _OverlapBuffer.count); +#else + Graphics.DrawProcedural(MeshTopology.Points, _OverlapBuffer.count); +#endif + } + } + + + /// + /// Updates the selected world points buffer. + /// + /// New list of world points + public void UpdateSelectedBuffer(List worldPoints) + { + _SelectedWorldPointsSet.Clear(); + _SelectedWorldPoints = worldPoints; + _SelectedWorldPointsSet.UnionWith(worldPoints); + worldPoints.ForEach(point => _SelectedWorldPointsSet.Add(point)); + SetSelectedBuffer(); + } + + + /// + /// Clears the current selected buffer and resets it from _SelectedWorldPoints + /// + private void SetSelectedBuffer() + { + if (_SelectedBuffer != null) + { + _SelectedBuffer.Release(); + } + if (_SelectedWorldPoints.Count > 0) + { + _SelectedBuffer = new ComputeBuffer(_SelectedWorldPoints.Count, 12); + _SelectedBuffer.SetData(_SelectedWorldPoints.ToArray()); + _ValidSelectedBuffer = true; + } + else + { + _SelectedBuffer = new ComputeBuffer(1, 12); + _SelectedBuffer.SetCounterValue(0); + _ValidSelectedBuffer = false; + } + if (_SelectedBuffer != null && _SelectedMaterial != null) + { + _SelectedMaterial.SetBuffer("worldPositions", _SelectedBuffer); + } + else + { + _SelectedBuffer.Release(); + } + } + + /// + /// Updates both the overlap and hovered buffer + /// + /// All vertices highlighted for possible selection + public void UpdateOverlapHoveredBuffer(HashSet worldPoints) + { + _OverlappedPoints.Clear(); + _HoveredPoints.Clear(); + foreach (Vector3 p in _SelectedWorldPoints) + { + if (worldPoints.Contains(p)) + { + _OverlappedPoints.Add(p); + } + } + foreach (Vector3 p in worldPoints) + { + if (!_SelectedWorldPointsSet.Contains(p)) + { + _HoveredPoints.Add(p); + } + } + + if (_OverlapBuffer != null) + { + _OverlapBuffer.Release(); + } + if (_OverlappedPoints.Count > 0) + { + _OverlapBuffer = new ComputeBuffer(_OverlappedPoints.Count, 12); + // _OverlapBuffer.SetData(_OverlappedPoints); + _OverlapBuffer.SetData(_OverlappedPoints.ToArray()); + _ValidOverlapBuffer = true; + } + else + { + _OverlapBuffer = new ComputeBuffer(1, 12); + _OverlapBuffer.SetCounterValue(0); + _ValidOverlapBuffer = false; + } + if (_OverlapMaterial != null) + { + _OverlapMaterial.SetBuffer("worldPositions", _OverlapBuffer); + } + else + { + _OverlapBuffer.Release(); + } + + if (_HoveredBuffer != null) + { + _HoveredBuffer.Release(); + } + if (_HoveredPoints.Count > 0) + { + _HoveredBuffer = new ComputeBuffer(_HoveredPoints.Count, 12); + // _HoveredBuffer.SetData(_HoveredPoints); + _HoveredBuffer.SetData(_HoveredPoints.ToArray()); + _ValidHoverBuffer = true; + } + else + { + _HoveredBuffer = new ComputeBuffer(1, 12); + _HoveredBuffer.SetCounterValue(0); + _ValidHoverBuffer = false; + } + if (_HoveredMaterial != null) + { + _HoveredMaterial.SetBuffer("worldPositions", _HoveredBuffer); + } + else + { + _HoveredBuffer.Dispose(); + } + } + + public void SetDisplayAllBuffer(HashSet worldPoints) + { + _DisplayAllPoints.Clear(); + _DisplayAllPoints = worldPoints.ToList(); + SetDisplayAllBuffer(); + } + + /// + /// Clears the current display all buffer and updates it using _DisplayAllPoints + /// + private void SetDisplayAllBuffer() + { + if (_DisplayAllBuffer != null) + { + _DisplayAllBuffer.Release(); + } + if (_DisplayAllPoints.Count > 0) + { + _DisplayAllBuffer = new ComputeBuffer(_DisplayAllPoints.Count, 12); + _DisplayAllBuffer.SetData(_DisplayAllPoints.ToArray()); + _ValidDisplayAllBuffer = true; + } + else + { + _DisplayAllBuffer = new ComputeBuffer(1, 12); + _DisplayAllBuffer.SetCounterValue(0); + _ValidDisplayAllBuffer = false; + } + if (_DisplayAllBuffer != null && _DisplayAllMaterial != null) + { + _DisplayAllMaterial.SetBuffer("worldPositions", _DisplayAllBuffer); + } + else + { + _DisplayAllBuffer.Release(); + } + } + + void OnDestroy() + { + if (_HoveredBuffer != null) + { + _HoveredBuffer.Release(); + } + if (_SelectedBuffer != null) + { + _SelectedBuffer.Release(); + } + if (_OverlapBuffer != null) + { + _OverlapBuffer.Release(); + } + if (_DisplayAllBuffer != null) + { + _DisplayAllBuffer.Release(); + } + } + + void OnDisable() + { + if (_HoveredBuffer != null) + { + _HoveredBuffer.Release(); + } + if (_SelectedBuffer != null) + { + _SelectedBuffer.Release(); + } + if (_OverlapBuffer != null) + { + _OverlapBuffer.Release(); + } + if (_DisplayAllBuffer != null) + { + _DisplayAllBuffer.Release(); + } + // Unregister from the scene save delegate + UnityEditor.SceneManagement.EditorSceneManager.sceneSaved -= OnSceneSaved; + } + } +#endif +} +#endif \ No newline at end of file diff --git a/Assets/EasyColliderEditor/Scripts/Shader/EasyColliderCompute.cs.meta b/Assets/EasyColliderEditor/Scripts/Shader/EasyColliderCompute.cs.meta new file mode 100644 index 00000000..807dc2d8 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Shader/EasyColliderCompute.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 58c194f3826ab0b429ddebb6e0fa5adf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/Shader/EasyColliderCompute.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/Shader/EasyColliderMeshColliderPreview.shader b/Assets/EasyColliderEditor/Scripts/Shader/EasyColliderMeshColliderPreview.shader new file mode 100644 index 00000000..2af1a61a --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Shader/EasyColliderMeshColliderPreview.shader @@ -0,0 +1,103 @@ +// just a simple transparent color shader. +Shader "Custom/EasyColliderMeshColliderPreview" { + Properties { + _Color("Main Color", Color) = (1,1,1,1) + } + SubShader { + Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"} + Blend SrcAlpha OneMinusSrcAlpha + // For single convex-hulls so we can view them inside of the mesh with the new mesh offsets. + Pass { + ZTest always + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + #pragma target 2.0 + + #include "UnityCG.cginc" + + struct appdata_t { + float4 vertex : POSITION; + float4 normal: NORMAL; + }; + + struct v2f { + float4 vertex : SV_POSITION; + }; + + half4 _Color; + + v2f vert (appdata_t v) + { + v2f o; + //world space vert + o.vertex = mul(unity_ObjectToWorld, v.vertex); + // world space normal + float3 worldNormal = UnityObjectToWorldNormal(v.normal); + // offset by a small amount (1mm) (to prevent z-clipping) + o.vertex.xyz += worldNormal * 0.001f; + // back to to world space + o.vertex = mul(unity_WorldToObject, o.vertex); + // object to clip position + o.vertex = UnityObjectToClipPos(o.vertex); + return o; + } + + + + fixed4 frag (v2f i) : SV_Target + { + // all mesh-collider previews have an alpha value that can be changed if you wish. + _Color.a = 0.8f; + return _Color; + } + ENDCG + } + //VHACD pass, normal z-testing. + Pass { + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + #pragma target 2.0 + + #include "UnityCG.cginc" + + struct appdata_t { + float4 vertex : POSITION; + float4 normal: NORMAL; + }; + + struct v2f { + float4 vertex : SV_POSITION; + }; + + half4 _Color; + + v2f vert (appdata_t v) + { + v2f o; + //world space vert + o.vertex = mul(unity_ObjectToWorld, v.vertex); + // world space normal + float3 worldNormal = UnityObjectToWorldNormal(v.normal); + // offset by a small amount (1mm) (to prevent z-clipping) + o.vertex.xyz += worldNormal * 0.001f; + // back to to world space + o.vertex = mul(unity_WorldToObject, o.vertex); + // object to clip position + o.vertex = UnityObjectToClipPos(o.vertex); + return o; + } + + + + fixed4 frag (v2f i) : SV_Target + { + // all mesh-collider previews have an alpha value that can be changed if you wish. + _Color.a = 0.8f; + return _Color; + } + ENDCG + } + } + } \ No newline at end of file diff --git a/Assets/EasyColliderEditor/Scripts/Shader/EasyColliderMeshColliderPreview.shader.meta b/Assets/EasyColliderEditor/Scripts/Shader/EasyColliderMeshColliderPreview.shader.meta new file mode 100644 index 00000000..08553d22 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Shader/EasyColliderMeshColliderPreview.shader.meta @@ -0,0 +1,17 @@ +fileFormatVersion: 2 +guid: fc68b98d1257f434680734c4924323ef +timeCreated: 1600087651 +licenseType: Store +ShaderImporter: + externalObjects: {} + defaultTextures: [] + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/Shader/EasyColliderMeshColliderPreview.shader + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/Shader/EasyColliderShader.shader b/Assets/EasyColliderEditor/Scripts/Shader/EasyColliderShader.shader new file mode 100644 index 00000000..c691ac8e --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Shader/EasyColliderShader.shader @@ -0,0 +1,181 @@ +Shader "Custom/EasyColliderShader" +{ + // Attempted to write this shader as human readable as possible in case someone is looking to modify it. + // I'm not great with shaders, so I'm sure a lot could be improved + // If you encounter any shader issues please contact me so I can try and fix them! + // the first pass is for Windows, the second is for Mac/Metal and uses PSIZE instead of geometry shader. + Properties + { + _Color ("Color", Color) = (0,1,0,1) + _Size ("Size", float) = 0.01 + } + + // everything else (vulkan does not use psize) + SubShader + { + Tags { "RenderType"="Opaque" } + + Pass + { + // Always draw selected vertex things on top. + ZTest Always + CGPROGRAM + #pragma target 4.5 + #pragma vertex vert + #pragma fragment frag + #pragma geometry geom + // In regular graphics shaders the compute buffer support requires minimum shader model 4.5. + #include "UnityCG.cginc" + + // default values + float4 _Color = float4(0,1,0,1); + float _Size = 0.01; + + // struct of data we pass in from the compute script. + struct worldPosition { + float3 position; + }; + + // buffer of all our world points passed from our compute script. + StructuredBuffer worldPositions; + + // output from vertex shader, goes into the geometry shader, and the fragment shader. + struct vertOutput { + float4 position: SV_POSITION; + }; + + // for the vertex shader, we just need to vertex id, which matches with the worldPoints buffer of data + vertOutput vert(uint id: SV_VertexID) + { + // Create a new vertexOutput, and set the position on it. + vertOutput o; + o.position = float4(worldPositions[id].position, 1.0f); + return o; + } + + // 36 vertices to make the box that is displayed over vertices. + #define totalVerts 36 + // geometry shader creates a box at each vertOutput passed in. + [maxvertexcount(totalVerts)] + void geom(point vertOutput p[1], inout TriangleStream triStream) + { + // dimensions of length & width are the size / 2. + float d = _Size/2; + // scale of the square will be based on camera distance, + // so that as you get closer or further, the displayed cube remains the same size on screen. + float scale = distance(_WorldSpaceCameraPos, p[0].position); + if (unity_OrthoParams.w ==1) { + scale = unity_OrthoParams.y*2; + } + // verts: + // by x (negative z): ---, -+-, +--, ++- + // (positive z): --+, -++, +-+, +++ + // create a square, order of vertices in triangles matters for normals. + const float4 square[totalVerts] = { + // z+ + float4(-d,d,d,0), float4(-d,-d,d,0), float4(d,-d,d,0), + float4(d,d,d,0), float4(-d,d,d,0), float4(d,-d,d,0), + // z- + float4(d,-d,-d,0), float4(-d,-d,-d,0), float4(-d,d,-d,0), + float4(d,d,-d,0), float4(d,-d,-d,0), float4(-d,d,-d,0), + // y+ + float4(-d,d,d,0), float4(d,d,d,0), float4(d,d,-d,0), + float4(d,d,-d,0), float4(-d,d,-d,0), float4(-d,d,d,0), + // y- + float4(-d,-d,d,0), float4(d,-d,-d,0), float4(d,-d,d,0), + float4(d,-d,-d,0), float4(-d,-d,d,0), float4(-d,-d,-d,0), + // x+ + float4(d,d,-d,0), float4(d,d,d,0), float4(d,-d,d,0), + float4(d,d,-d,0), float4(d,-d,d,0), float4(d,-d,-d,0), + // x- + float4(-d,d,-d,0), float4(-d,-d,d,0), float4(-d,d,d,0), + float4(-d,d,-d,0), float4(-d,-d,-d,0), float4(-d,-d,d,0), + }; + + // array of vertices to make the triangles with + vertOutput Vertex[totalVerts]; + + // build the cube + for(int i=0; i worldPositions; + + // output from vertex shader, goes into the geometry shader, and the fragment shader. + struct vertOutput { + float4 position: SV_POSITION; + }; + + // for the vertex shader, we just need to vertex id, which matches with the worldPoints buffer of data + vertOutput vert(uint id: SV_VertexID, out float pointSize:PSIZE) + { + // Create a new vertexOutput, and set the position on it. + vertOutput o; + o.position = float4(worldPositions[id].position, 1.0f); + o.position = UnityObjectToClipPos(o.position); + // approximately equal to the windows version size. + pointSize = _Size * 700; + return o; + } + + // just returns the color for all fragments. simple. + float4 frag(vertOutput i) : COLOR + { + return _Color; + } + + ENDCG + } + + } + FallBack "Diffuse" +} diff --git a/Assets/EasyColliderEditor/Scripts/Shader/EasyColliderShader.shader.meta b/Assets/EasyColliderEditor/Scripts/Shader/EasyColliderShader.shader.meta new file mode 100644 index 00000000..5e76e21f --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/Shader/EasyColliderShader.shader.meta @@ -0,0 +1,16 @@ +fileFormatVersion: 2 +guid: 47ca8cc19e520064baecb98dc4e2eb9e +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/Shader/EasyColliderShader.shader + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/VHACDParameters.cs b/Assets/EasyColliderEditor/Scripts/VHACDParameters.cs new file mode 100644 index 00000000..fc9fa287 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/VHACDParameters.cs @@ -0,0 +1,208 @@ +#if (UNITY_EDITOR) +namespace ECE +{ + using UnityEngine; + using System.Collections.Generic; + [System.Serializable] + public class VHACDParameters + { + public float NormalExtrudeMultiplier = 0.0f; + + [HideInInspector] + /// + /// is the current calculation for displaying a preview? + /// + public bool IsCalculationForPreview; + + public VHACD_CONVERSION ConvertTo; + + [HideInInspector] + /// + /// the suffix to add to saved convex hulls: objectName_suffix_01 etc. + /// + public string SaveSuffix = "_ConvexHull_"; + + /// + /// Method to use to attach resulting convex hulls to attach to object. + /// + public VHACD_RESULT_METHOD vhacdResultMethod = VHACD_RESULT_METHOD.AttachTo; + + [HideInInspector] + /// + /// Run VHACD only on the selected vertices? + /// + public bool UseSelectedVertices = false; + + [HideInInspector] + /// + /// Current mesh filter VHACD is calculating. + /// + public int CurrentMeshFilter = 0; + + [HideInInspector] + /// + /// Should child meshes be seperately done in the calculation / adding of convex hulls. + /// + public bool SeparateChildMeshes = false; + + [HideInInspector] + /// + /// should the attach to method be run per mesh that is separated? + /// + public bool PerMeshAttachOverride = false; + + [HideInInspector] + /// + /// List of mesh filters for VHACD calculation. + /// + public List MeshFilters = new List(); + + [HideInInspector] + /// + /// Gameobject to attach mesh colliders to using the result of VHACD. + /// + public GameObject AttachTo; + + [HideInInspector] + /// + /// Save path of current VHACD meshes + /// + public string SavePath; + + + [Range(0, 1f)] + /// + /// maximum concavity + /// + public double concavity; + + [Range(0, 1f)] + /// + /// controls bias toward clipping along symmetry planes + /// + public double alpha; + + [Range(0, 1f)] + /// + /// controls bias toward clipping along revolution axes + /// + public double beta; + + [Range(0, 1f)] + /// + /// controls adaptive sampling of the generated convex-hulls + /// + public double minVolumePerCH; + + [Range(10000, 64000000)] + /// + /// maximum number of voxels generated during voxelization stage + /// + public int resolution; + + [Range(4, 1024)] + /// + /// controls maximum number of triangles per convex hull + /// + public int maxNumVerticesPerConvexHull; + + [Range(1, 16)] + /// + /// controls the granularity of the search for the "best" clipping plane + /// + public int planeDownsampling; + + [Range(1, 16)] + /// + /// controls the precision of the convex-hull generation process during the clipping plane selection stage + /// + public int convexhullDownSampling; + + [Range(1, 128)] + /// + /// maximum number of convex hulls + /// + public int maxConvexHulls; + + /// + /// When enabled, will project the output convex hull vertices onto the original source mesh + /// + public bool projectHullVertices; + + /// + /// Fill mode to determine what is inside/outside the mesh + /// Flood fill: basic flood fill algorithm + /// Raycast fill: raycasts are used to determine inside/outside + /// Surface: used when the surface is to represent a hollow object. + /// + public VHACD_FILL_MODE fillMode; + + /// + /// Should we force recalculation when resulting hulls have a hull with triangle count >=256. + /// + public bool forceUnder256Triangles; + + /// + /// Creates a VHACD parameters object with default values. + /// + public VHACDParameters() + { + concavity = 0.0025; + alpha = 0.05; + beta = 0.05; + minVolumePerCH = 0.0001; + resolution = 10000; + maxNumVerticesPerConvexHull = 64; + planeDownsampling = 4; + convexhullDownSampling = 4; + maxConvexHulls = 1; + projectHullVertices = true; + fillMode = VHACD_FILL_MODE.FLOOD_FILL; + forceUnder256Triangles = true; + } + + /// + /// Creates a VHACDParameters object with the values of another VHACDParam + /// + /// Values to copy from + public VHACDParameters(VHACDParameters other) + { + ConvertTo = other.ConvertTo; + SaveSuffix = other.SaveSuffix; + vhacdResultMethod = other.vhacdResultMethod; + AttachTo = other.AttachTo; + concavity = other.concavity; + alpha = other.alpha; + beta = other.beta; + minVolumePerCH = other.minVolumePerCH; + resolution = other.resolution; + maxNumVerticesPerConvexHull = other.maxNumVerticesPerConvexHull; + planeDownsampling = other.planeDownsampling; + convexhullDownSampling = other.convexhullDownSampling; + maxConvexHulls = other.maxConvexHulls; + projectHullVertices = other.projectHullVertices; + fillMode = other.fillMode; + forceUnder256Triangles = other.forceUnder256Triangles; + SeparateChildMeshes = other.SeparateChildMeshes; + UseSelectedVertices = other.UseSelectedVertices; + MeshFilters = new List(); + + foreach (MeshFilter f in other.MeshFilters) + { + MeshFilters.Add(f); + } + NormalExtrudeMultiplier = other.NormalExtrudeMultiplier; + PerMeshAttachOverride = other.PerMeshAttachOverride; + } + + /// + /// Clones the current instance of VHACDParameters. + /// + /// Copy of the VHACDParameters instance. + public VHACDParameters Clone() + { + return new VHACDParameters(this); + } + } +} +#endif \ No newline at end of file diff --git a/Assets/EasyColliderEditor/Scripts/VHACDParameters.cs.meta b/Assets/EasyColliderEditor/Scripts/VHACDParameters.cs.meta new file mode 100644 index 00000000..8210212f --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/VHACDParameters.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 77dedb303367131439faaeb70ed23f25 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/VHACDParameters.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/VHACDScriptableSettings.cs b/Assets/EasyColliderEditor/Scripts/VHACDScriptableSettings.cs new file mode 100644 index 00000000..3157e784 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/VHACDScriptableSettings.cs @@ -0,0 +1,74 @@ +#if (UNITY_EDITOR) +namespace ECE +{ + using System.Collections; + using System.Collections.Generic; + using UnityEngine; + using UnityEditor; + public class VHACDScriptableSettings : ScriptableObject + { + [SerializeField] VHACDParameters _vhacdParameters; + + public VHACDParameters GetParameters() { return _vhacdParameters; } + public void SetParameters(VHACDParameters parameters) + { + VHACDParameters copy = new VHACDParameters(parameters); + _vhacdParameters = copy; + } + + /// + /// Saves the current parameters after popping up a save file panel asking where to save. + /// + /// + public static void Save(VHACDParameters vhacdParamsToSave) + { + string firstAssetPath = FindFirstSOAssetPath(); + string path = EditorUtility.SaveFilePanel("Select where to save the current VHACD Settings.", firstAssetPath, "VHACDSettings", "asset"); + if (path.Contains(Application.dataPath)) + { + path = path.Replace(Application.dataPath, "Assets"); + if (!string.IsNullOrEmpty(path)) + { + VHACDScriptableSettings settingstoSave = ScriptableObject.CreateInstance(); + settingstoSave.SetParameters(vhacdParamsToSave); + AssetDatabase.CreateAsset(settingstoSave, path); + AssetDatabase.SaveAssets(); + } + } + } + + /// + /// Pops open a file panel and loads + /// + /// + public static VHACDScriptableSettings Load() + { + string firstAssetPath = FindFirstSOAssetPath(); // probably just better to load wherever it finds a vhacd settings object. + string path = EditorUtility.OpenFilePanel("Select VHACD Settings to Load", firstAssetPath, "asset"); + path = path.Replace(Application.dataPath, "Assets"); + VHACDScriptableSettings settings = AssetDatabase.LoadAssetAtPath(path); + return settings; + } + + public static string FindFirstSOAssetPath() + { + + string[] ecp = AssetDatabase.FindAssets("t:VHACDScriptableSettings"); + string assetPath = ""; + if (ecp.Length > 0) + { + assetPath = AssetDatabase.GUIDToAssetPath(ecp[0]); + } + if (!string.IsNullOrEmpty(assetPath)) + { + int index = assetPath.LastIndexOf("/"); + if (index >= 0) + { + assetPath = assetPath.Remove(index, assetPath.Length - index); + } + } + return assetPath; + } + } +} +#endif \ No newline at end of file diff --git a/Assets/EasyColliderEditor/Scripts/VHACDScriptableSettings.cs.meta b/Assets/EasyColliderEditor/Scripts/VHACDScriptableSettings.cs.meta new file mode 100644 index 00000000..72132792 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/VHACDScriptableSettings.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 4787408722c64ab4db0b7df26d9653a7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/VHACDScriptableSettings.cs + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/Scripts/VHACDSettings.meta b/Assets/EasyColliderEditor/Scripts/VHACDSettings.meta new file mode 100644 index 00000000..0a24656c --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/VHACDSettings.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f2afcddae4edf9041bf10bee2f4e86df +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/EasyColliderEditor/Scripts/VHACDSettings/ECE_DefaultVHACDSettings.asset b/Assets/EasyColliderEditor/Scripts/VHACDSettings/ECE_DefaultVHACDSettings.asset new file mode 100644 index 00000000..6dff6917 --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/VHACDSettings/ECE_DefaultVHACDSettings.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bdfcb28eff3a4ed1b855c46906612757eea88b952c26fe861f7e0b938d6c418a +size 1063 diff --git a/Assets/EasyColliderEditor/Scripts/VHACDSettings/ECE_DefaultVHACDSettings.asset.meta b/Assets/EasyColliderEditor/Scripts/VHACDSettings/ECE_DefaultVHACDSettings.asset.meta new file mode 100644 index 00000000..4c37bd1b --- /dev/null +++ b/Assets/EasyColliderEditor/Scripts/VHACDSettings/ECE_DefaultVHACDSettings.asset.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: b342658f1ee710947aa3d97df0dc576f +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/Scripts/VHACDSettings/ECE_DefaultVHACDSettings.asset + uploadId: 885904 diff --git a/Assets/EasyColliderEditor/ThirdPartyNotices.txt b/Assets/EasyColliderEditor/ThirdPartyNotices.txt new file mode 100644 index 00000000..e62c7b93 --- /dev/null +++ b/Assets/EasyColliderEditor/ThirdPartyNotices.txt @@ -0,0 +1,11 @@ +Third Party Notices + + +Easy Collider Editor is governed by the Asset Store EULA; however, the following components are governed by licenses indicated below: + + +1. VHACD.dll “BSD-3-Clause” +2. VHACD_OSX.bundle “BSD-3-Clause” + + +BSD-3-Clause: See VHACDLicense.txt in EasyColliderEditor/Scripts/Plugins for license information. \ No newline at end of file diff --git a/Assets/EasyColliderEditor/ThirdPartyNotices.txt.meta b/Assets/EasyColliderEditor/ThirdPartyNotices.txt.meta new file mode 100644 index 00000000..c046e1b6 --- /dev/null +++ b/Assets/EasyColliderEditor/ThirdPartyNotices.txt.meta @@ -0,0 +1,16 @@ +fileFormatVersion: 2 +guid: fbd80c39db074c64a89a5e820a12b2f8 +timeCreated: 1596043080 +licenseType: Store +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 67880 + packageName: Easy Collider Editor + packageVersion: 6.20.1 + assetPath: Assets/EasyColliderEditor/ThirdPartyNotices.txt + uploadId: 885904 diff --git a/Assets/Supermarket Store/Prefabs/RackAssets/GreenBeans.prefab b/Assets/Supermarket Store/Prefabs/RackAssets/GreenBeans.prefab index 4d724299..7437f044 100644 --- a/Assets/Supermarket Store/Prefabs/RackAssets/GreenBeans.prefab +++ b/Assets/Supermarket Store/Prefabs/RackAssets/GreenBeans.prefab @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2764ee8f231a0c301d385f01dbe6ffced304a0f3ef386407c3519e9084248e11 -size 2542 +oid sha256:8a377c001d6048eae8fc7678a4698c7c04185d24da3ad7045c3d0d57ece6e68d +size 25536 diff --git a/Assets/vFavorites.meta b/Assets/vFavorites.meta new file mode 100644 index 00000000..e16c6407 --- /dev/null +++ b/Assets/vFavorites.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d454140e53231466b8d485f9ff487b2e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/vFavorites/Manual.pdf b/Assets/vFavorites/Manual.pdf new file mode 100644 index 00000000..ed485aa0 --- /dev/null +++ b/Assets/vFavorites/Manual.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8216f0957395a1a854fc0815ed84f534be71ff2158d979e35c2ef2e510cb819f +size 555474 diff --git a/Assets/vFavorites/Manual.pdf.meta b/Assets/vFavorites/Manual.pdf.meta new file mode 100644 index 00000000..2735e178 --- /dev/null +++ b/Assets/vFavorites/Manual.pdf.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 842468ccf23564770b4b1cb9f7eca30e +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 263643 + packageName: vFavorites 2 + packageVersion: 2.0.14 + assetPath: Assets/vFavorites/Manual.pdf + uploadId: 874224 diff --git a/Assets/vFavorites/VFavorites.asmdef b/Assets/vFavorites/VFavorites.asmdef new file mode 100644 index 00000000..8897fa16 --- /dev/null +++ b/Assets/vFavorites/VFavorites.asmdef @@ -0,0 +1,16 @@ +{ + "name": "VFavorites", + "rootNamespace": "", + "references": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/vFavorites/VFavorites.asmdef.meta b/Assets/vFavorites/VFavorites.asmdef.meta new file mode 100644 index 00000000..a6ae1a5a --- /dev/null +++ b/Assets/vFavorites/VFavorites.asmdef.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: d8266c7db84a045d3b706b23e60aa197 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 263643 + packageName: vFavorites 2 + packageVersion: 2.0.14 + assetPath: Assets/vFavorites/VFavorites.asmdef + uploadId: 874224 diff --git a/Assets/vFavorites/VFavorites.cs b/Assets/vFavorites/VFavorites.cs new file mode 100644 index 00000000..ff148164 --- /dev/null +++ b/Assets/vFavorites/VFavorites.cs @@ -0,0 +1,2041 @@ +#if UNITY_EDITOR +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using UnityEngine.SceneManagement; +using UnityEditor.SceneManagement; +using UnityEditor.ShortcutManagement; +using System.Reflection; +using System.Linq; +using UnityEngine.UIElements; +using Type = System.Type; +using static VFavorites.Libs.VUtils; +using static VFavorites.Libs.VGUI; +using static VFavorites.VFavoritesData; + + +namespace VFavorites +{ + public static class VFavorites + { + static void WrappedOnGUI(object _) + { + if (wrappedBrowser?.GetType() != t_BrowserWindow) { originalBrowserGUI(); return; } + + + void createData() + { + if (data) return; + + data = ScriptableObject.CreateInstance(); + + AssetDatabase.CreateAsset(data, GetScriptPath("VFavorites").GetParentPath().CombinePath("vFavorites Data.asset")); + + + + // // migrateDataFromV1 + + // if (!EditorPrefsCached.HasKey("vFavorites-guids-" + GetProjectId())) return; + // if (EditorPrefsCached.HasKey("vHierarchy-dataMigrationFromV1Attempted-" + GetProjectId())) return; + + // EditorPrefsCached.SetBool("vHierarchy-dataMigrationFromV1Attempted-" + GetProjectId(), true); + + + // if (!data.pages.Any()) + // data.pages.Add(new Page("Page 1")); + + + // var guidsFromV1 = EditorPrefsCached.GetString("vFavorites-guids-" + GetProjectId()).Split('-').Where(r => r != "").ToList(); + + // foreach (var guid in guidsFromV1) + // if (AssetDatabase.LoadAssetAtPath(guid.ToPath()) is Object obj) + // data.pages.First().items.Add(new Item(obj)); + + + // data.Dirty(); + // data.Save(); + + } + void closeNavbarSearch() + { + if (!curEvent.isLayout) return; + if (t_VFolders == null) return; + + + if (GUI.GetNameOfFocusedControl() == "navbar search field") + GUI.FocusControl(null); + + if (!wrappedBrowser.GetMemberValue("m_SearchFieldText").IsNullOrEmpty()) + { + wrappedBrowser.SetMemberValue("m_SearchFieldText", ""); + wrappedBrowser.GetMemberValue("m_SearchFilter").SetMemberValue("m_NameFilter", ""); + + wrappedBrowser.InvokeMethod("UpdateSearchDelayed"); + + GUIUtility.keyboardControl = 0; + } + + + // mouse input on vFavorites throws exceptions if search is active on vFolders navbar + // here we close search to avoid this + + } + + void pageScroll() + { + if (!curEvent.isScroll) return; + if (!VFavoritesMenu.pageScrollEnabled) return; + + + + var scrollDelta = curEvent.mouseDelta.y; + + if (scrollDelta == 0 && curEvent.holdingShift) + scrollDelta = curEvent.mouseDelta.x; + + if (scrollDelta < 0 && data.curPageIndex == 0) return; + + + + if (scrollDelta < 0) + { + data.curPageIndex--; + prevPageButtonBrightness = 2; + } + + if (scrollDelta > 0) + { + data.curPageIndex++; + nextPageButtonBrightness = 2; + + } + + CancelDragging(); + CancelRowAnimations(); + + curEvent.Use(); + + } + + void background() + { + var color = isDarkTheme ? Greyscale(.2f) : Greyscale(.78f); + + totalRect_groupSpace.Draw(color); + + } + void pages() + { + void page(Rect pageRect, Page page) + { + void findSelectedItem() + { + if (!curEvent.isLayout) return; + + foreach (var item in page.items) + item.isSelected = false; + + if (draggingItem) return; + if (mousePresesdOnItem) return; + if (page.lastItemDragTime_ticks > page.lastItemSelectTime_ticks) return; + + Item lastSelectedItem = null; + + foreach (var item in page.items) + { + if (!item.isLoadable) continue; + if (lastSelectedItem?.lastSelectTime_ticks > item.lastSelectTime_ticks) continue; + + + var isSelected = false; + + if (item.isFolder) + { + var targetBrowser = isWrappedBrowserLocked && isOneColumn ? allBrowsers.FirstOrDefault(r => !r.GetMemberValue("isLocked")) ?? wrappedBrowser + : wrappedBrowser; + + if (targetBrowser.GetFieldValue("m_ViewMode") == 1) + isSelected = targetBrowser.InvokeMethod("GetActiveFolderPath") == item.assetPath; + else + isSelected = Selection.activeObject == item.obj; + + } + + if (!item.isFolder) + isSelected = Selection.activeObject == item.obj; + + + if (isSelected) + lastSelectedItem = item; + + } + + if (lastSelectedItem != null) + lastSelectedItem.isSelected = true; + + } + + void rows() + { + void row(float y, Item item) + { + var rowRect = pageRect.SetHeight(rowHeight).SetY(y).SetX(0); + + var iconOffset = 6; + var iconSize = 25 * Mathf.Min(1, data.rowScale); + var nameOffset = 3; + var deletedOrNotLoadedLabelOffset = 1; + + float highlightAmount = 0f; + + + void set_highlightAmount() + { + if (animatingDroppedItem && item == droppedItem) + highlightAmount = droppedItemHighlightAmount; + + if (item.isSelected) + highlightAmount = 1; + + if (draggingItem && item == draggedItem) + highlightAmount = 1; + + if (mousePresesdOnItem && item == pressedItem) + highlightAmount = 1; + + } + + void shadow() + { + if (item != draggedItem && item != droppedItem) return; + + var amount = item == droppedItem ? droppedItemShadowAmount : 1; + + if (amount.Approx(0)) return; + + rowRect.AddWidthFromMid(30).DrawBlurred(Greyscale(0, .55f * amount), 22); + + } + void background() + { + var evenColor = isDarkTheme ? Greyscale(.249f) : Greyscale(.82f); + var oddColor = isDarkTheme ? Greyscale(.228f) : Greyscale(.85f); + var highlightedColor = isDarkTheme ? Greyscale(.335f) : Greyscale(.9f); + + var rowColor = Lerp(evenColor, oddColor, rowRect.y.PingPong(rowHeight) / rowHeight); + + Lerp(ref rowColor, highlightedColor, highlightAmount); + + rowRect.Draw(rowColor); + + } + void icon() + { + var iconRect = rowRect.MoveX(iconOffset).SetWidth(iconSize).SetHeightFromMid(iconSize); + + if (item.isSceneGameObject) + iconRect = iconRect.MoveX(1).Resize(.5f); + + void asset() + { + if (!item.isAsset) return; + + var iconTexture = item.isLoadable ? AssetPreview.GetAssetPreview(item.obj) ?? AssetPreview.GetMiniThumbnail(item.obj) : AssetPreview.GetMiniTypeThumbnail(item.type); + + GUI.DrawTexture(iconRect, iconTexture); + + } + void sceneGameObject() + { + if (!item.isSceneGameObject) return; + + void getIconNameFromAssetPreview() + { + if (!item.isLoadable) return; + + item.sceneGameObjectIconName = AssetPreview.GetMiniThumbnail(item.obj).name; + + } + void getIconNameFromVHierarchy() + { + if (!item.isLoadable) return; + if (!(item.obj is GameObject gameObject)) return; + if (mi_VHierarchy_GetIconName == null) return; + + var iconNameFromVHierarchy = (string)mi_VHierarchy_GetIconName.Invoke(null, new object[] { gameObject }); + + if (!iconNameFromVHierarchy.IsNullOrEmpty()) + item.sceneGameObjectIconName = iconNameFromVHierarchy; + + } + + getIconNameFromAssetPreview(); + getIconNameFromVHierarchy(); + + var iconTexture = EditorGUIUtility.IconContent(item.sceneGameObjectIconName.IsNullOrEmpty() ? "GameObject icon" : item.sceneGameObjectIconName).image; + + GUI.DrawTexture(iconRect, iconTexture); + + } + void folder() + { + if (!item.isFolder) return; + + iconRect = iconRect.Resize(-1.5f); + + void drawNormal() + { + if (isDarkTheme) + if (highlightAmount == 1) return; + + GUI.DrawTexture(iconRect, EditorGUIUtility.IconContent("Folder icon").image); + + } + void drawHighlighted() + { + if (!isDarkTheme) return; + if (highlightAmount != 1) return; + + SetGUIColor(Greyscale(.84f)); + + GUI.DrawTexture(iconRect, EditorGUIUtility.IconContent("Folder On icon").image); + + ResetGUIColor(); + + } + void drawViaVFolders() + { + mi_VFolders_DrawBigFolderIcon?.Invoke(null, new object[] { iconRect, item.globalId.guid }); + } + + drawNormal(); + drawHighlighted(); + drawViaVFolders(); + + } + + asset(); + sceneGameObject(); + folder(); + + } + void name() + { + var nameRect = rowRect.MoveX(iconOffset + iconSize + nameOffset).MoveY(-.5f).SetHeightFromMid(16); + + void normal() + { + if (isDarkTheme) + if (highlightAmount == 1) return; + + GUI.Label(nameRect, item.name); + + } + void highlighted() + { + if (!isDarkTheme) return; + if (highlightAmount != 1) return; + if (!curEvent.isRepaint) return; + + SetGUIColor(Greyscale(.91f)); + + GUI.skin.GetStyle("WhiteLabel").Draw(nameRect, item.name, false, false, false, false); + + ResetGUIColor(); + + } + + normal(); + highlighted(); + + } + void deletedOrNotLoaded() + { + var labelRect = rowRect.MoveX(iconOffset + iconSize + nameOffset + item.name.GetLabelWidth() + deletedOrNotLoadedLabelOffset).MoveY(.5f); + + SetGUIEnabled(false); + SetLabelFontSize(10); + + if (item.isDeleted) + GUI.Label(labelRect, "Deleted"); + + else if (!item.isLoadable) + GUI.Label(labelRect, "Not loaded"); + + ResetLabelStyle(); + ResetGUIEnabled(); + + } + void crossButton() + { + if (!rowRect.IsHovered()) return; + if (draggingItem) return; + // if (mousePresesdOnItem) return; // idk + + var buttonRect = rowRect.SetWidthFromRight(0).MoveX(-crossButtonOffsetFromRight).SetWidthFromMid(crossButtonSize); + var iconRect = buttonRect.SetSizeFromMid(16); + + var normalColor = Greyscale(item.isSelected ? .48f : .4f); + var hoveredColor = isDarkTheme ? Greyscale(.8f) : normalColor; + var pressedColor = Greyscale(.6f); + + SetGUIColor(buttonRect.IsHovered() ? (mousePressed ? pressedColor : hoveredColor) : normalColor); + GUI.Label(iconRect, EditorGUIUtility.IconContent("CrossIcon")); + ResetGUIColor(); + + buttonRect.MarkInteractive(); + + + if (!mousePressedOnCrossButtonArea) return; + if (!curEvent.isMouseUp) return; + if (!buttonRect.IsHovered()) return; + + CancelRowAnimations(); + data.curPage.rowGaps[data.curPage.items.IndexOf(item)] = rowHeight; + + data.curPage.items.Remove(item); + + data.Dirty(); + data.Save(); + + curEvent.Use(); + + } + void click() + { + if (!rowRect.IsHovered()) return; + if (!curEvent.isMouseUp) return; + + curEvent.Use(); + + if (draggingItem) return; + if (mouseDragDistance > 2) return; + if (!item.isLoadable) return; + + SelectItem(item); + + } + void doubleclick() + { + if (!rowRect.IsHovered()) return; + if (!doubleclickUnhandled) return; + + OpenItem(item); + + doubleclickUnhandled = false; + + } + + + rowRect.MarkInteractive(); + + set_highlightAmount(); + + shadow(); + background(); + icon(); + name(); + deletedOrNotLoaded(); + crossButton(); + click(); + doubleclick(); + + } + + void normalRow(int i) + { + Space(page.rowGaps[i]); + Space(rowHeight); + + if (page.items[i] == droppedItem && animatingDroppedItem && page == data.curPage) return; + + row(lastRect.y, page.items[i]); + + } + void draggedRow() + { + if (!draggingItem) return; + if (page != data.curPage) return; + + + row(draggedItemY_rowsSpace, draggedItem); + + } + void droppedRow() + { + if (!animatingDroppedItem) return; + if (page != data.curPage) return; + + row(droppedItemY_rowsSpace, droppedItem); + + } + + + if (curEvent.isRepaint && skipNextRepaint) { skipNextRepaint = false; return; } + + + if (curEvent.holdingShift && curEvent.isScroll) + curEvent.e.delta = new Vector2(0, curEvent.e.delta.x + curEvent.e.delta.y); + + + GUILayout.BeginArea(pageRect); + page.scrollPos = EditorGUILayout.BeginScrollView(new Vector2(0, page.scrollPos), GUIStyle.none, GUIStyle.none).y; + + for (int i = 0; i < page.items.Count; i++) + normalRow(i); + + Space(page.rowGaps.Last()); + + Space(60); + + draggedRow(); + droppedRow(); + + EditorGUILayout.EndScrollView(); + GUILayout.EndArea(); + + + } + void curtains() + { + var height = 25; + var color = isDarkTheme ? Greyscale(.2f) : Greyscale(.78f); + + pageRect.SetHeight(height).DrawCurtainDown(color.SetAlpha((page.scrollPos / 20).Smoothstep())); + pageRect.SetHeightFromBottom(height).DrawCurtainUp(color); + + } + void tutor() + { + if (page.items.Any() || draggingItem) return; + + SetGUIEnabled(false); + SetLabelFontSize(11); + SetLabelAlignmentCenter(); + + GUI.Label(pageRect.MoveY(-13), "Drop folders, assets"); + GUI.Label(pageRect.MoveY(5), "or GameObjects"); + + ResetGUIEnabled(); + ResetLabelStyle(); + } + + + findSelectedItem(); + + rows(); + curtains(); + tutor(); + + } + + var spaceBetweenPages = 10; + + + if (pagesScrollPos == -1) + pagesScrollPos = data.curPageIndex; + + while (data.pages.Count <= pagesScrollPos.CeilToInt()) + data.pages.Add(new Page("Page " + (data.pages.Count + 1))); + + + for (int i = pagesScrollPos.FloorToInt(); i <= pagesScrollPos.CeilToInt(); i++) + page(totalRect_groupSpace.SetX((totalRect_groupSpace.width + spaceBetweenPages) * (i - pagesScrollPos)), data.pages[i]); + + } + void widget() + { + var widthToAdd = 48; + var height = 24; + var distToBottom = 13; + + var color = isDarkTheme ? Greyscale(.1f) : Greyscale(.85f); + var shadowSize = 10; + var shadowAlpha = .23f; + + var chevronSize = 15; + var chevronOffset = 12; + var chevronBrightness = .5f; + + var textSize = 11; + var textBrightness = .65f; + + widgetRect_groupSpace = totalRect_groupSpace.SetWidthFromMid(data.curPage.name.GetLabelWidth(textSize) + widthToAdd).SetHeightFromBottom(height).MoveY(-distToBottom); + + + void shadow() + { + widgetRect_groupSpace.Resize(1).DrawBlurred(Greyscale(0f, shadowAlpha), shadowSize); + } + void background() + { + widgetRect_groupSpace.DrawWithRoundedCorners(color, 1223); + } + + void nameLabel() + { + if (renamingPage) return; + + + var buttonRect = widgetRect_groupSpace.SetWidthFromMid(data.curPage.name.GetLabelWidth(textSize) - 2); + + buttonRect.MarkInteractive(); + + + var activated = curEvent.isMouseUp && buttonRect.IsHovered(); + + + var brightness = !buttonRect.IsHovered() ? textBrightness : (mousePressed ? .75f : 1); + + SetGUIColor(Greyscale(brightness)); + SetLabelAlignmentCenter(); + SetLabelFontSize(textSize); + SetLabelBold(); + + GUI.Label(widgetRect_groupSpace, data.curPage.name); + + ResetGUIColor(); + ResetLabelStyle(); + + + if (!activated) return; + + renamingPage = true; + prevPageName = data.curPage.name; + + curEvent.Use(); + + } + void leftButton() + { + if (renamingPage) return; + + var iconRect = widgetRect_groupSpace.SetWidth(chevronOffset * 2).SetSizeFromMid(chevronSize, chevronSize); + var buttonRect = Rect.zero.SetX(0).SetY(widgetRect_groupSpace.y).SetXMax(iconRect.xMax + 4).SetYMax(totalRect_groupSpace.yMax); + + buttonRect.MarkInteractive(); + + + var active = data.curPageIndex > 0; + var activated = curEvent.isMouseUp && buttonRect.IsHovered(); + + + var brightness = prevPageButtonBrightness * (!buttonRect.IsHovered() || !active ? chevronBrightness : (mousePressed ? .75f : 1)); + + SetGUIColor(Greyscale(brightness)); + + GUI.DrawTexture(iconRect, EditorGUIUtility.IconContent("NodeChevronLeft@2x").image); + + ResetGUIColor(); + + + + if (!activated) return; + + curEvent.Use(); + + + + if (!active) return; + + CancelDragging(); + CancelRowAnimations(); + + data.curPageIndex--; + + + } + void rightButton() + { + if (renamingPage) return; + + var iconRect = widgetRect_groupSpace.SetWidthFromRight(chevronOffset * 2).SetSizeFromMid(chevronSize, chevronSize); + var buttonRect = Rect.zero.SetX(iconRect.x - 6).SetY(widgetRect_groupSpace.y).SetXMax(totalRect_groupSpace.xMax).SetYMax(totalRect_groupSpace.yMax); + + buttonRect.MarkInteractive(); + + + var active = true;// data.curPageIndex < data.pages.Count - 1 && data.curPage.items.Any(); + var activated = curEvent.isMouseUp && buttonRect.IsHovered(); + + + var brightness = nextPageButtonBrightness * (!buttonRect.IsHovered() || !active ? chevronBrightness : (mousePressed ? .75f : 1)); + + SetGUIColor(Greyscale(brightness)); + + GUI.DrawTexture(iconRect, EditorGUIUtility.IconContent("NodeChevronRight@2x").image); + + ResetGUIColor(); + + + + if (!activated) return; + + curEvent.Use(); + + + + if (!active) return; + + CancelDragging(); + CancelRowAnimations(); + + data.curPageIndex++; + + } + + void nameTextField() + { + if (!renamingPage) return; + + var textFieldRect = widgetRect_groupSpace.AddHeightFromMid(-2).SetWidthFromMid(data.curPage.name.GetLabelWidth(textSize) + 4); + + var s = new GUIStyle(GUI.skin.textField); + s.alignment = TextAnchor.MiddleCenter; + s.fontSize = 11; + + EditorGUIUtility.AddCursorRect(textFieldRect, MouseCursor.CustomCursor); + + GUI.SetNextControlName("asdasdasd"); + EditorGUI.FocusTextInControl("asdasdasd"); + + + data.curPage.name = EditorGUI.TextField(textFieldRect, data.curPage.name, s); + + + if (data.curPage.name.IsNullOrEmpty()) + data.curPage.name = "Page " + (data.curPageIndex + 1); + + EditorGUIUtility.AddCursorRect(textFieldRect, MouseCursor.CustomCursor); + + + + } + void cancelRename_button() + { + if (!renamingPage) return; + + var iconRect = widgetRect_groupSpace.SetWidth(chevronOffset * 2).SetSizeFromMid(chevronSize - 2); + var buttonRect = iconRect.SetSizeFromMid(25, 25); + + buttonRect.MarkInteractive(); + + + var brightness = !buttonRect.IsHovered() ? chevronBrightness : (mousePressed ? .75f : 1); + + SetGUIColor(Greyscale(brightness)); + + GUI.DrawTexture(iconRect, EditorGUIUtility.IconContent("CrossIcon").image); + + ResetGUIColor(); + + + + if (!curEvent.isMouseUp) return; + if (!buttonRect.IsHovered()) return; + + curEvent.Use(); + + data.curPage.name = prevPageName; + + renamingPage = false; + + } + void acceptRename_button() + { + if (!renamingPage) return; + + var iconRect = widgetRect_groupSpace.SetWidthFromRight(chevronOffset * 2).SetSizeFromMid(chevronSize - 0); + var buttonRect = iconRect.SetSizeFromMid(25, 25); + + buttonRect.MarkInteractive(); + + + var brightness = !buttonRect.IsHovered() ? chevronBrightness : (mousePressed ? .75f : 1); + + SetGUIColor(Greyscale(brightness)); + + GUI.DrawTexture(iconRect, EditorGUIUtility.IconContent("check").image); + + ResetGUIColor(); + + + + if (!curEvent.isMouseUp) return; + if (!buttonRect.IsHovered()) return; + + curEvent.Use(); + + renamingPage = false; + + data.Dirty(); + data.Save(); + + } + void acceptRename_enterKey() + { + if (!renamingPage) return; + if (curEvent.keyCode != KeyCode.Return) return; + + renamingPage = false; + + data.Dirty(); + data.Save(); + + } + void cancelRename_escapeKey() + { + if (!renamingPage) return; + if (curEvent.keyCode != KeyCode.Escape) return; + + data.curPage.name = prevPageName; + + renamingPage = false; + + } + + + shadow(); + background(); + + leftButton(); + rightButton(); + nameLabel(); + + nameTextField(); + + cancelRename_button(); + acceptRename_button(); + + acceptRename_enterKey(); + cancelRename_escapeKey(); + + } + void keys() + { + if (isWrappedBrowserLocked && !totalRect_browserSpace.IsHovered()) return; + + void prevPage() + { + if (!curEvent.isKeyDown) return; + if (curEvent.keyCode != KeyCode.LeftArrow) return; + if (!VFavoritesMenu.arrowKeysEnabled) return; + if (data.curPageIndex == 0) return; + + CancelDragging(); + CancelRowAnimations(); + + data.curPageIndex--; + + prevPageButtonBrightness = 2; + + curEvent.Use(); + + } + void nextPage() + { + if (curEvent.keyCode != KeyCode.RightArrow) return; + if (!curEvent.isKeyDown) return; + if (!VFavoritesMenu.arrowKeysEnabled) return; + + CancelDragging(); + CancelRowAnimations(); + + data.curPageIndex++; + + nextPageButtonBrightness = 2; + + curEvent.Use(); + + } + void selectPrev() + { + if (!curEvent.isKeyDown) return; + if (curEvent.keyCode != KeyCode.UpArrow) return; + if (!VFavoritesMenu.arrowKeysEnabled) return; + + var iToSelect = data.curPage.items.IndexOfFirst(r => r.isSelected) - 1; + + if (iToSelect < 0) + iToSelect = data.curPage.items.LastIndex(); + + if (iToSelect.IsInRangeOf(data.curPage.items)) + SelectItem(data.curPage.items[iToSelect]); + + curEvent.Use(); + + } + void selectNext() + { + if (!curEvent.isKeyDown) return; + if (curEvent.keyCode != KeyCode.DownArrow) return; + if (!VFavoritesMenu.arrowKeysEnabled) return; + + var iToSelect = data.curPage.items.IndexOfFirst(r => r.isSelected) + 1; + + if (iToSelect >= data.curPage.items.Count) + iToSelect = 0; + + if (iToSelect.IsInRangeOf(data.curPage.items)) + SelectItem(data.curPage.items[iToSelect]); + + curEvent.Use(); + + } + void numberKeys() + { + if (!curEvent.isKeyDown) return; + if (!VFavoritesMenu.numberKeysEnabled) return; + if (EditorGUIUtility.editingTextField) return; + + + var i = ((int)curEvent.keyCode - 48); + + if (i == 0) i = 10; + + if (!i.IsInRange(1, 10)) return; + + + data.curPageIndex = i - 1; + + curEvent.Use(); + + + } + + prevPage(); + nextPage(); + selectPrev(); + selectNext(); + numberKeys(); + + } + + void doOriginalGUIFirst_() + { + originalBrowserGUI(); + + GUI.BeginGroup(totalRect_browserSpace); + GUI.color = GUI.color.SetAlpha(currentOpacity); + + background(); + pages(); + widget(); + + GUI.color = GUI.color.SetAlpha(1); + GUI.EndGroup(); + + } + void doVFavoritesGUIFirst_() + { + keys(); + pageScroll(); + + GUI.BeginGroup(totalRect_browserSpace); + + widget(); + pages(); + + GUI.EndGroup(); + + if (totalRect_browserSpace.IsHovered()) + if (curEvent.isMouseUp || curEvent.isMouseDrag || curEvent.isScroll) // prevents these events from reaching original gui + curEvent.Use(); + + originalBrowserGUI(); + + } + + void originalBrowserGUI() + { + if (origBrowserOnGUIDelegate.GetMethodInfo().DeclaringType.Name.Contains("VTabs")) return; + + if (isOneColumn && currentOpacity.Approx(1)) // to optimize locked one-column browser functioning as a dedicated favorites window + if (originalGUICalledOnce) // needs to be called once to init stuff so pinging object won't throw exceptions + return; + + + + if (origBrowserOnGUIDelegate.GetMethodInfo().IsStatic) // wrapped by vFolders + origBrowserOnGUIDelegate.GetMethodInfo().Invoke(null, new[] { wrappedBrowser }); + else + origBrowserOnGUIDelegate.GetMethodInfo().Invoke(wrappedBrowser, null); + + originalGUICalledOnce = true; + + } + + + + + createData(); + closeNavbarSearch(); + + UpdateMouseState(); + UpdateDragging(); + + + var doOriginalGUIFirst = VFavoritesMenu.pageScrollEnabled ? curEvent.isRepaint || curEvent.isLayout + : curEvent.isRepaint; + + if (renamingPage) + doOriginalGUIFirst = !curEvent.isMouseDown && !curEvent.isMouseUp; + + if (renamingPage && curEvent.isMouseDown) + curEvent.Use(); + + + if (doOriginalGUIFirst) + doOriginalGUIFirst_(); + else + doVFavoritesGUIFirst_(); + + + if (isWrappedBrowserLocked) + if (!totalRect_browserSpace.IsHovered() && !animatingDroppedItem && !animatingPageScroll) return; + + if (animatingOpacity || animatingDroppedItem || animatingPageScroll || animatingRowGaps) + wrappedBrowser.Repaint(); + + } + + static void SelectItem(Item item) + { + void openFolder(EditorWindow browser, string path) + { + var folderAsset = AssetDatabase.LoadAssetAtPath(path); + + if (browser.GetFieldValue("m_ViewMode") == 1) +#if UNITY_6000_3_OR_NEWER + browser.InvokeMethod("SetFolderSelection", new[] { (EntityId)folderAsset.GetInstanceID() }, false); +#else + browser.InvokeMethod("SetFolderSelection", new[] { folderAsset.GetInstanceID() }, false); +#endif + else + { + Selection.activeObject = folderAsset; + +#if UNITY_6000_3_OR_NEWER + browser.GetMemberValue("m_AssetTree")?.GetPropertyValue("data")?.InvokeMethod("SetExpanded", (EntityId)folderAsset.GetInstanceID(), true); +#else + browser.GetMemberValue("m_AssetTree")?.GetPropertyValue("data")?.InvokeMethod("SetExpanded", folderAsset.GetInstanceID(), true); +#endif + + } + + } + + void selectSceneObject() + { + if (!item.isSceneGameObject) return; + + Selection.activeObject = item.obj; + + } + void selectAsset_twoColumns() + { + if (!item.isAsset) return; + if (wrappedBrowser.GetFieldValue("m_ViewMode") != 1) return; + + openFolder(wrappedBrowser, item.assetPath.GetParentPath()); + + Selection.activeObject = item.obj; + + } + void selectAsset_oneColumn() + { + if (!item.isAsset) return; + if (wrappedBrowser.GetFieldValue("m_ViewMode") != 0) return; + + Selection.activeObject = item.obj; + + } + void openFolder_unlocked() + { + if (!item.isFolder) return; + if (isWrappedBrowserLocked) return; + + openFolder(wrappedBrowser, item.assetPath); + + } + void openFolder_locked() + { + if (!item.isFolder) return; + if (!isWrappedBrowserLocked) return; + + var unlockedBrowser = allBrowsers.FirstOrDefault(r => !r.GetMemberValue("isLocked")); + var browserToUse = isOneColumn ? unlockedBrowser : lockedBrowser; + + if (!browserToUse) return; + + openFolder(browserToUse, item.assetPath); + + } + + selectSceneObject(); + selectAsset_twoColumns(); + selectAsset_oneColumn(); + openFolder_unlocked(); + openFolder_locked(); + + item.lastSelectTime_ticks = data.curPage.lastItemSelectTime_ticks = System.DateTime.UtcNow.Ticks; + + } + static void OpenItem(Item item) + { + void openText() + { + if (item.assetPath.GetExtension() != ".cs" + && item.assetPath.GetExtension() != ".shader" + && item.assetPath.GetExtension() != ".compute" + && item.assetPath.GetExtension() != ".cginc" + && item.assetPath.GetExtension() != ".json") return; + + + AssetDatabase.OpenAsset(item.globalId.guid.LoadGuid()); + + } + void openPrefab() + { + if (item.type != typeof(GameObject)) return; + if (!item.isLoadable) return; + if ((item.obj as GameObject).scene.rootCount != 0) return; + + AssetDatabase.OpenAsset(item.obj); + + } + void openScene() + { + if (item.type != typeof(SceneAsset)) return; + if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) return; // if clicked cancel + + EditorSceneManager.SaveOpenScenes(); + EditorSceneManager.OpenScene(item.assetPath); + + } + void openSceneForNotLoadedGameObject() + { + if (!item.isSceneGameObject) return; + if (item.isLoadable) return; + if (item.isDeleted) return; + + EditorSceneManager.SaveOpenScenes(); + EditorSceneManager.OpenScene(item.assetPath); + + Selection.activeObject = item.obj; + + } + + openText(); + openPrefab(); + openScene(); + openSceneForNotLoadedGameObject(); + + } + + static float rowHeight => 44 * data.rowScale; + + static float crossButtonOffsetFromRight = 23; + static float crossButtonSize = 16; + + static Rect totalRect_browserSpace + { + get + { + if (isOneColumn) + return wrappedBrowser.position.SetPos(0, 0); + + + var treeViewRect = wrappedBrowser.GetFieldValue("m_TreeViewRect"); + + return wrappedBrowser.position.SetPos(0, 0).SetWidth(treeViewRect.width).SetHeightFromBottom(treeViewRect.height); + + } + } + static Rect totalRect_groupSpace => totalRect_browserSpace.SetPos(0, 0); + + static Rect widgetRect_browserSpace => widgetRect_groupSpace.MoveY(totalRect_browserSpace.y); + static Rect widgetRect_groupSpace; + + static bool renamingPage; + static string prevPageName; + + static int lastNumberKeyPressedIndex; + static long lastNumberKeyPressedTime_ticks; + + static bool isOneColumn => wrappedBrowser?.GetFieldValue("m_ViewMode") == 0; + + static bool skipNextRepaint; + static bool originalGUICalledOnce; + + + + + + + + static void UpdateMouseState() // called from WrappedOnGUI + { + if (!shortcutPressed && !isWrappedBrowserLocked) { setDefaultState(); return; } + if (!totalRect_browserSpace.IsHovered()) { setDefaultState(); return; } + + void setDefaultState() + { + mousePressed = false; + mousePressedOnCrossButtonArea = false; + mousePressedOnWidget = false; + pressedItem = null; + doubleclickUnhandled = false; + + } + + void position() + { + mousePosiion_browserSpace = curEvent.mousePosition; + } + void down() + { + if (!curEvent.isMouseDown) return; + + mousePressed = true; + mousePressedOnCrossButtonArea = totalRect_browserSpace.SetWidthFromRight(0).MoveX(-crossButtonOffsetFromRight).SetWidthFromMid(crossButtonSize).IsHovered(); + mousePressedOnWidget = curEvent.mousePosition.y >= widgetRect_browserSpace.y; + + mouseDownPosiion_browserSpace = curEvent.mousePosition; + + var pressedItemIndex = (mouseDownPosition_rowsSpace.y / rowHeight).FloorToInt(); + if (pressedItemIndex.IsInRangeOf(data.curPage.items) && !mousePressedOnCrossButtonArea && !mousePressedOnWidget) + pressedItem = data.curPage.items[pressedItemIndex]; + + doubleclickUnhandled = !mousePressedOnCrossButtonArea && curEvent.clickCount == 2; + + curEvent.Use(); + + } + void up() + { + if (!curEvent.isMouseUp) return; + + mousePressed = false; + doubleclickUnhandled = false; + pressedItem = null; + + } + + position(); + down(); + up(); + + } + + static bool mousePressed; + static bool mousePresesdOnItem => pressedItem != null; + static bool mousePressedOnCrossButtonArea; + static bool mousePressedOnWidget; + static bool doubleclickUnhandled; + + static float groupRectOffsetY => isOneColumn ? 0 : -20; + + static Vector2 mousePosiion_browserSpace; + static Vector2 mousePosition_groupSpace => mousePosiion_browserSpace.AddY(groupRectOffsetY); + static Vector2 mousePosition_rowsSpace => mousePosiion_browserSpace.AddY(groupRectOffsetY + data.curPage.scrollPos); + + static Vector2 mouseDownPosiion_browserSpace; + static Vector2 mouseDownPosition_groupSpace => mouseDownPosiion_browserSpace.AddY(groupRectOffsetY); + static Vector2 mouseDownPosition_rowsSpace => mouseDownPosiion_browserSpace.AddY(groupRectOffsetY + data.curPage.scrollPos); + + static float mouseDragDistance => (mousePosiion_browserSpace - mouseDownPosiion_browserSpace).magnitude; + + + static Item pressedItem; + + + + + + + static void UpdateAnimations() // Update + { + void calcDeltaTime() + { + deltaTime = (float)(EditorApplication.timeSinceStartup - lastLayoutTime); + + if (deltaTime > .05f) + deltaTime = .0166f; + + lastLayoutTime = EditorApplication.timeSinceStartup; + + } + void opacity() + { + if (!VFavoritesMenu.fadeAnimationsEnabled) { currentOpacity = targetOpacity; return; } + if (!UnityEditorInternal.InternalEditorUtility.isApplicationActive) { currentOpacity = targetOpacity; return; } + + SmoothDamp(ref currentOpacity, targetOpacity, 11, ref currentOpacityDerivative, deltaTime); + + if (targetOpacity == 0 && currentOpacity < .04f) + currentOpacity = 0; + + } + void pagesScroll() + { + if (!data) return; + if (!VFavoritesMenu.pageScrollAnimationEnabled) { pagesScrollPos = data.curPageIndex; return; } + + if (pagesScrollPos == -1) + pagesScrollPos = data.curPageIndex; + + SmoothDamp(ref pagesScrollPos, data.curPageIndex, 5, ref pagesScrollDerivative, deltaTime); + + if (pagesScrollPos.DistanceTo(data.curPageIndex) < .001f) + pagesScrollPos = data.curPageIndex; + + } + void rowGaps() + { + if (!data) return; + + var lerpSpeed = 10; + + for (int i = 0; i < data.curPage.rowGaps.Count; i++) + data.curPage.rowGaps[i] = Lerp(data.curPage.rowGaps[i], draggingItem && i == insertDraggedItemAtIndex ? rowHeight : 0, lerpSpeed, deltaTime); + + + } + void droppedItem() + { + if (!animatingDroppedItem) return; + + var yLerpSpeed = 8; + var shadowLerpSpeed = 8; + var highlightLerpSpeed = 10; + + SmoothDamp(ref droppedItemY_rowsSpace, data.curPage.items.IndexOf(VFavorites.droppedItem) * rowHeight, yLerpSpeed, ref droppedItemYDerivative, deltaTime); + Lerp(ref droppedItemShadowAmount, 0, shadowLerpSpeed, deltaTime); + Lerp(ref droppedItemHighlightAmount, 0, highlightLerpSpeed, deltaTime); + + if (droppedItemShadowAmount < .01f) + animatingDroppedItem = false; + + } + void pageButtons() + { + if (!VFavoritesMenu.pageScrollAnimationEnabled) { prevPageButtonBrightness = nextPageButtonBrightness = 1; return; } + + var lerpSpeed = 7; + + Lerp(ref prevPageButtonBrightness, 1, lerpSpeed, deltaTime); + Lerp(ref nextPageButtonBrightness, 1, lerpSpeed, deltaTime); + + + } + + calcDeltaTime(); + opacity(); + pagesScroll(); + rowGaps(); + droppedItem(); + pageButtons(); + + } + + static void CancelRowAnimations() + { + for (int i = 0; i < data.curPage.rowGaps.Count; i++) + data.curPage.rowGaps[i] = 0; + + animatingDroppedItem = false; + droppedItem = null; + + } + + static float deltaTime; + static double lastLayoutTime; + + static bool animatingRowGaps => data.curPage.rowGaps.Any(r => r > .1f && r < rowHeight - .1f); + + static float pagesScrollPos = -1; + static float pagesScrollDerivative; + static bool animatingPageScroll => !pagesScrollPos.Approx(data ? data.curPageIndex : 0); + + static float droppedItemY_rowsSpace; + static float droppedItemYDerivative; + static float droppedItemShadowAmount; + static float droppedItemHighlightAmount; + static bool animatingDroppedItem; + + static float prevPageButtonBrightness = 1; + static float nextPageButtonBrightness = 1; + + static float currentOpacity; + static float currentOpacityDerivative; + static float targetOpacity => isAltPressed || renamingPage || draggingItemFromPageToOutside || isWrappedBrowserLocked ? 1 : 0; // holdingAlt instead of shortcutPressed to prevent unwrapping due to incorrect event modifiers on key down on mac + static bool animatingOpacity => currentOpacity.DistanceTo(targetOpacity) > .01f; + + + + + + static void UpdateDragging() // called from WrappedOnGUI + { + void initFromOutside() + { + if (draggingItem) return; + if (!totalRect_browserSpace.IsHovered()) return; + if (!curEvent.isDragUpdate) return; + if (!DragAndDrop.objectReferences.FirstOrDefault()) return; + if (draggingItemFromPageToOutside) return; // to avoid duplication when dragging item from page to outside and back + + + animatingDroppedItem = false; + + draggingItem = true; + draggingItemFromPage = false; + + draggedItem = new Item(DragAndDrop.objectReferences.FirstOrDefault()); + draggedItemHoldOffset = 0; + + data.curPage.lastItemDragTime_ticks = System.DateTime.UtcNow.Ticks; + + } + void initFromPage() + { + if (draggingItem) return; + if (!totalRect_browserSpace.IsHovered()) return; + if (!curEvent.isMouseDrag) return; + if (mouseDragDistance < 2) return; + + + var i = (mouseDownPosition_rowsSpace.y / rowHeight).FloorToInt(); + + if (i >= data.curPage.items.Count) return; + if (i < 0) return; // somehow i = -1 with mouseDownPosition_rowsSpace.y = -20 when dragging in from outside quickly + + + animatingDroppedItem = false; + + draggingItem = true; + draggingItemFromPage = true; + draggingItemFromPageAtIndex = i; + + draggedItem = data.curPage.items[i]; + draggedItemHoldOffset = (i * rowHeight + rowHeight / 2) - mouseDownPosition_rowsSpace.y; + + data.curPage.lastItemDragTime_ticks = System.DateTime.UtcNow.Ticks; + + data.curPage.items.Remove(draggedItem); + data.curPage.rowGaps[draggingItemFromPageAtIndex] = rowHeight; + + data.Dirty(); + data.Save(); + + } + + void acceptFromOutside() + { + if (!draggingItem) return; + if (!curEvent.isDragPerform) return; + + DragAndDrop.AcceptDrag(); + curEvent.Use(); + + AcceptDragging(); + + } + void acceptFromPage() + { + if (!draggingItem) return; + if (!curEvent.isMouseUp) return; + + curEvent.Use(); + + AcceptDragging(); + } + + void cancelFromOutside() + { + if (!draggingItemFromOutside) return; + if (totalRect_browserSpace.IsHovered()) return; + + CancelDragging(); + + } + void cancelFromPageAndInitToOutside() + { + if (!curEvent.isMouseDrag) return; + if (!draggingItemFromPage) return; + if (totalRect_browserSpace.IsHovered()) return; + if (DragAndDrop.objectReferences.Any()) return; + + + DragAndDrop.PrepareStartDrag(); + DragAndDrop.objectReferences = new[] { draggedItem.obj }; + DragAndDrop.StartDrag(draggedItem.name); + + CancelDragging(); + + draggingItemFromPageToOutside = true; + + } + + void setVisualMode() + { + if (!draggingItem) return; + + DragAndDrop.visualMode = DragAndDropVisualMode.Generic; + + } + void setHotControl() + { + if (!draggingItem) return; + + EditorGUIUtility.hotControl = EditorGUIUtility.GetControlID(FocusType.Passive); + + } + + void resetDraggingItemFromPageToOutside() // delayCall loop + { + if (!DragAndDrop.objectReferences.Any()) + draggingItemFromPageToOutside = false; + + EditorApplication.delayCall -= resetDraggingItemFromPageToOutside; + EditorApplication.delayCall += resetDraggingItemFromPageToOutside; + + // DragAndDrop.objectReferences is unreliable outside of delayCall + + } + + + initFromOutside(); + initFromPage(); + + acceptFromOutside(); + acceptFromPage(); + + cancelFromOutside(); + cancelFromPageAndInitToOutside(); + + setVisualMode(); + setHotControl(); + + EditorApplication.delayCall -= resetDraggingItemFromPageToOutside; + EditorApplication.delayCall += resetDraggingItemFromPageToOutside; + + } + + static void AcceptDragging() + { + draggingItem = false; + draggingItemFromPage = false; + mousePressed = false; + + data.curPage.items.AddAt(draggedItem, insertDraggedItemAtIndex); + + data.curPage.rowGaps[insertDraggedItemAtIndex] -= rowHeight; + data.curPage.rowGaps.AddAt(0, insertDraggedItemAtIndex); + + droppedItem = draggedItem; + + droppedItemY_rowsSpace = draggedItemY_groupSpace + data.curPage.scrollPos; + droppedItemYDerivative = 0; + droppedItemShadowAmount = droppedItemHighlightAmount = 1; + animatingDroppedItem = true; + + draggedItem = null; + + EditorGUIUtility.hotControl = 0; + + data.Dirty(); + data.Save(); + + } + static void CancelDragging() + { + if (!draggingItem) return; + + draggingItem = false; + mousePressed = false; + + + if (!draggingItemFromPage) { draggedItem = null; EditorGUIUtility.hotControl = 0; return; } + + data.curPage.items.AddAt(draggedItem, draggingItemFromPageAtIndex); + + data.curPage.rowGaps[draggingItemFromPageAtIndex] -= rowHeight; + droppedItem = draggedItem; + droppedItemY_rowsSpace = draggedItemY_groupSpace - data.curPage.scrollPos; + droppedItemShadowAmount = droppedItemHighlightAmount = 1; + animatingDroppedItem = true; + + draggingItemFromPage = false; + + draggedItem = null; + + EditorGUIUtility.hotControl = 0; + + data.Dirty(); + data.Save(); + + } + + static bool draggingItem; + static bool draggingItemFromPage; + static bool draggingItemFromPageToOutside; + static bool draggingItemFromOutside => draggingItem && !draggingItemFromPage; + static int draggingItemFromPageAtIndex; + static Item draggedItem; + static float draggedItemHoldOffset; + static float draggedItemY_groupSpace => (mousePosition_groupSpace.y - rowHeight / 2 + draggedItemHoldOffset).Clamp(0, 12321); + static float draggedItemY_rowsSpace => draggedItemY_groupSpace + data.curPage.scrollPos; + static int insertDraggedItemAtIndex => ((mousePosition_rowsSpace.y + draggedItemHoldOffset) / rowHeight).FloorToInt().Clamp(0, data.curPage.items.Count); + + static Item droppedItem; + + + + + + + + + + + + + + + static void UpdateGUIWrapping() // called from EditorApplicaton.update + { + void wrap() + { + if (wrappedBrowser) return; + if (!UnityEditorInternal.InternalEditorUtility.isApplicationActive) return; + if (!shortcutPressed) return; + if (EditorWindow.mouseOverWindow?.GetType() != t_BrowserWindow) return; + if (t_VTabs != null && !EditorPrefsCached.GetBool("vTabs-pluginDisabled", false) && EditorWindow.mouseOverWindow.GetMemberValue("isLocked")) return; + + + WrapBrowserGUI(EditorWindow.mouseOverWindow); + + wrappedBrowser = EditorWindow.mouseOverWindow; + + wrappedBrowser.Focus(); + wrappedBrowser.Repaint(); + + t_BrowserWindow.SetFieldValue("s_LastInteractedProjectBrowser", wrappedBrowser); // so vTabs can copy its layout setting + + + wrappedBrowserWasLockedBeforeWrapping = wrappedBrowser.GetMemberValue("isLocked"); + + } + void unwrap() + { + if (!wrappedBrowser) return; + if (shortcutPressed && wrappedBrowser.hasFocus) return; + if (currentOpacity > 0 && wrappedBrowser.hasFocus) return; + + + CancelDragging(); + CancelRowAnimations(); + + UnwrapBrowserGUI(); + + wrappedBrowser.Repaint(); + + wrappedBrowser = null; + + } + + unwrap(); + wrap(); + + } + + static void WrapBrowserGUI(EditorWindow browser) + { + var hostView = fi_m_Parent.GetValue(browser); + var newDelegate = mi_WrappedOnGUI.CreateDelegate(t_EditorWindowDelegate, hostView); + + origBrowserOnGUIDelegate = fi_m_OnGUI.GetValue(hostView) as System.Delegate; + fi_m_OnGUI.SetValue(hostView, newDelegate); + + } + static void UnwrapBrowserGUI() + { + if (wrappedBrowser.GetFieldValue("m_Parent").GetFieldValue("m_OnGUI").Method != mi_WrappedOnGUI) return; + + wrappedBrowser.GetFieldValue("m_Parent").SetFieldValue("m_OnGUI", origBrowserOnGUIDelegate); + + } + + static EditorWindow wrappedBrowser; + static System.Delegate origBrowserOnGUIDelegate; + + static bool wrappedBrowserWasLockedBeforeWrapping; + + static bool shortcutPressed + { + get + { + if (VFavoritesMenu.activeOnAltEnabled) + return isAltPressed; + + if (VFavoritesMenu.activeOnAltShiftEnabled) + return curEvent.modifiers == (EventModifiers.Alt | EventModifiers.Shift); + + if (VFavoritesMenu.activeOnCtrlAltEnabled) + if (Application.platform == RuntimePlatform.OSXEditor) + return curEvent.modifiers == (EventModifiers.Command | EventModifiers.Alt); + else + return curEvent.modifiers == (EventModifiers.Control | EventModifiers.Alt); + + + return false; + + } + } + + + + + static bool isAltPressed + { + get + { +#if !UNITY_EDITOR_LINUX + return curEvent.holdingAlt; +#else + + if (curEvent.holdingAlt) + _isAltPressed_cachedForLinux = true; + + else if (curEvent.keyCode == KeyCode.LeftAlt) + if (curEvent.isKeyDown) + _isAltPressed_cachedForLinux = true; + + else if (curEvent.isKeyUp) + _isAltPressed_cachedForLinux = false; + + + return _isAltPressed_cachedForLinux; + +#endif + } + } + static bool _isAltPressed_cachedForLinux; + + + + + + + static void UpdateLocking() // called from EditorApplicaton.update + { + void unsetWrappedBrowser() + { + if (!isWrappedBrowserLocked) return; + + if (wrappedBrowser.GetFieldValue("m_Parent").GetFieldValue("m_OnGUI").Method == mi_WrappedOnGUI) return; + + wrappedBrowser = null; + + // nulls wrappedBrowser if it was unwrapped externally + // ie when locked browser gets moved or maximized + + } + void markLockedBrowser() + { + if (!lockedBrowser) return; + + MarkAsLocked(lockedBrowser); + + // fixes marking getting reset on scene load + + } + void setMinWidthOnLockedBrowser() + { + if (!lockedBrowser) return; + if (lockedBrowser.minSize.x == 100) return; + + lockedBrowser.minSize = Vector2.one * 100; + + } + void setTitle() + { + + if (!lockedBrowser) return; + if (lockedBrowser.GetFieldValue("m_ViewMode") != 0) return; // one column) + if (lockedBrowser.titleContent.text == "vFavorites") return; + + var icon = EditorIcons.GetIcon("Favorite"); + + lockedBrowser.titleContent = new GUIContent("vFavorites", icon); + + } + + void lock_() + { + if (lockedBrowser) return; + if (!wrappedBrowser) return; + if (!wrappedBrowser.GetMemberValue("isLocked")) return; + if (wrappedBrowserWasLockedBeforeWrapping) return; + + lockedBrowser = wrappedBrowser; + + EditorPrefsCached.SetInt("vFavorites-lockedBrowserHash", lockedBrowser.GetHashCode()); + EditorPrefsCached.SetInt("vFavorites-lockedBrowserDockAreaInstanceId", lockedBrowser.GetMemberValue("m_Parent").GetInstanceID()); + + curEvent.Use(); + + } + void unlock() + { + if (!lockedBrowser) return; + if (lockedBrowser.GetMemberValue("isLocked")) return; + + + lockedBrowser.titleContent = lockedBrowser.InvokeMethod("GetLocalizedTitleContent"); + + lockedBrowser = null; + + EditorPrefsCached.SetInt("vFavorites-lockedBrowserHash", 0); + EditorPrefsCached.SetInt("vFavorites-lockedBrowserDockAreaInstanceId", 0); + + curEvent.Use(); + + } + void wrap() + { + if (!lockedBrowser) return; + if (!lockedBrowser.hasFocus) return; + if (isWrappedBrowserLocked) return; + + WrapBrowserGUI(lockedBrowser); + + currentOpacity = 1; + + wrappedBrowser = lockedBrowser; + + } + + + unsetWrappedBrowser(); + markLockedBrowser(); + setMinWidthOnLockedBrowser(); + setTitle(); + + lock_(); + unlock(); + wrap(); + + } + + static EditorWindow lockedBrowser + { + get + { + if (_lockedBrowser) return _lockedBrowser; + + + var lockedBrowserInstanceId = EditorPrefsCached.GetInt("vFavorites-lockedBrowserInstanceId", 0); + + if (lockedBrowserInstanceId == 0) return null; + + + + var window = _EditorUtility_InstanceIDToObject(lockedBrowserInstanceId) as EditorWindow; + + if (window && window.GetType() == t_BrowserWindow) // prevents iid collisions + _lockedBrowser = window; + + if (_lockedBrowser) return _lockedBrowser; + + + + _lockedBrowser = allBrowsers.FirstOrDefault(r => IsMarkedAsLocked(r)); + + if (_lockedBrowser) return _lockedBrowser; // todo set instanceid + + + + // EditorPrefsCached.SetInt("vFavorites-lockedBrowserInstanceId", 0); // one attempt to find the locked browser isn't enough after unmaximize + + return null; + + } + set + { + if (_lockedBrowser) + MarkAsUnlocked(_lockedBrowser); + + if (value == null) + { + EditorPrefsCached.SetInt("vFavorites-lockedBrowserInstanceId", 0); + + _lockedBrowser = null; + + } + else + { + EditorPrefsCached.SetInt("vFavorites-lockedBrowserInstanceId", value.GetInstanceID()); + + MarkAsLocked(value); + + _lockedBrowser = value; + + } + + } + } + static EditorWindow _lockedBrowser; + + static bool IsMarkedAsLocked(EditorWindow browser) => browser.GetMemberValue("m_SearchFilter")?.GetMemberValue("m_OriginalText") == "asd"; + static void MarkAsLocked(EditorWindow browser) => browser.GetMemberValue("m_SearchFilter")?.SetMemberValue("m_OriginalText", "asd"); + static void MarkAsUnlocked(EditorWindow browser) => browser.GetMemberValue("m_SearchFilter")?.SetMemberValue("m_OriginalText", ""); + + static bool isWrappedBrowserLocked => wrappedBrowser && wrappedBrowser == lockedBrowser; + + static bool CanBrowserBeWrapped_byVTabs(EditorWindow browser) => !IsMarkedAsLocked(browser); + + + + + static void RepaintOnAltUp() // Update + { + var lastEvent = typeof(Event).GetFieldValue("s_Current"); + + if (wasAlt && !lastEvent.alt) + if (EditorWindow.mouseOverWindow?.GetType() == t_BrowserWindow) + EditorApplication.RepaintProjectWindow(); + + wasAlt = lastEvent.alt; + + } + + static bool wasAlt; + + + + + + + + + + + + + [InitializeOnLoadMethod] + static void Init() + { + if (VFavoritesMenu.pluginDisabled) return; + + void subscribe() + { + EditorApplication.update -= UpdateGUIWrapping; + EditorApplication.update += UpdateGUIWrapping; + + EditorApplication.update -= UpdateLocking; + EditorApplication.update += UpdateLocking; + + EditorApplication.update -= UpdateAnimations; + EditorApplication.update += UpdateAnimations; + + EditorApplication.update -= RepaintOnAltUp; + EditorApplication.update += RepaintOnAltUp; + + + + EditorApplication.quitting -= VFavoritesState.Save; + EditorApplication.quitting += VFavoritesState.Save; + + } + void loadData() + { + data = AssetDatabase.LoadAssetAtPath(EditorPrefsCached.GetString("vFavorites-lastKnownDataPath-" + GetProjectId())); + + + if (data) return; + + data = AssetDatabase.FindAssets("t:VFavoritesData").Select(guid => AssetDatabase.LoadAssetAtPath(guid.ToPath())).FirstOrDefault(); + + + if (!data) return; + + EditorPrefsCached.SetString("vFavorites-lastKnownDataPath-" + GetProjectId(), data.GetPath()); + + } + void loadDataDelayed() + { + if (data) return; + + EditorApplication.delayCall += () => EditorApplication.delayCall += loadData; + + // AssetDatabase isn't up to date at this point (it gets updated after InitializeOnLoadMethod) + // and if current AssetDatabase state doesn't contain the data - it won't be loaded during Init() + // so here we schedule an additional, delayed attempt to load the data + // this addresses reports of data loss when trying to load it on a new machine + + } + + subscribe(); + loadData(); + loadDataDelayed(); + + } + + public static VFavoritesData data; + + + + + + + + static void BeforeWindowCreated_byVTabs(object dockArea) + { + if (!wrappedBrowser) return; + if (wrappedBrowser.GetFieldValue("m_Parent") != dockArea) return; + + CancelDragging(); + CancelRowAnimations(); + + UnwrapBrowserGUI(); + + draggingItemFromPageToOutside = false; + + } + + + + + + + static IEnumerable allBrowsers => _allBrowsers ??= t_BrowserWindow.GetFieldValue("s_ProjectBrowsers").Cast(); + static IEnumerable _allBrowsers; + + static Type t_BrowserWindow = typeof(Editor).Assembly.GetType("UnityEditor.ProjectBrowser"); + static Type t_HostView = typeof(Editor).Assembly.GetType("UnityEditor.HostView"); + static Type t_EditorWindowDelegate = t_HostView.GetNestedType("EditorWindowDelegate", maxBindingFlags); + static FieldInfo fi_m_Parent = typeof(EditorWindow).GetField("m_Parent", maxBindingFlags); + static FieldInfo fi_m_OnGUI = t_HostView.GetField("m_OnGUI", maxBindingFlags); + static MethodInfo mi_WrappedOnGUI = typeof(VFavorites).GetMethod(nameof(WrappedOnGUI), maxBindingFlags); + + + static Type t_VHierarchy = Type.GetType("VHierarchy.VHierarchy") ?? Type.GetType("VHierarchy.VHierarchy, VHierarchy, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"); + static Type t_VFolders = Type.GetType("VFolders.VFolders") ?? Type.GetType("VFolders.VFolders, VFolders, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"); + static Type t_VTabs = Type.GetType("VTabs.VTabs") ?? Type.GetType("VTabs.VTabs, VTabs, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"); + + static MethodInfo mi_VHierarchy_GetIconName = t_VHierarchy?.GetMethod("GetIconName_forVFavorites", maxBindingFlags); + static MethodInfo mi_VFolders_DrawBigFolderIcon = t_VFolders?.GetMethod("DrawBigFolderIcon_forVFavorites", maxBindingFlags); + + + + + + const string version = "2.0.14"; + + } +} +#endif diff --git a/Assets/vFavorites/VFavorites.cs.meta b/Assets/vFavorites/VFavorites.cs.meta new file mode 100644 index 00000000..4b491bb4 --- /dev/null +++ b/Assets/vFavorites/VFavorites.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 6651039474d594cdd91489768cf293f6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 263643 + packageName: vFavorites 2 + packageVersion: 2.0.14 + assetPath: Assets/vFavorites/VFavorites.cs + uploadId: 874224 diff --git a/Assets/vFavorites/VFavoritesData.cs b/Assets/vFavorites/VFavoritesData.cs new file mode 100644 index 00000000..c2dbe74c --- /dev/null +++ b/Assets/vFavorites/VFavoritesData.cs @@ -0,0 +1,225 @@ +#if UNITY_EDITOR +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Serialization; +using UnityEditor; +using UnityEditor.ShortcutManagement; +using System.Reflection; +using System.Linq; +using UnityEngine.UIElements; +using UnityEngine.SceneManagement; +using UnityEditor.SceneManagement; +using Type = System.Type; +using static VFavorites.VFavoritesState; +using static VFavorites.Libs.VUtils; +using static VFavorites.Libs.VGUI; + + + +namespace VFavorites +{ + public class VFavoritesData : ScriptableObject + { + public List pages = new List(); + + public Page curPage + { + get + { + while (curPageIndex >= pages.Count - 1) + pages.Add(new Page("Page " + (pages.Count + 1))); + + return pages[curPageIndex]; + + } + } + + public int curPageIndex { get => VFavoritesState.instance.curPageIndex; set => VFavoritesState.instance.curPageIndex = value; } + + public float rowScale = 1; + + + [System.Serializable] + public class Page + { + public List items = new List(); + + public string name = ""; + + public Page(string name) => this.name = name; + + + + + [System.NonSerialized] + public List _rowGaps = new List(); + public List rowGaps + { + get + { + if (_rowGaps == null) + _rowGaps = new List(); + + while (_rowGaps.Count < items.Count + 1) _rowGaps.Add(0); + while (_rowGaps.Count > items.Count + 1) _rowGaps.RemoveLast(); + + return _rowGaps; + + } + } + + + public float scrollPos { get => state.scrollPos; set => state.scrollPos = value; } + + public long lastItemSelectTime_ticks { get => state.lastItemSelectTime_ticks; set => state.lastItemSelectTime_ticks = value; } + public long lastItemDragTime_ticks { get => state.lastItemDragTime_ticks; set => state.lastItemDragTime_ticks = value; } + + + public PageState state + { + get + { + if (!VFavoritesState.instance.pageStates_byPageId.ContainsKey(id)) + VFavoritesState.instance.pageStates_byPageId[id] = new PageState(); + + return VFavoritesState.instance.pageStates_byPageId[id]; + + } + } + + public int id + { + get + { + if (_id == 0) + _id = Random.value.GetHashCode(); + + return _id; + + } + } + public int _id = 0; + + } + + [System.Serializable] + public class Item + { + public GlobalID globalId; + + + public Type type => Type.GetType(_typeString) ?? typeof(DefaultAsset); + public string _typeString; + + public Object obj => _obj != null ? _obj : (_obj = globalId.GetObject()); + public Object _obj; + + + public bool isSceneGameObject; + public bool isFolder; + public bool isAsset; + + + public bool isLoadable => obj != null; + + public bool isDeleted + { + get + { + if (!isSceneGameObject) + return !isLoadable; + + if (isLoadable) + return false; + + if (!AssetDatabase.LoadAssetAtPath(globalId.guid.ToPath())) + return true; + + for (int i = 0; i < EditorSceneManager.sceneCount; i++) + if (EditorSceneManager.GetSceneAt(i).path == globalId.guid.ToPath()) + return true; + + return false; + + } + } + + public string assetPath => globalId.guid.ToPath(); + + + public Item(Object o) + { + globalId = o.GetGlobalID(); + + + isSceneGameObject = o is GameObject go && go.scene.rootCount != 0; + isFolder = AssetDatabase.IsValidFolder(o.GetPath()); + isAsset = !isSceneGameObject && !isFolder; + + _typeString = o.GetType().AssemblyQualifiedName; + + _name = o.name; + + } + + + + + + public string name + { + get + { + if (!isLoadable) return _name; + + if (assetPath.GetExtension() == ".cs") + _name = obj.name.Decamelcase(); + else + _name = obj.name; + + return _name; + + } + } + public string _name { get => state._name; set => state._name = value; } + + public string sceneGameObjectIconName { get => state.sceneGameObjectIconName; set => state.sceneGameObjectIconName = value; } + + public long lastSelectTime_ticks { get => state.lastSelectTime_ticks; set => state.lastSelectTime_ticks = value; } + public bool isSelected { get => state.isSelected; set => state.isSelected = value; } + + + + public ItemState state + { + get + { + if (!VFavoritesState.instance.itemStates_byItemId.ContainsKey(id)) + VFavoritesState.instance.itemStates_byItemId[id] = new ItemState(); + + return VFavoritesState.instance.itemStates_byItemId[id]; + + } + } + + public int id + { + get + { + if (_id == 0) + _id = Random.value.GetHashCode(); + + return _id; + + } + } + public int _id = 0; + + + } + + + } +} +#endif \ No newline at end of file diff --git a/Assets/vFavorites/VFavoritesData.cs.meta b/Assets/vFavorites/VFavoritesData.cs.meta new file mode 100644 index 00000000..5d302ff2 --- /dev/null +++ b/Assets/vFavorites/VFavoritesData.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 066cf82f8f80d408c856e48fc8f1127b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 263643 + packageName: vFavorites 2 + packageVersion: 2.0.14 + assetPath: Assets/vFavorites/VFavoritesData.cs + uploadId: 874224 diff --git a/Assets/vFavorites/VFavoritesLibs.cs b/Assets/vFavorites/VFavoritesLibs.cs new file mode 100644 index 00000000..745493d2 --- /dev/null +++ b/Assets/vFavorites/VFavoritesLibs.cs @@ -0,0 +1,1893 @@ +#if UNITY_EDITOR +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using UnityEditor.ShortcutManagement; +using System.Reflection; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using System.Linq; +using UnityEngine.UIElements; +using Type = System.Type; +using static VFavorites.Libs.VUtils; + + +namespace VFavorites.Libs +{ + public static class VUtils + { + #region Text + + + public static string Decamelcase(this string str) => Regex.Replace(Regex.Replace(str, @"(\P{Ll})(\P{Ll}\p{Ll})", "$1 $2"), @"(\p{Ll})(\P{Ll})", "$1 $2"); + + public static string Remove(this string s, string toRemove) + { + if (toRemove == "") return s; + return s.Replace(toRemove, ""); + } + + public static bool IsEmpty(this string s) => s == ""; + public static bool IsNullOrEmpty(this string s) => string.IsNullOrEmpty(s); + + + + + + #endregion + + #region IEnumerables + + + public static T AddAt(this List l, T r, int i) + { + if (i < 0) i = 0; + if (i >= l.Count) + l.Add(r); + else + l.Insert(i, r); + return r; + } + public static T RemoveLast(this List l) + { + if (!l.Any()) return default; + + var r = l.Last(); + + l.RemoveAt(l.Count - 1); + + return r; + } + + public static void Add(this List list, params T[] items) + { + foreach (var r in items) + list.Add(r); + } + + public static int LastIndex(this List l) => l.Count - 1; // toremove + + // public static T GetAtWrapped(this List list, int i) // toremove + // { + // while (i < 0) i += list.Count; + // while (i >= list.Count) i -= list.Count; + + // return list[i]; + // } + + + + + + #endregion + + #region Linq + + + public static T NextTo(this IEnumerable e, T to) => e.SkipWhile(r => !r.Equals(to)).Skip(1).FirstOrDefault(); + public static T PreviousTo(this IEnumerable e, T to) => e.Reverse().SkipWhile(r => !r.Equals(to)).Skip(1).FirstOrDefault(); + public static T NextToOtFirst(this IEnumerable e, T to) => e.NextTo(to) ?? e.First(); + public static T PreviousToOrLast(this IEnumerable e, T to) => e.PreviousTo(to) ?? e.Last(); + + public static Dictionary MergeDictionaries(IEnumerable> dicts) + { + if (dicts.Count() == 0) return null; + if (dicts.Count() == 1) return dicts.First(); + + var mergedDict = new Dictionary(dicts.First()); + + foreach (var dict in dicts.Skip(1)) + foreach (var r in dict) + if (!mergedDict.ContainsKey(r.Key)) + mergedDict.Add(r.Key, r.Value); + + return mergedDict; + } + + public static IEnumerable InsertFirst(this IEnumerable ie, T t) => new[] { t }.Concat(ie); + + public static bool None(this IEnumerable ie, System.Func f) => !ie.Any(f); + public static bool None(this IEnumerable ie) => !ie.Any(); + + public static int IndexOfFirst(this List list, System.Func f) => list.FirstOrDefault(f) is T t ? list.IndexOf(t) : -1; + public static int IndexOfLast(this List list, System.Func f) => list.LastOrDefault(f) is T t ? list.IndexOf(t) : -1; + + public static void SortBy(this List list, System.Func keySelector) where T2 : System.IComparable => list.Sort((q, w) => keySelector(q).CompareTo(keySelector(w))); + + public static void RemoveValue(this IDictionary dictionary, TValue value) + { + if (dictionary.FirstOrDefault(r => r.Value.Equals(value)) is var kvp) + dictionary.Remove(kvp); + } + + + public static void ForEach(this IEnumerable sequence, System.Action action) { foreach (T item in sequence) action(item); } + + + + + + #endregion + + #region Reflection + + public static object GetFieldValue(this object o, string fieldName, bool exceptionIfNotFound = true) + { + var type = (o as Type) ?? o.GetType(); + var target = o is Type ? null : o; + + + if (type.GetFieldInfo(fieldName) is FieldInfo fieldInfo) + return fieldInfo.GetValue(target); + + + if (exceptionIfNotFound) + throw new System.Exception($"Field '{fieldName}' not found in type '{type.Name}' and its parent types"); + + return null; + + } + public static object GetPropertyValue(this object o, string propertyName, bool exceptionIfNotFound = true) + { + var type = (o as Type) ?? o.GetType(); + var target = o is Type ? null : o; + + + if (type.GetPropertyInfo(propertyName) is PropertyInfo propertyInfo) + return propertyInfo.GetValue(target); + + + if (exceptionIfNotFound) + throw new System.Exception($"Property '{propertyName}' not found in type '{type.Name}' and its parent types"); + + return null; + + } + public static object GetMemberValue(this object o, string memberName, bool exceptionIfNotFound = true) + { + var type = (o as Type) ?? o.GetType(); + var target = o is Type ? null : o; + + + if (type.GetFieldInfo(memberName) is FieldInfo fieldInfo) + return fieldInfo.GetValue(target); + + if (type.GetPropertyInfo(memberName) is PropertyInfo propertyInfo) + return propertyInfo.GetValue(target); + + + if (exceptionIfNotFound) + throw new System.Exception($"Member '{memberName}' not found in type '{type.Name}' and its parent types"); + + return null; + + } + + public static void SetFieldValue(this object o, string fieldName, object value, bool exceptionIfNotFound = true) + { + var type = (o as Type) ?? o.GetType(); + var target = o is Type ? null : o; + + + if (type.GetFieldInfo(fieldName) is FieldInfo fieldInfo) + fieldInfo.SetValue(target, value); + + + else if (exceptionIfNotFound) + throw new System.Exception($"Field '{fieldName}' not found in type '{type.Name}' and its parent types"); + + } + public static void SetPropertyValue(this object o, string propertyName, object value, bool exceptionIfNotFound = true) + { + var type = (o as Type) ?? o.GetType(); + var target = o is Type ? null : o; + + + if (type.GetPropertyInfo(propertyName) is PropertyInfo propertyInfo) + propertyInfo.SetValue(target, value); + + + else if (exceptionIfNotFound) + throw new System.Exception($"Property '{propertyName}' not found in type '{type.Name}' and its parent types"); + + } + public static void SetMemberValue(this object o, string memberName, object value, bool exceptionIfNotFound = true) + { + var type = (o as Type) ?? o.GetType(); + var target = o is Type ? null : o; + + + if (type.GetFieldInfo(memberName) is FieldInfo fieldInfo) + fieldInfo.SetValue(target, value); + + else if (type.GetPropertyInfo(memberName) is PropertyInfo propertyInfo) + propertyInfo.SetValue(target, value); + + + else if (exceptionIfNotFound) + throw new System.Exception($"Member '{memberName}' not found in type '{type.Name}' and its parent types"); + + } + + public static object InvokeMethod(this object o, string methodName, params object[] parameters) // todo handle null params (can't get their type) + { + var type = (o as Type) ?? o.GetType(); + var target = o is Type ? null : o; + + + if (type.GetMethodInfo(methodName, parameters.Select(r => r.GetType()).ToArray()) is MethodInfo methodInfo) + return methodInfo.Invoke(target, parameters); + + + throw new System.Exception($"Method '{methodName}' not found in type '{type.Name}', its parent types and interfaces"); + + } + + + + static FieldInfo GetFieldInfo(this Type type, string fieldName) + { + if (fieldInfoCache.TryGetValue(type, out var fieldInfosByNames)) + if (fieldInfosByNames.TryGetValue(fieldName, out var fieldInfo)) + return fieldInfo; + + + if (!fieldInfoCache.ContainsKey(type)) + fieldInfoCache[type] = new Dictionary(); + + for (var curType = type; curType != null; curType = curType.BaseType) + if (curType.GetField(fieldName, maxBindingFlags) is FieldInfo fieldInfo) + return fieldInfoCache[type][fieldName] = fieldInfo; + + + return fieldInfoCache[type][fieldName] = null; + + } + static Dictionary> fieldInfoCache = new Dictionary>(); + + static PropertyInfo GetPropertyInfo(this Type type, string propertyName) + { + if (propertyInfoCache.TryGetValue(type, out var propertyInfosByNames)) + if (propertyInfosByNames.TryGetValue(propertyName, out var propertyInfo)) + return propertyInfo; + + + if (!propertyInfoCache.ContainsKey(type)) + propertyInfoCache[type] = new Dictionary(); + + for (var curType = type; curType != null; curType = curType.BaseType) + if (curType.GetProperty(propertyName, maxBindingFlags) is PropertyInfo propertyInfo) + return propertyInfoCache[type][propertyName] = propertyInfo; + + + return propertyInfoCache[type][propertyName] = null; + + } + static Dictionary> propertyInfoCache = new Dictionary>(); + + static MethodInfo GetMethodInfo(this Type type, string methodName, params Type[] argumentTypes) + { + var methodHash = methodName.GetHashCode() ^ argumentTypes.Aggregate(0, (hash, r) => hash ^= r.GetHashCode()); + + + if (methodInfoCache.TryGetValue(type, out var methodInfosByHashes)) + if (methodInfosByHashes.TryGetValue(methodHash, out var methodInfo)) + return methodInfo; + + + + if (!methodInfoCache.ContainsKey(type)) + methodInfoCache[type] = new Dictionary(); + + for (var curType = type; curType != null; curType = curType.BaseType) + if (curType.GetMethod(methodName, maxBindingFlags, null, argumentTypes, null) is MethodInfo methodInfo) + return methodInfoCache[type][methodHash] = methodInfo; + + foreach (var interfaceType in type.GetInterfaces()) + if (interfaceType.GetMethod(methodName, maxBindingFlags, null, argumentTypes, null) is MethodInfo methodInfo) + return methodInfoCache[type][methodHash] = methodInfo; + + + + return methodInfoCache[type][methodHash] = null; + + } + static Dictionary> methodInfoCache = new Dictionary>(); + + + + public static T GetFieldValue(this object o, string fieldName, bool exceptionIfNotFound = true) => (T)o.GetFieldValue(fieldName, exceptionIfNotFound); + public static T GetPropertyValue(this object o, string propertyName, bool exceptionIfNotFound = true) => (T)o.GetPropertyValue(propertyName, exceptionIfNotFound); + public static T GetMemberValue(this object o, string memberName, bool exceptionIfNotFound = true) => (T)o.GetMemberValue(memberName, exceptionIfNotFound); + public static T InvokeMethod(this object o, string methodName, params object[] parameters) => (T)o.InvokeMethod(methodName, parameters); + + + + + + + public static List GetSubclasses(this Type t) => t.Assembly.GetTypes().Where(type => type.IsSubclassOf(t)).ToList(); + + public static object GetDefaultValue(this FieldInfo f, params object[] constructorVars) => f.GetValue(System.Activator.CreateInstance(((MemberInfo)f).ReflectedType, constructorVars)); + + public static object GetDefaultValue(this FieldInfo f) => f.GetValue(System.Activator.CreateInstance(((MemberInfo)f).ReflectedType)); + + + public static IEnumerable GetFieldsWithoutBase(this Type t) => t.GetFields().Where(r => !t.BaseType.GetFields().Any(rr => rr.Name == r.Name)); + + public static IEnumerable GetPropertiesWithoutBase(this Type t) => t.GetProperties().Where(r => !t.BaseType.GetProperties().Any(rr => rr.Name == r.Name)); + + + public const BindingFlags maxBindingFlags = (BindingFlags)62; + + + + + + + + #endregion + + #region Math + + + public static bool Approx(this float f1, float f2) => Mathf.Approximately(f1, f2); + public static float DistanceTo(this float f1, float f2) => Mathf.Abs(f1 - f2); + public static float DistanceTo(this Vector2 f1, Vector2 f2) => (f1 - f2).magnitude; + public static float DistanceTo(this Vector3 f1, Vector3 f2) => (f1 - f2).magnitude; + public static float Dist(float f1, float f2) => Mathf.Abs(f1 - f2); + public static float Avg(float f1, float f2) => (f1 + f2) / 2; + public static float Abs(this float f) => Mathf.Abs(f); + public static int Abs(this int f) => Mathf.Abs(f); + public static float Sign(this float f) => Mathf.Sign(f); + public static float Clamp(this float f, float f0, float f1) => Mathf.Clamp(f, f0, f1); + public static int Clamp(this int f, int f0, int f1) => Mathf.Clamp(f, f0, f1); + public static float Clamp01(this float f) => Mathf.Clamp(f, 0, 1); + public static Vector2 Clamp01(this Vector2 f) => new Vector2(f.x.Clamp01(), f.y.Clamp01()); + public static Vector3 Clamp01(this Vector3 f) => new Vector3(f.x.Clamp01(), f.y.Clamp01(), f.z.Clamp01()); + + + public static float Pow(this float f, float pow) => Mathf.Pow(f, pow); + public static int Pow(this int f, int pow) => (int)Mathf.Pow(f, pow); + + public static float Round(this float f) => Mathf.Round(f); + public static float Ceil(this float f) => Mathf.Ceil(f); + public static float Floor(this float f) => Mathf.Floor(f); + public static int RoundToInt(this float f) => Mathf.RoundToInt(f); + public static int CeilToInt(this float f) => Mathf.CeilToInt(f); + public static int FloorToInt(this float f) => Mathf.FloorToInt(f); + public static int ToInt(this float f) => (int)f; + public static float ToFloat(this int f) => (float)f; + public static float ToFloat(this double f) => (float)f; + + + public static float Sqrt(this float f) => Mathf.Sqrt(f); + + public static float Max(this float f, float ff) => Mathf.Max(f, ff); + public static float Min(this float f, float ff) => Mathf.Min(f, ff); + public static int Max(this int f, int ff) => Mathf.Max(f, ff); + public static int Min(this int f, int ff) => Mathf.Min(f, ff); + + public static float Loop(this float f, float boundMin, float boundMax) + { + while (f < boundMin) f += boundMax - boundMin; + while (f > boundMax) f -= boundMax - boundMin; + return f; + } + public static float Loop(this float f, float boundMax) => f.Loop(0, boundMax); + + public static float PingPong(this float f, float boundMin, float boundMax) => boundMin + Mathf.PingPong(f - boundMin, boundMax - boundMin); + public static float PingPong(this float f, float boundMax) => f.PingPong(0, boundMax); + + + public static float TriangleArea(Vector2 A, Vector2 B, Vector2 C) => Vector3.Cross(A - B, A - C).z.Abs() / 2; + public static Vector2 LineIntersection(Vector2 A, Vector2 B, Vector2 C, Vector2 D) + { + var a1 = B.y - A.y; + var b1 = A.x - B.x; + var c1 = a1 * A.x + b1 * A.y; + + var a2 = D.y - C.y; + var b2 = C.x - D.x; + var c2 = a2 * C.x + b2 * C.y; + + var d = a1 * b2 - a2 * b1; + + var x = (b2 * c1 - b1 * c2) / d; + var y = (a1 * c2 - a2 * c1) / d; + + return new Vector2(x, y); + + } + + public static float ProjectOn(this Vector2 v, Vector2 on) => Vector3.Project(v, on).magnitude; + public static float AngleTo(this Vector2 v, Vector2 to) => Vector2.Angle(v, to); + + public static Vector2 Rotate(this Vector2 v, float deg) => Quaternion.AngleAxis(deg, Vector3.forward) * v; + + public static float Smoothstep(this float f) { f = f.Clamp01(); return f * f * (3 - 2 * f); } + + public static float InverseLerp(this Vector2 v, Vector2 a, Vector2 b) + { + var ab = b - a; + var av = v - a; + return Vector2.Dot(av, ab) / Vector2.Dot(ab, ab); + } + + public static bool IsOdd(this int i) => i % 2 == 1; + public static bool IsEven(this int i) => i % 2 == 0; + + public static bool IsInRange(this int i, int a, int b) => i >= a && i <= b; + public static bool IsInRange(this float i, float a, float b) => i >= a && i <= b; + + public static bool IsInRangeOf(this int i, IList list) => i.IsInRange(0, list.Count - 1); + public static bool IsInRangeOf(this int i, T[] array) => i.IsInRange(0, array.Length - 1); + + + + + + + + #endregion + + #region Lerping + + + public static float LerpT(float lerpSpeed, float deltaTime) => 1 - Mathf.Exp(-lerpSpeed * 2f * deltaTime); + public static float LerpT(float lerpSpeed) => LerpT(lerpSpeed, Time.deltaTime); + + public static float Lerp(float f1, float f2, float t) => Mathf.LerpUnclamped(f1, f2, t); + public static float Lerp(ref float f1, float f2, float t) => f1 = Lerp(f1, f2, t); + + public static Vector2 Lerp(Vector2 f1, Vector2 f2, float t) => Vector2.LerpUnclamped(f1, f2, t); + public static Vector2 Lerp(ref Vector2 f1, Vector2 f2, float t) => f1 = Lerp(f1, f2, t); + + public static Vector3 Lerp(Vector3 f1, Vector3 f2, float t) => Vector3.LerpUnclamped(f1, f2, t); + public static Vector3 Lerp(ref Vector3 f1, Vector3 f2, float t) => f1 = Lerp(f1, f2, t); + + public static Color Lerp(Color f1, Color f2, float t) => Color.LerpUnclamped(f1, f2, t); + public static Color Lerp(ref Color f1, Color f2, float t) => f1 = Lerp(f1, f2, t); + + + public static float Lerp(float current, float target, float speed, float deltaTime) => Mathf.Lerp(current, target, LerpT(speed, deltaTime)); + public static float Lerp(ref float current, float target, float speed, float deltaTime) => current = Lerp(current, target, speed, deltaTime); + + public static Vector2 Lerp(Vector2 current, Vector2 target, float speed, float deltaTime) => Vector2.Lerp(current, target, LerpT(speed, deltaTime)); + public static Vector2 Lerp(ref Vector2 current, Vector2 target, float speed, float deltaTime) => current = Lerp(current, target, speed, deltaTime); + + public static Vector3 Lerp(Vector3 current, Vector3 target, float speed, float deltaTime) => Vector3.Lerp(current, target, LerpT(speed, deltaTime)); + public static Vector3 Lerp(ref Vector3 current, Vector3 target, float speed, float deltaTime) => current = Lerp(current, target, speed, deltaTime); + + public static float SmoothDamp(float current, float target, float speed, ref float derivative, float deltaTime) => Mathf.SmoothDamp(current, target, ref derivative, .5f / speed, Mathf.Infinity, deltaTime); + public static float SmoothDamp(ref float current, float target, float speed, ref float derivative, float deltaTime) => current = SmoothDamp(current, target, speed, ref derivative, deltaTime); + public static float SmoothDamp(float current, float target, float speed, ref float derivative) => SmoothDamp(current, target, speed, ref derivative, Time.deltaTime); + public static float SmoothDamp(ref float current, float target, float speed, ref float derivative) => current = SmoothDamp(current, target, speed, ref derivative, Time.deltaTime); + + public static Vector2 SmoothDamp(Vector2 current, Vector2 target, float speed, ref Vector2 derivative, float deltaTime) => Vector2.SmoothDamp(current, target, ref derivative, .5f / speed, Mathf.Infinity, deltaTime); + public static Vector2 SmoothDamp(ref Vector2 current, Vector2 target, float speed, ref Vector2 derivative, float deltaTime) => current = SmoothDamp(current, target, speed, ref derivative, deltaTime); + public static Vector2 SmoothDamp(Vector2 current, Vector2 target, float speed, ref Vector2 derivative) => SmoothDamp(current, target, speed, ref derivative, Time.deltaTime); + public static Vector2 SmoothDamp(ref Vector2 current, Vector2 target, float speed, ref Vector2 derivative) => current = SmoothDamp(current, target, speed, ref derivative, Time.deltaTime); + + public static Vector3 SmoothDamp(Vector3 current, Vector3 target, float speed, ref Vector3 derivative, float deltaTime) => Vector3.SmoothDamp(current, target, ref derivative, .5f / speed, Mathf.Infinity, deltaTime); + public static Vector3 SmoothDamp(ref Vector3 current, Vector3 target, float speed, ref Vector3 derivative, float deltaTime) => current = SmoothDamp(current, target, speed, ref derivative, deltaTime); + public static Vector3 SmoothDamp(Vector3 current, Vector3 target, float speed, ref Vector3 derivative) => SmoothDamp(current, target, speed, ref derivative, Time.deltaTime); + public static Vector3 SmoothDamp(ref Vector3 current, Vector3 target, float speed, ref Vector3 derivative) => current = SmoothDamp(current, target, speed, ref derivative, Time.deltaTime); + + + + + + + #endregion + + #region Colors + + + public static Color HSLToRGB(float h, float s, float l) + { + float hue2Rgb(float v1, float v2, float vH) + { + if (vH < 0f) + vH += 1f; + + if (vH > 1f) + vH -= 1f; + + if (6f * vH < 1f) + return v1 + (v2 - v1) * 6f * vH; + + if (2f * vH < 1f) + return v2; + + if (3f * vH < 2f) + return v1 + (v2 - v1) * (2f / 3f - vH) * 6f; + + return v1; + } + + if (s.Approx(0)) return new Color(l, l, l); + + float k1; + + if (l < .5f) + k1 = l * (1f + s); + else + k1 = l + s - s * l; + + + var k2 = 2f * l - k1; + + float r, g, b; + r = hue2Rgb(k2, k1, h + 1f / 3); + g = hue2Rgb(k2, k1, h); + b = hue2Rgb(k2, k1, h - 1f / 3); + + return new Color(r, g, b); + } + public static Color LCHtoRGB(float l, float c, float h) + { + l *= 100; + c *= 100; + h *= 360; + + double xw = 0.948110; + double yw = 1.00000; + double zw = 1.07304; + + float a = c * Mathf.Cos(Mathf.Deg2Rad * h); + float b = c * Mathf.Sin(Mathf.Deg2Rad * h); + + float fy = (l + 16) / 116; + float fx = fy + (a / 500); + float fz = fy - (b / 200); + + float x = (float)System.Math.Round(xw * ((System.Math.Pow(fx, 3) > 0.008856) ? System.Math.Pow(fx, 3) : ((fx - 16 / 116) / 7.787)), 5); + float y = (float)System.Math.Round(yw * ((System.Math.Pow(fy, 3) > 0.008856) ? System.Math.Pow(fy, 3) : ((fy - 16 / 116) / 7.787)), 5); + float z = (float)System.Math.Round(zw * ((System.Math.Pow(fz, 3) > 0.008856) ? System.Math.Pow(fz, 3) : ((fz - 16 / 116) / 7.787)), 5); + + float r = x * 3.2406f - y * 1.5372f - z * 0.4986f; + float g = -x * 0.9689f + y * 1.8758f + z * 0.0415f; + float bValue = x * 0.0557f - y * 0.2040f + z * 1.0570f; + + r = r > 0.0031308f ? 1.055f * (float)System.Math.Pow(r, 1 / 2.4) - 0.055f : r * 12.92f; + g = g > 0.0031308f ? 1.055f * (float)System.Math.Pow(g, 1 / 2.4) - 0.055f : g * 12.92f; + bValue = bValue > 0.0031308f ? 1.055f * (float)System.Math.Pow(bValue, 1 / 2.4) - 0.055f : bValue * 12.92f; + + // r = (float)System.Math.Round(System.Math.Max(0, System.Math.Min(1, r))); + // g = (float)System.Math.Round(System.Math.Max(0, System.Math.Min(1, g))); + // bValue = (float)System.Math.Round(System.Math.Max(0, System.Math.Min(1, bValue))); + + return new Color(r, g, bValue); + + } + + + + public static Color Greyscale(float brightness, float alpha = 1) => new Color(brightness, brightness, brightness, alpha); + + public static Color SetAlpha(this Color color, float alpha) { color.a = alpha; return color; } + + public static Color MultiplyAlpha(this Color color, float k) { color.a *= k; return color; } + + + + + + #endregion + + #region Rects + + + public static Rect Resize(this Rect rect, float px) { rect.x += px; rect.y += px; rect.width -= px * 2; rect.height -= px * 2; return rect; } + + public static Rect SetPos(this Rect rect, Vector2 v) => rect.SetPos(v.x, v.y); + public static Rect SetPos(this Rect rect, float x, float y) { rect.x = x; rect.y = y; return rect; } + + public static Rect SetX(this Rect rect, float x) => rect.SetPos(x, rect.y); + public static Rect SetY(this Rect rect, float y) => rect.SetPos(rect.x, y); + public static Rect SetXMax(this Rect rect, float xMax) { rect.xMax = xMax; return rect; } + public static Rect SetYMax(this Rect rect, float yMax) { rect.yMax = yMax; return rect; } + + public static Rect SetMidPos(this Rect r, Vector2 v) => r.SetPos(v).MoveX(-r.width / 2).MoveY(-r.height / 2); + + public static Rect Move(this Rect rect, Vector2 v) { rect.position += v; return rect; } + public static Rect Move(this Rect rect, float x, float y) { rect.x += x; rect.y += y; return rect; } + public static Rect MoveX(this Rect rect, float px) { rect.x += px; return rect; } + public static Rect MoveY(this Rect rect, float px) { rect.y += px; return rect; } + + public static Rect SetWidth(this Rect rect, float f) { rect.width = f; return rect; } + public static Rect SetWidthFromMid(this Rect rect, float px) { rect.x += rect.width / 2; rect.width = px; rect.x -= rect.width / 2; return rect; } + public static Rect SetWidthFromRight(this Rect rect, float px) { rect.x += rect.width; rect.width = px; rect.x -= rect.width; return rect; } + + public static Rect SetHeight(this Rect rect, float f) { rect.height = f; return rect; } + public static Rect SetHeightFromMid(this Rect rect, float px) { rect.y += rect.height / 2; rect.height = px; rect.y -= rect.height / 2; return rect; } + public static Rect SetHeightFromBottom(this Rect rect, float px) { rect.y += rect.height; rect.height = px; rect.y -= rect.height; return rect; } + + public static Rect AddWidth(this Rect rect, float f) => rect.SetWidth(rect.width + f); + public static Rect AddWidthFromMid(this Rect rect, float f) => rect.SetWidthFromMid(rect.width + f); + public static Rect AddWidthFromRight(this Rect rect, float f) => rect.SetWidthFromRight(rect.width + f); + + public static Rect AddHeight(this Rect rect, float f) => rect.SetHeight(rect.height + f); + public static Rect AddHeightFromMid(this Rect rect, float f) => rect.SetHeightFromMid(rect.height + f); + public static Rect AddHeightFromBottom(this Rect rect, float f) => rect.SetHeightFromBottom(rect.height + f); + + public static Rect SetSize(this Rect rect, Vector2 v) => rect.SetWidth(v.x).SetHeight(v.y); + public static Rect SetSize(this Rect rect, float w, float h) => rect.SetWidth(w).SetHeight(h); + public static Rect SetSize(this Rect rect, float f) { rect.height = rect.width = f; return rect; } + + public static Rect SetSizeFromMid(this Rect r, Vector2 v) => r.Move(r.size / 2).SetSize(v).Move(-v / 2); + public static Rect SetSizeFromMid(this Rect r, float x, float y) => r.SetSizeFromMid(new Vector2(x, y)); + public static Rect SetSizeFromMid(this Rect r, float f) => r.SetSizeFromMid(new Vector2(f, f)); + + public static Rect AlignToPixelGrid(this Rect r) => GUIUtility.AlignRectToDevice(r); + + + + + + #endregion + + #region Vectors + + + public static Vector2 AddX(this Vector2 v, float f) => new Vector2(v.x + f, v.y + 0); + public static Vector2 AddY(this Vector2 v, float f) => new Vector2(v.x + 0, v.y + f); + + public static Vector3 AddX(this Vector3 v, float f) => new Vector3(v.x + f, v.y + 0, v.z + 0); + public static Vector3 AddY(this Vector3 v, float f) => new Vector3(v.x + 0, v.y + f, v.z + 0); + public static Vector3 AddZ(this Vector3 v, float f) => new Vector3(v.x + 0, v.y + 0, v.z + f); + + public static Vector2 xx(this Vector3 v) { return new Vector2(v.x, v.x); } + public static Vector2 xy(this Vector3 v) { return new Vector2(v.x, v.y); } + public static Vector2 xz(this Vector3 v) { return new Vector2(v.x, v.z); } + public static Vector2 yx(this Vector3 v) { return new Vector2(v.y, v.x); } + public static Vector2 yy(this Vector3 v) { return new Vector2(v.y, v.y); } + public static Vector2 yz(this Vector3 v) { return new Vector2(v.y, v.z); } + public static Vector2 zx(this Vector3 v) { return new Vector2(v.z, v.x); } + public static Vector2 zy(this Vector3 v) { return new Vector2(v.z, v.y); } + public static Vector2 zz(this Vector3 v) { return new Vector2(v.z, v.z); } + + + + + + #endregion + + #region Compute + + + [System.Serializable] + public class GaussianKernel + { + public GaussianKernel(bool isEvenSize = false, int radius = 7, float sharpness = .5f) + { + this.isEvenSize = isEvenSize; + this.radius = radius; + this.sharpness = sharpness; + } + + public bool isEvenSize = false; + + public int radius = 7; + public float sharpness = .5f; + + public int size => radius * 2 + (isEvenSize ? 0 : 1); + public float sigma => 1 - Mathf.Pow(sharpness, .1f) * .99999f; + + public float[,] Array2d() + { + float[,] kr = new float[size, size]; + + if (size == 1) { kr[0, 0] = 1; return kr; } + + var a = -2f * radius * radius / Mathf.Log(sigma); + var sum = 0f; + + for (int y = 0; y < size; y++) + for (int x = 0; x < size; x++) + { + var rX = size % 2 == 1 ? (x - radius) : (x - radius) + .5f; + var rY = size % 2 == 1 ? (y - radius) : (y - radius) + .5f; + var dist = Mathf.Sqrt(rX * rX + rY * rY); + kr[x, y] = Mathf.Exp(-dist * dist / a); + sum += kr[x, y]; + } + + for (int y = 0; y < size; y++) + for (int x = 0; x < size; x++) + kr[x, y] /= sum; + + return kr; + } + public float[] ArrayFlat() + { + var gk = Array2d(); + float[] flat = new float[size * size]; + + for (int i = 0; i < size; i++) + for (int j = 0; j < size; j++) + flat[(i * size + j)] = gk[i, j]; + + return flat; + } + } + + + + + + #endregion + + #region Objects + + + public static Object[] FindObjects(Type type) + { +#if UNITY_2023_1_OR_NEWER + return Object.FindObjectsByType(type, FindObjectsSortMode.None); +#else + return Object.FindObjectsOfType(type); +#endif + } + public static T[] FindObjects() where T : Object + { +#if UNITY_2023_1_OR_NEWER + return Object.FindObjectsByType(FindObjectsSortMode.None); +#else + return Object.FindObjectsOfType(); +#endif + } + + public static void Destroy(this Object r) + { + if (Application.isPlaying) + Object.Destroy(r); + else + Object.DestroyImmediate(r); + + } + + public static void DestroyImmediate(this Object o) => Object.DestroyImmediate(o); + + + + + + #endregion + + #region GlobalID + +#if UNITY_EDITOR + + [System.Serializable] + public struct GlobalID : System.IEquatable + { + public Object GetObject() => GlobalObjectId.GlobalObjectIdentifierToObjectSlow(globalObjectId); + public int GetObjectInstanceId() => _GlobalObjectId_GlobalObjectIdentifierToInstanceIDSlow(globalObjectId); + + + public string guid => globalObjectId.assetGUID.ToString(); + public ulong fileId => globalObjectId.targetObjectId; + + public bool isNull => globalObjectId.identifierType == 0; + public bool isAsset => globalObjectId.identifierType == 1; + public bool isSceneObject => globalObjectId.identifierType == 2; + + public GlobalObjectId globalObjectId => _globalObjectId.Equals(default) && globalObjectIdString != null && GlobalObjectId.TryParse(globalObjectIdString, out var r) ? _globalObjectId = r : _globalObjectId; + public GlobalObjectId _globalObjectId; + + public GlobalID(Object o) => globalObjectIdString = (_globalObjectId = GlobalObjectId.GetGlobalObjectIdSlow(o)).ToString(); + public GlobalID(string s) => globalObjectIdString = GlobalObjectId.TryParse(s, out _globalObjectId) ? s : s; + + public string globalObjectIdString; + + + + public bool Equals(GlobalID other) => this.globalObjectIdString.Equals(other.globalObjectIdString); + + public static bool operator ==(GlobalID a, GlobalID b) => a.Equals(b); + public static bool operator !=(GlobalID a, GlobalID b) => !a.Equals(b); + + public override bool Equals(object other) => other is GlobalID otherglobalID && this.Equals(otherglobalID); + public override int GetHashCode() => globalObjectIdString == null ? 0 : globalObjectIdString.GetHashCode(); + + + public override string ToString() => globalObjectIdString; + + } + + public static GlobalID GetGlobalID(this Object o) => new GlobalID(o); + + public static int[] GetObjectInstanceIds(this IEnumerable globalIDs) + { + var goids = globalIDs.Select(r => r.globalObjectId).ToArray(); + + var iids = new int[goids.Length]; + + _GlobalObjectId_GlobalObjectIdentifiersToInstanceIDsSlow(goids, iids); + + return iids; + } + + +#endif + + + + + #endregion + + #region Paths + + + public static string GetParentPath(this string path) => path.Substring(0, path.LastIndexOf('/')); + public static bool HasParentPath(this string path) => path.Contains('/') && path.GetParentPath() != ""; + + public static string ToGlobalPath(this string localPath) => Application.dataPath + "/" + localPath.Substring(0, localPath.Length - 1); + public static string ToLocalPath(this string globalPath) => "Assets" + globalPath.Remove(Application.dataPath); + + + + public static string CombinePath(this string p, string p2) => Path.Combine(p, p2); + + public static bool IsSubpathOf(this string path, string of) => path.StartsWith(of + "/") || of == ""; + + public static string GetDirectory(this string pathOrDirectory) + { + var directory = pathOrDirectory.Contains('.') ? pathOrDirectory.Substring(0, pathOrDirectory.LastIndexOf('/')) : pathOrDirectory; + + if (directory.Contains('.')) + directory = directory.Substring(0, directory.LastIndexOf('/')); + + return directory; + + } + + public static bool DirectoryExists(this string pathOrDirectory) => Directory.Exists(pathOrDirectory.GetDirectory()); + + public static string EnsureDirExists(this string pathOrDirectory) // todo to EnsureDirectoryExists + { + var directory = pathOrDirectory.GetDirectory(); + + if (directory.HasParentPath() && !Directory.Exists(directory.GetParentPath())) + EnsureDirExists(directory.GetParentPath()); + + if (!Directory.Exists(directory)) + Directory.CreateDirectory(directory); + + return pathOrDirectory; + + } + + + + public static string ClearDir(this string dir) + { + if (!Directory.Exists(dir)) return dir; + + var diri = new DirectoryInfo(dir); + foreach (var r in diri.EnumerateFiles()) r.Delete(); + foreach (var r in diri.EnumerateDirectories()) r.Delete(true); + + return dir; + } + + + + + + +#if UNITY_EDITOR + + public static string EnsurePathIsUnique(this string path) + { + if (!path.DirectoryExists()) return path; + + var s = AssetDatabase.GenerateUniqueAssetPath(path); // returns empty if parent dir doesnt exist + + return s == "" ? path : s; + + } + + public static void EnsureDirExistsAndRevealInFinder(string dir) + { + EnsureDirExists(dir); + UnityEditor.EditorUtility.OpenWithDefaultApp(dir); + } + +#endif + + + + #endregion + + #region AssetDatabase + +#if UNITY_EDITOR + + public static AssetImporter GetImporter(this Object t) => AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(t)); + + public static string ToPath(this string guid) => AssetDatabase.GUIDToAssetPath(guid); // returns empty string if not found + public static List ToPaths(this IEnumerable guids) => guids.Select(r => r.ToPath()).ToList(); + + public static string GetFilename(this string path, bool withExtension = false) => withExtension ? Path.GetFileName(path) : Path.GetFileNameWithoutExtension(path); // prev GetName + public static string GetExtension(this string path) => Path.GetExtension(path); + + + public static string ToGuid(this string pathInProject) => AssetDatabase.AssetPathToGUID(pathInProject); + public static List ToGuids(this IEnumerable pathsInProject) => pathsInProject.Select(r => r.ToGuid()).ToList(); + + public static string GetPath(this Object o) => AssetDatabase.GetAssetPath(o); + public static string GetGuid(this Object o) => AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(o)); + + public static string GetScriptPath(string scriptName) => AssetDatabase.FindAssets("t: script " + scriptName, null).FirstOrDefault()?.ToPath() ?? "scirpt not found"; + + + public static bool IsValidGuid(this string guid) => AssetDatabase.AssetPathToGUID(AssetDatabase.GUIDToAssetPath(guid), AssetPathToGUIDOptions.OnlyExistingAssets) != ""; + + + + // toremove + public static Object LoadGuid(this string guid) => AssetDatabase.LoadAssetAtPath(guid.ToPath(), typeof(Object)); + public static T LoadGuid(this string guid) where T : Object => AssetDatabase.LoadAssetAtPath(guid.ToPath()); + + + + + public static List FindAllAssetsOfType_guids(Type type) => AssetDatabase.FindAssets("t:" + type.Name).ToList(); + public static List FindAllAssetsOfType_guids(Type type, string path) => AssetDatabase.FindAssets("t:" + type.Name, new[] { path }).ToList(); + public static List FindAllAssetsOfType() where T : Object => FindAllAssetsOfType_guids(typeof(T)).Select(r => (T)r.LoadGuid()).ToList(); + public static List FindAllAssetsOfType(string path) where T : Object => FindAllAssetsOfType_guids(typeof(T), path).Select(r => (T)r.LoadGuid()).ToList(); + + public static T Reimport(this T t) where T : Object { AssetDatabase.ImportAsset(t.GetPath(), ImportAssetOptions.ForceUpdate); return t; } + +#endif + + + + + + #endregion + + #region Serialization + + [System.Serializable] + public class SerializableDictionary : Dictionary, ISerializationCallbackReceiver + { + [SerializeField] List keys = new List(); + [SerializeField] List values = new List(); + + public void OnBeforeSerialize() + { + keys.Clear(); + values.Clear(); + + foreach (KeyValuePair kvp in this) + { + keys.Add(kvp.Key); + values.Add(kvp.Value); + } + + } + public void OnAfterDeserialize() + { + this.Clear(); + + for (int i = 0; i < keys.Count; i++) + this[keys[i]] = values[i]; + + } + + } + + + #endregion + + #region Editor + +#if UNITY_EDITOR + + public static void ToggleDefineDisabledInScript(Type scriptType) + { + var path = GetScriptPath(scriptType.Name); + + var lines = File.ReadAllLines(path); + if (lines.First().StartsWith("#define DISABLED")) + File.WriteAllLines(path, lines.Skip(1)); + else + File.WriteAllLines(path, lines.Prepend("#define DISABLED // this line was added by VUtils.ToggleDefineDisabledInScript")); + + AssetDatabase.ImportAsset(path); + } + public static bool ScriptHasDefineDisabled(Type scriptType) => File.ReadLines(GetScriptPath(scriptType.Name)).First().StartsWith("#define DISABLED"); + public static void SetDefineDisabledInScript(Type scriptType, bool defineDisabled) + { + if (ScriptHasDefineDisabled(scriptType) != defineDisabled) + ToggleDefineDisabledInScript(scriptType); + + } + + public static int GetProjectId() => Application.dataPath.GetHashCode(); + + public static void PingObject(Object o, bool select = false, bool focusProjectWindow = true) + { + if (select) + { + Selection.activeObject = null; + Selection.activeObject = o; + } + if (focusProjectWindow) EditorUtility.FocusProjectWindow(); + EditorGUIUtility.PingObject(o); + + } + public static void PingObject(string guid, bool select = false, bool focusProjectWindow = true) => PingObject(AssetDatabase.LoadAssetAtPath(guid.ToPath())); + + + public static void OpenFolder(string path) + { + var folder = AssetDatabase.LoadAssetAtPath(path, typeof(Object)); + + var t = typeof(Editor).Assembly.GetType("UnityEditor.ProjectBrowser"); + var w = (EditorWindow)t.GetField("s_LastInteractedProjectBrowser").GetValue(null); + + var m_ListAreaState = t.GetField("m_ListAreaState", maxBindingFlags).GetValue(w); + + m_ListAreaState.GetType().GetField("m_SelectedInstanceIDs").SetValue(m_ListAreaState, new List { folder.GetInstanceID() }); + + t.GetMethod("OpenSelectedFolders", maxBindingFlags).Invoke(null, null); + + } + + public static void Dirty(this Object o) => UnityEditor.EditorUtility.SetDirty(o); + public static void Save(this Object o) => AssetDatabase.SaveAssetIfDirty(o); + public static void RecordUndo(this Object o) => Undo.RecordObject(o, ""); + + + public static EditorWindow OpenObjectPicker(Object obj = null, bool allowSceneObjects = false, string searchFilter = "", int controlID = 0) where T : Object + { + EditorGUIUtility.ShowObjectPicker(obj, allowSceneObjects, searchFilter, controlID); + + return Resources.FindObjectsOfTypeAll(typeof(Editor).Assembly.GetType("UnityEditor.ObjectSelector")).FirstOrDefault() as EditorWindow; + + } + public static EditorWindow OpenColorPicker(System.Action colorChangedCallback, Color color, bool showAlpha = true, bool hdr = false) + { + typeof(Editor).Assembly.GetType("UnityEditor.ColorPicker").InvokeMethod("Show", colorChangedCallback, color, showAlpha, hdr); + + return typeof(Editor).Assembly.GetType("UnityEditor.ColorPicker").GetPropertyValue("instance"); + + } + + public static void MoveTo(this EditorWindow window, Vector2 position, bool ensureFitsOnScreen = true) + { + if (!ensureFitsOnScreen) { window.position = window.position.SetPos(position); return; } + + var windowRect = window.position; + var unityWindowRect = EditorGUIUtility.GetMainWindowPosition(); + + position.x = position.x.Max(unityWindowRect.position.x); + position.y = position.y.Max(unityWindowRect.position.y); + + position.x = position.x.Min(unityWindowRect.xMax - windowRect.width); + position.y = position.y.Min(unityWindowRect.yMax - windowRect.height); + + window.position = windowRect.SetPos(position); + + } + + + + public static void RemoveEditorErrors() => removeEditorErrorsMethod.Invoke(null, new object[] { 1 }); + static MethodInfo removeEditorErrorsMethod = System.AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(r => r.GetName().ToString().Contains("UnityEditor.CoreModule")).GetTypes().First(r => r.Name.Contains("LogEntry")).GetMethod("RemoveLogEntriesByMode", BindingFlags.Static | BindingFlags.NonPublic); + + + + public static class EditorPrefsCached + { + public static bool GetBool(string key, bool defaultValue = false) + { + if (bools_byKey.ContainsKey(key)) + return bools_byKey[key]; + else + return bools_byKey[key] = EditorPrefs.GetBool(key, defaultValue); + + } + public static float GetFloat(string key, float defaultValue = 0) + { + if (floats_byKey.ContainsKey(key)) + return floats_byKey[key]; + else + return floats_byKey[key] = EditorPrefs.GetFloat(key, defaultValue); + + } + public static int GetInt(string key, int defaultValue = 0) + { + if (ints_byKey.ContainsKey(key)) + return ints_byKey[key]; + else + return ints_byKey[key] = EditorPrefs.GetInt(key, defaultValue); + + } + public static string GetString(string key, string defaultValue = "") + { + if (strings_byKey.ContainsKey(key)) + return strings_byKey[key]; + else + return strings_byKey[key] = EditorPrefs.GetString(key, defaultValue); + + } + + + public static void SetBool(string key, bool value) + { + bools_byKey[key] = value; + + EditorPrefs.SetBool(key, value); + + } + public static void SetFloat(string key, float value) + { + floats_byKey[key] = value; + + EditorPrefs.SetFloat(key, value); + + } + public static void SetInt(string key, int value) + { + ints_byKey[key] = value; + + EditorPrefs.SetInt(key, value); + + } + public static void SetString(string key, string value) + { + strings_byKey[key] = value; + + EditorPrefs.SetString(key, value); + + } + + + static Dictionary bools_byKey = new(); + static Dictionary floats_byKey = new(); + static Dictionary ints_byKey = new(); + static Dictionary strings_byKey = new(); + + } + + +#endif + + + + + + #endregion + + #region Instance/Entity ID mess + + + static int _GlobalObjectId_GlobalObjectIdentifierToInstanceIDSlow(GlobalObjectId id) + { +#if UNITY_6000_3_OR_NEWER + return GlobalObjectId.GlobalObjectIdentifierToEntityIdSlow(id); +#else + return GlobalObjectId.GlobalObjectIdentifierToInstanceIDSlow(id); +#endif + + } + + static void _GlobalObjectId_GlobalObjectIdentifiersToInstanceIDsSlow(GlobalObjectId[] identifiers, int[] outputInstanceIDs) + { +#if UNITY_6000_3_OR_NEWER + + var outputEntityIds = new EntityId[outputInstanceIDs.Length]; + + GlobalObjectId.GlobalObjectIdentifiersToEntityIdsSlow(identifiers, outputEntityIds); + + for (int i = 0; i < outputEntityIds.Length; i++) + outputInstanceIDs[i] = (int)outputEntityIds[i]; + +#else + + GlobalObjectId.GlobalObjectIdentifiersToInstanceIDsSlow(identifiers, outputInstanceIDs); + +#endif + + } + + static void _GlobalObjectId_GetGlobalObjectIdsSlow(int[] ids, GlobalObjectId[] outputIdentifiers) + { +#if UNITY_6000_3_OR_NEWER + GlobalObjectId.GetGlobalObjectIdsSlow(ids.Select(r => (EntityId)r).ToArray(), outputIdentifiers); +#else + GlobalObjectId.GetGlobalObjectIdsSlow(ids, outputIdentifiers); +#endif + + } + + + + public static Object _EditorUtility_InstanceIDToObject(int iid) + { +#if UNITY_6000_3_OR_NEWER + return EditorUtility.EntityIdToObject(iid); +#else + return EditorUtility.InstanceIDToObject(iid); +#endif + } + + public static string _AssetDatabase_GetAssetPath(int instanceID) + { +#if UNITY_6000_3_OR_NEWER + return AssetDatabase.GetAssetPath((EntityId)instanceID); +#else + return AssetDatabase.GetAssetPath(instanceID); +#endif + } + + public static int[] _Selection_instanceIDs + { + get + { +#if UNITY_6000_3_OR_NEWER + return Selection.entityIds.Select(r => (int)r).ToArray(); +#else + return Selection.instanceIDs; +#endif + } + } + + + #endregion + + } + + public static partial class VGUI + { + #region Colors + + public static class GUIColors + { + public static Color windowBackground => isDarkTheme ? Greyscale(.22f) : Greyscale(.78f); // prev backgroundCol + public static Color pressedButtonBackground => isDarkTheme ? new Color(.48f, .76f, 1f, 1f) * 1.4f : new Color(.48f, .7f, 1f, 1f) * 1.2f; // prev pressedButtonCol + public static Color greyedOutTint => Greyscale(.7f); + public static Color selectedBackground => isDarkTheme ? new Color(.17f, .365f, .535f) : new Color(.2f, .375f, .555f) * 1.2f; + } + + + #endregion + + #region Shortcuts + + public static Rect lastRect => GUILayoutUtility.GetLastRect(); + + public static bool isDarkTheme => EditorGUIUtility.isProSkin; + + public static float GetLabelWidth(this string s) => GUI.skin.label.CalcSize(new GUIContent(s)).x; + public static float GetLabelWidth(this string s, int fotSize) + { + SetLabelFontSize(fotSize); + + var r = s.GetLabelWidth(); + + ResetLabelStyle(); + + return r; + + } + public static float GetLabelWidth(this string s, bool isBold) + { + if (isBold) + SetLabelBold(); + + var r = s.GetLabelWidth(); + + if (isBold) + ResetLabelStyle(); + + return r; + + } + + public static void SetGUIEnabled(bool enabled) { _prevGuiEnabled = GUI.enabled; GUI.enabled = enabled; } + public static void ResetGUIEnabled() => GUI.enabled = _prevGuiEnabled; + static bool _prevGuiEnabled = true; + + public static void SetLabelFontSize(int size) => GUI.skin.label.fontSize = size; + public static void SetLabelBold() => GUI.skin.label.fontStyle = FontStyle.Bold; + public static void SetLabelAlignmentCenter() => GUI.skin.label.alignment = TextAnchor.MiddleCenter; + public static void ResetLabelStyle() + { + GUI.skin.label.fontSize = 0; + GUI.skin.label.fontStyle = FontStyle.Normal; + GUI.skin.label.alignment = TextAnchor.MiddleLeft; + } + + + public static void SetGUIColor(Color c) + { + if (!_guiColorModified) + _defaultGuiColor = GUI.color; + + _guiColorModified = true; + + GUI.color = _defaultGuiColor * c; + + } + public static void ResetGUIColor() + { + GUI.color = _guiColorModified ? _defaultGuiColor : Color.white; + + _guiColorModified = false; + + } + static bool _guiColorModified; + static Color _defaultGuiColor; + + + + #endregion + + #region Events + + + public class WrappedEvent + { + public Event e; + + public bool isRepaint => e.type == EventType.Repaint; + public bool isLayout => e.type == EventType.Layout; + public bool isUsed => e.type == EventType.Used; + public bool isMouseLeaveWindow => e.type == EventType.MouseLeaveWindow; + public bool isMouseEnterWindow => e.type == EventType.MouseEnterWindow; + public bool isContextClick => e.type == EventType.ContextClick; + + public bool isKeyDown => e.type == EventType.KeyDown; + public bool isKeyUp => e.type == EventType.KeyUp; + public KeyCode keyCode => e.keyCode; + public char characted => e.character; + + public bool isExecuteCommand => e.type == EventType.ExecuteCommand; + public string commandName => e.commandName; + + public bool isMouse => e.isMouse; + public bool isMouseDown => e.type == EventType.MouseDown; + public bool isMouseUp => e.type == EventType.MouseUp; + public bool isMouseDrag => e.type == EventType.MouseDrag; + public bool isMouseMove => e.type == EventType.MouseMove; + public bool isScroll => e.type == EventType.ScrollWheel; + public int mouseButton => e.button; + public int clickCount => e.clickCount; + public Vector2 mousePosition => e.mousePosition; + public Vector2 mousePosition_screenSpace => GUIUtility.GUIToScreenPoint(e.mousePosition); + public Vector2 mouseDelta => e.delta; + + public bool isDragUpdate => e.type == EventType.DragUpdated; + public bool isDragPerform => e.type == EventType.DragPerform; + public bool isDragExit => e.type == EventType.DragExited; + + public EventModifiers modifiers => e.modifiers; + public bool holdingAnyModifierKey => modifiers != EventModifiers.None; + + public bool holdingAlt => e.alt; + public bool holdingShift => e.shift; + public bool holdingCtrl => e.control; + public bool holdingCmd => e.command; + public bool holdingCmdOrCtrl => e.command || e.control; + + public bool holdingAltOnly => e.modifiers == EventModifiers.Alt; // in some sessions FunctionKey is always pressed? + public bool holdingShiftOnly => e.modifiers == EventModifiers.Shift; // in some sessions FunctionKey is always pressed? + public bool holdingCtrlOnly => e.modifiers == EventModifiers.Control; + public bool holdingCmdOnly => e.modifiers == EventModifiers.Command; + public bool holdingCmdOrCtrlOnly => (e.modifiers == EventModifiers.Command || e.modifiers == EventModifiers.Control); + + public EventType type => e.type; + + public void Use() => e?.Use(); + + + public WrappedEvent(Event e) => this.e = e; + + public override string ToString() => e.ToString(); + + } + + public static WrappedEvent Wrap(this Event e) => new(e); + + public static WrappedEvent curEvent => _curEvent ??= typeof(Event).GetFieldValue("s_Current").Wrap(); + static WrappedEvent _curEvent; + + + + + + #endregion + + #region Layout + + + + public static void BeginPanel(string title, float height, System.Action onClose = null, System.Action onApply = null) + { + + void bg() + { + GUI.enabled = false; + SetGUIColor(Greyscale(.75f)); + var r = ExpandWidthLabelRect(0).SetHeight(height); + GUI.Button(r, ""); + GUI.Button(r, ""); + GUI.Button(r, ""); + GUI.Button(r, ""); + GUI.Button(r, ""); + GUI.Button(r, ""); + GUI.Button(r, ""); + GUI.Button(r, ""); + ResetGUIColor(); + GUI.enabled = true; + } + + void layout() + { + + GUILayout.BeginHorizontal(); + Space(7); + EditorGUIUtility.labelWidth -= 7; + GUILayout.BeginVertical(); + + } + + void title_() + { + Space(5); + EditorGUI.PrefixLabel(ExpandWidthLabelRect(), new GUIContent(title)); + // GUI.skin.label.fontStyle = FontStyle.Bold; + // GUILayout.Label(title); + + } + + void buttons() + { + var rClose = lastRect.SetWidthFromRight(16).MoveX(1).MoveY(-1); + + // if() + GUI.color = Greyscale(rClose.IsHovered() ? .9f : .6f); + GUI.Label(rClose, EditorGUIUtility.IconContent("CrossIcon")); + + GUI.color = Color.clear; + if (GUI.Button(rClose, "")) + onClose?.Invoke(); + + + var rApply = rClose.MoveX(-19).Resize(-1); + + GUI.color = Greyscale(rApply.IsHovered() ? .9f : .6f); + GUI.Label(rApply, EditorGUIUtility.IconContent("check")); + + GUI.color = Color.clear; + if (GUI.Button(rApply, "")) + onApply?.Invoke(); + + + GUI.color = Color.white; + + } + + + + bg(); + layout(); + title_(); + buttons(); + + Space(5); + + } + + public static void EndPanel() + { + GUILayout.EndVertical(); + Space(7); + EditorGUIUtility.labelWidth = 0; + GUILayout.EndHorizontal(); + } + + + + public static void BeginIndent(float f) + { + GUILayout.BeginHorizontal(); + GUILayout.Space(f); + GUILayout.BeginVertical(); + + _indentLabelWidthStack.Push(EditorGUIUtility.labelWidth); + + EditorGUIUtility.labelWidth -= f; + } + + public static void EndIndent(float f = 0) + { + GUILayout.EndVertical(); + GUILayout.Space(f); + GUILayout.EndHorizontal(); + + EditorGUIUtility.labelWidth = _indentLabelWidthStack.Pop(); + } + static Stack _indentLabelWidthStack = new Stack(); + + + + + #endregion + + #region Drawing + + public static Rect Draw(this Rect r) { EditorGUI.DrawRect(r, Color.black); return r; } + public static Rect Draw(this Rect r, Color c) { EditorGUI.DrawRect(r, c); return r; } + + + + public static Rect DrawWithRoundedCorners(this Rect rect, Color color, int cornerRadius) + { + if (!curEvent.isRepaint) return rect; + + cornerRadius = cornerRadius.Min((rect.height / 2).FloorToInt()).Min((rect.width / 2).FloorToInt()); + + GUIStyle style; + + void getStyle() + { + if (_roundedStylesByCornerRadius.TryGetValue(cornerRadius, out style)) return; + + var pixelsPerPoint = 2; + + var res = cornerRadius * 2 * pixelsPerPoint; + var pixels = new Color[res * res]; + + var white = Greyscale(1, 1); + var clear = Greyscale(1, 0); + var halfRes = res / 2; + + for (int x = 0; x < res; x++) + for (int y = 0; y < res; y++) + { + var sqrMagnitude = (new Vector2(x - halfRes + .5f, y - halfRes + .5f)).sqrMagnitude; + pixels[x + y * res] = sqrMagnitude <= halfRes * halfRes ? white : clear; + } + + var texture = new Texture2D(res, res); + texture.SetPropertyValue("pixelsPerPoint", pixelsPerPoint); + texture.hideFlags = HideFlags.DontSave; + texture.SetPixels(pixels); + texture.Apply(); + + + + style = new GUIStyle(); + style.normal.background = texture; + style.alignment = TextAnchor.MiddleCenter; + style.border = new RectOffset(cornerRadius, cornerRadius, cornerRadius, cornerRadius); + + + _roundedStylesByCornerRadius[cornerRadius] = style; + + } + void draw() + { + SetGUIColor(color); + + style.Draw(rect, false, false, false, false); + + ResetGUIColor(); + + } + + getStyle(); + draw(); + + return rect; + + } + public static Rect DrawWithRoundedCorners(this Rect rect, Color color, float cornerRadius) => rect.DrawWithRoundedCorners(color, cornerRadius.RoundToInt()); + static Dictionary _roundedStylesByCornerRadius = new Dictionary(); + + + public static Rect DrawBlurred(this Rect rect, Color color, int blurRadius) + { + if (!curEvent.isRepaint) return rect; + + var pixelsPerPoint = .5f; + // var pixelsPerPoint = 1f; + + var blurRadiusScaled = (blurRadius * pixelsPerPoint).RoundToInt().Max(1).Min(123); + + var croppedRectWidth = (rect.width * pixelsPerPoint).RoundToInt().Min(blurRadiusScaled * 2); + var croppedRectHeight = (rect.height * pixelsPerPoint).RoundToInt().Min(blurRadiusScaled * 2); + + var textureWidth = croppedRectWidth + blurRadiusScaled * 2; + var textureHeight = croppedRectHeight + blurRadiusScaled * 2; + + GUIStyle style; + + void getStyle() + { + if (_blurredStylesByTextureSize.TryGetValue((textureWidth, textureHeight), out style)) return; + + // VDebug.LogStart(blurRadius + ""); + + var pixels = new Color[textureWidth * textureHeight]; + var kernel = new GaussianKernel(false, blurRadiusScaled).Array2d(); + + for (int x = 0; x < textureWidth; x++) + for (int y = 0; y < textureHeight; y++) + { + var sum = 0f; + + for (int xSample = (x - blurRadiusScaled).Max(blurRadiusScaled); xSample <= (x + blurRadiusScaled).Min(textureWidth - 1 - blurRadiusScaled); xSample++) + for (int ySample = (y - blurRadiusScaled).Max(blurRadiusScaled); ySample <= (y + blurRadiusScaled).Min(textureHeight - 1 - blurRadiusScaled); ySample++) + sum += kernel[blurRadiusScaled + xSample - x, blurRadiusScaled + ySample - y]; + + pixels[x + y * textureWidth] = Greyscale(1, sum); + + } + + var texture = new Texture2D(textureWidth, textureHeight); + texture.SetPropertyValue("pixelsPerPoint", pixelsPerPoint); + texture.hideFlags = HideFlags.DontSave; + texture.SetPixels(pixels); + texture.Apply(); + + + style = new GUIStyle(); + style.normal.background = texture; + style.alignment = TextAnchor.MiddleCenter; + + var borderX = ((textureWidth / 2f - 1) / pixelsPerPoint).FloorToInt(); + var borderY = ((textureHeight / 2f - 1) / pixelsPerPoint).FloorToInt(); + style.border = new RectOffset(borderX, borderX, borderY, borderY); + + _blurredStylesByTextureSize[(textureWidth, textureHeight)] = style; + + // VDebug.LogFinish(); + + } + void draw() + { + SetGUIColor(color); + + style.Draw(rect.SetSizeFromMid(rect.width + blurRadius * 2, rect.height + blurRadius * 2), false, false, false, false); + + ResetGUIColor(); + + } + + getStyle(); + draw(); + + return rect; + + } + public static Rect DrawBlurred(this Rect rect, Color color, float blurRadius) => rect.DrawBlurred(color, blurRadius.RoundToInt()); + static Dictionary<(int, int), GUIStyle> _blurredStylesByTextureSize = new Dictionary<(int, int), GUIStyle>(); + + + static void DrawCurtain(this Rect rect, Color color, int dir) + { + void genTextures() + { + if (_gradientTextures != null) return; + + _gradientTextures = new Texture2D[4]; + + // var pixels = Enumerable.Range(0, 256).Select(r => Greyscale(1, r / 255f)); + var pixels = Enumerable.Range(0, 256).Select(r => Greyscale(1, (r / 255f).Smoothstep())); + + var up = new Texture2D(1, 256); + up.SetPixels(pixels.Reverse().ToArray()); + up.Apply(); + up.hideFlags = HideFlags.DontSave; + up.wrapMode = TextureWrapMode.Clamp; + _gradientTextures[0] = up; + + var down = new Texture2D(1, 256); + down.SetPixels(pixels.ToArray()); + down.Apply(); + down.hideFlags = HideFlags.DontSave; + down.wrapMode = TextureWrapMode.Clamp; + _gradientTextures[1] = down; + + var left = new Texture2D(256, 1); + left.SetPixels(pixels.ToArray()); + left.Apply(); + left.hideFlags = HideFlags.DontSave; + left.wrapMode = TextureWrapMode.Clamp; + _gradientTextures[2] = left; + + var right = new Texture2D(256, 1); + right.SetPixels(pixels.Reverse().ToArray()); + right.Apply(); + right.hideFlags = HideFlags.DontSave; + right.wrapMode = TextureWrapMode.Clamp; + _gradientTextures[3] = right; + + } + void draw() + { + SetGUIColor(color); + + GUI.DrawTexture(rect, _gradientTextures[dir]); + + ResetGUIColor(); + + } + + genTextures(); + draw(); + + } + public static void DrawCurtainUp(this Rect rect, Color color) => rect.DrawCurtain(color, 0); + public static void DrawCurtainDown(this Rect rect, Color color) => rect.DrawCurtain(color, 1); + public static void DrawCurtainLeft(this Rect rect, Color color) => rect.DrawCurtain(color, 2); + public static void DrawCurtainRight(this Rect rect, Color color) => rect.DrawCurtain(color, 3); + static Texture2D[] _gradientTextures; + + + + public static bool IsHovered(this Rect r) => r.Contains(curEvent.mousePosition); + + #endregion + + #region Spacing + + public static void Space(float px = 6) => GUILayout.Space(px); + + public static void Divider(float space = 15, float yOffset = 0) + { + GUILayout.Label("", GUILayout.Height(space), GUILayout.ExpandWidth(true)); + lastRect.SetHeightFromMid(1).SetWidthFromMid(lastRect.width - 16).MoveY(yOffset).Draw(isDarkTheme ? Color.white * .42f : Color.white * .72f); + } + + public static Rect ExpandSpace() { GUILayout.Label("", GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true)); return lastRect; } + + public static Rect ExpandWidthLabelRect() { GUILayout.Label(""/* , GUILayout.Height(0) */, GUILayout.ExpandWidth(true)); return lastRect; } + public static Rect ExpandWidthLabelRect(float height) { GUILayout.Label("", GUILayout.Height(height), GUILayout.ExpandWidth(true)); return lastRect; } + + + #endregion + + #region Icons + + public static class EditorIcons + { + public static Texture2D GetIcon(string iconNameOrPath) + { + if (icons_byName.TryGetValue(iconNameOrPath, out var cachedResult)) return cachedResult; + + var icon = typeof(EditorGUIUtility).InvokeMethod("LoadIcon", iconNameOrPath) as Texture2D; + + return icons_byName[iconNameOrPath] = icon; + + } + + static Dictionary icons_byName = new Dictionary(); + } + + + + + // toremove: + + public static void DrawIcon(Rect rect, string icon, Color? col = null) + { + if (icon == "") return; + Texture2D tex = (Texture2D)EditorGUIUtility.FindTexture(icon); + if (!tex) tex = (Texture2D)EditorGUIUtility.Load(icon); + if (!tex) tex = (Texture2D)Resources.Load(icon); + DrawIcon(rect, tex, col); + } + public static void DrawIcon(Rect rect, string icon, bool greyedOut) => DrawIcon(rect, icon, greyedOut ? GUIColors.greyedOutTint : (Color?)null); + + public static void DrawIcon(Rect rect, Texture2D tex, Color? col = null) + { + var color = Color.white; + if (col != null) color = col.GetValueOrDefault(); + + GUI.DrawTexture(rect, tex, ScaleMode.ScaleToFit, true, 0, color, 0, 0); + } + public static void DrawIcon(Rect rect, Texture2D tex, bool greyedOut) => DrawIcon(rect, tex, greyedOut ? GUIColors.greyedOutTint : (Color?)null); + + public static void DrawIconForGuid(Rect rect, string guid, bool greyedOut = false) + { + if (!guid.IsValidGuid()) return; + + var type = AssetDatabase.GetMainAssetTypeAtPath(guid.ToPath()); + + if (AssetDatabase.IsValidFolder(guid.ToPath())) + DrawIcon(rect, "Folder Icon", greyedOut); + + else if (guid.ToPath().GetExtension() == ".cs") + DrawIcon(rect, "cs Script Icon", greyedOut); + + else DrawIcon(rect, AssetPreview.GetMiniTypeThumbnail(type), greyedOut); + } + + // public static void DrawIcon(string icon, bool greyedOut = false, bool pressed = false) => DrawIcon(lastRect, icon, greyedOut, pressed); + // public static void DrawIcon(Rect rect, string icon, bool greyedOut = false, bool pressed = false) + // { + // if (icon == "") return; + // Texture2D tex = (Texture2D)EditorGUIUtility.FindTexture(icon); + // if (!tex) tex = (Texture2D)EditorGUIUtility.Load(icon); + // if (!tex) tex = (Texture2D)Resources.Load(icon); + // DrawIcon(rect, tex, greyedOut, pressed); + // } + // public static void DrawIcon(Rect rect, GUIContent tex, bool greyedOut = false) //some icons get fucked up if drawn differently + // { + // var r = GUI.enabled; + // if (greyedOut) GUI.enabled = false; + // var s = new GUIStyle(); + // GUI.Label(rect.Resize(0), tex, s); + // GUI.enabled = r; + // } + // public static void DrawIcon(Rect rect, Texture2D tex, bool greyedOut = false, bool pressed = false) + // { + // var col = Color.white; + // if (greyedOut) col *= greyedOutColor; + // if (pressed) col *= pressedCol; + // GUI.DrawTexture(rect, tex, ScaleMode.ScaleToFit, true, 0, col, 0, 0); + // } + // public static void DrawIcon(Rect rect, string icon, Color col) => DrawIcon(lastRect, EditorGUIUtility.FindTexture(icon), col); + // public static void DrawIcon(Rect rect, Texture2D tex, Color col) => GUI.DrawTexture(rect, tex, ScaleMode.ScaleToFit, true, 0, col, 0, 0); + + + + #endregion + + #region Other + + public static void MarkInteractive(this Rect rect) + { + if (!curEvent.isRepaint) return; + + var unclippedRect = (Rect)_mi_GUIClip_UnclipToWindow.Invoke(null, new object[] { rect }); + + var curGuiView = _pi_GUIView_current.GetValue(null); + + _mi_GUIView_MarkHotRegion.Invoke(curGuiView, new object[] { unclippedRect }); + + } + static PropertyInfo _pi_GUIView_current = typeof(Editor).Assembly.GetType("UnityEditor.GUIView").GetProperty("current", maxBindingFlags); + static MethodInfo _mi_GUIView_MarkHotRegion = typeof(Editor).Assembly.GetType("UnityEditor.GUIView").GetMethod("MarkHotRegion", maxBindingFlags); + static MethodInfo _mi_GUIClip_UnclipToWindow = typeof(GUI).Assembly.GetType("UnityEngine.GUIClip").GetMethod("UnclipToWindow", maxBindingFlags, null, new[] { typeof(Rect) }, null); + + + + #endregion + + + } +} +#endif \ No newline at end of file diff --git a/Assets/vFavorites/VFavoritesLibs.cs.meta b/Assets/vFavorites/VFavoritesLibs.cs.meta new file mode 100644 index 00000000..f42b9cc9 --- /dev/null +++ b/Assets/vFavorites/VFavoritesLibs.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 38092f82a4c054086826261d88277942 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 263643 + packageName: vFavorites 2 + packageVersion: 2.0.14 + assetPath: Assets/vFavorites/VFavoritesLibs.cs + uploadId: 874224 diff --git a/Assets/vFavorites/VFavoritesMenu.cs b/Assets/vFavorites/VFavoritesMenu.cs new file mode 100644 index 00000000..ba147693 --- /dev/null +++ b/Assets/vFavorites/VFavoritesMenu.cs @@ -0,0 +1,140 @@ +#if UNITY_EDITOR +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEditor; +using static VFavorites.Libs.VUtils; +using static VFavorites.Libs.VGUI; + + + +namespace VFavorites +{ + public class VFavoritesMenu + { + + public static bool pageScrollEnabled { get => EditorPrefsCached.GetBool("vFavorites-pageScrollEnabled", true); set => EditorPrefsCached.SetBool("vFavorites-pageScrollEnabled", value); } + public static bool numberKeysEnabled { get => EditorPrefsCached.GetBool("vFavorites-numberKeysEnabled", true); set => EditorPrefsCached.SetBool("vFavorites-numberKeysEnabled", value); } + public static bool arrowKeysEnabled { get => EditorPrefsCached.GetBool("vFavorites-arrowKeysEnabled", true); set => EditorPrefsCached.SetBool("vFavorites-arrowKeysEnabled", value); } + + public static bool fadeAnimationsEnabled { get => EditorPrefsCached.GetBool("vFavorites-fadeAnimationsEnabled", true); set => EditorPrefsCached.SetBool("vFavorites-fadeAnimationsEnabled", value); } + public static bool pageScrollAnimationEnabled { get => EditorPrefsCached.GetBool("vFavorites-pageScrollAnimationEnabled", true); set => EditorPrefsCached.SetBool("vFavorites-pageScrollAnimationEnabled", value); } + + public static int activeOnKeyCombination { get => EditorPrefsCached.GetInt("vFavorites-activeOnKeyCombination", 0); set => EditorPrefsCached.SetInt("vFavorites-activeOnKeyCombination", value); } + public static bool activeOnAltEnabled { get => activeOnKeyCombination == 0; set => activeOnKeyCombination = 0; } + public static bool activeOnAltShiftEnabled { get => activeOnKeyCombination == 1; set => activeOnKeyCombination = 1; } + public static bool activeOnCtrlAltEnabled { get => activeOnKeyCombination == 2; set => activeOnKeyCombination = 2; } + + public static bool pluginDisabled { get => EditorPrefsCached.GetBool("vFavorites-pluginDisabled", false); set => EditorPrefsCached.SetBool("vFavorites-pluginDisabled", value); } + + + + + const string dir = "Tools/vFavorites/"; + + const string pageScroll = dir + "Scroll to change page"; + const string numberKeys = dir + "1-9 keys to change page"; + const string arrowKeys = dir + "Arrow keys to change page or selection "; + + const string fadeAnimations = dir + "Fade animations"; + const string pageScrollAnimation = dir + "Page scroll animation"; + + const string activeOnAlt = dir + "Holding Alt"; + const string activeOnAltShift = dir + "Holding Alt and Shift"; +#if UNITY_EDITOR_OSX + const string activeOnCtrlAlt = dir + "Holding Cmd and Alt"; +#else + const string activeOnCtrlAlt = dir + "Holding Ctrl and Alt"; + +#endif + + const string disablePlugin = dir + "Disable vFavorites"; + + + + + + + [MenuItem(dir + "Shortcuts", false, 1)] static void dadsas() { } + [MenuItem(dir + "Shortcuts", true, 1)] static bool dadsas123() => false; + + [MenuItem(pageScroll, false, 2)] static void dadsadasadsdadsas() => pageScrollEnabled = !pageScrollEnabled; + [MenuItem(pageScroll, true, 2)] static bool dadsadasdadsdasadsas() { Menu.SetChecked(pageScroll, pageScrollEnabled); return !pluginDisabled; } + + [MenuItem(numberKeys, false, 4)] static void dadsadadsas() => numberKeysEnabled = !numberKeysEnabled; + [MenuItem(numberKeys, true, 4)] static bool dadsaddasadsas() { Menu.SetChecked(numberKeys, numberKeysEnabled); return !pluginDisabled; } + + [MenuItem(arrowKeys, false, 5)] static void dadsadaddassas() => arrowKeysEnabled = !arrowKeysEnabled; + [MenuItem(arrowKeys, true, 5)] static bool dadadssaddasadsas() { Menu.SetChecked(arrowKeys, arrowKeysEnabled); return !pluginDisabled; } + + + + + + [MenuItem(dir + "Animations", false, 101)] static void dadsadsas() { } + [MenuItem(dir + "Animations", true, 101)] static bool dadadssas123() => false; + + [MenuItem(fadeAnimations, false, 102)] static void dadsdasadadsas() => fadeAnimationsEnabled = !fadeAnimationsEnabled; + [MenuItem(fadeAnimations, true, 102)] static bool dadsadadsadsdasadsas() { Menu.SetChecked(fadeAnimations, fadeAnimationsEnabled); return !pluginDisabled; } + + [MenuItem(pageScrollAnimation, false, 103)] static void dadsdasdasadadsas() => pageScrollAnimationEnabled = !pageScrollAnimationEnabled; + [MenuItem(pageScrollAnimation, true, 103)] static bool dadsadaddassadsdasadsas() { Menu.SetChecked(pageScrollAnimation, pageScrollAnimationEnabled); return !pluginDisabled; } + + + + + [MenuItem(dir + "Open when", false, 1001)] static void dadsaddssas() { } + [MenuItem(dir + "Open when", true, 1001)] static bool dadadsssas123() => false; + + [MenuItem(activeOnAlt, false, 1002)] static void dadsdasasdadsas() => activeOnAltEnabled = !activeOnAltEnabled; + [MenuItem(activeOnAlt, true, 1002)] static bool dadsadadssdadsdasadsas() { Menu.SetChecked(activeOnAlt, activeOnAltEnabled); return !pluginDisabled; } + + [MenuItem(activeOnAltShift, false, 1003)] static void dadsdasasdadsadsas() => activeOnAltShiftEnabled = !activeOnAltShiftEnabled; + [MenuItem(activeOnAltShift, true, 1003)] static bool dadsadadssdasdadsdasadsas() { Menu.SetChecked(activeOnAltShift, activeOnAltShiftEnabled); return !pluginDisabled; } + + [MenuItem(activeOnCtrlAlt, false, 1004)] static void dadsdasadasadssdadsas() => activeOnCtrlAltEnabled = !activeOnCtrlAltEnabled; + [MenuItem(activeOnCtrlAlt, true, 1004)] static bool dadsadadsadssdadsdasadsas() { Menu.SetChecked(activeOnCtrlAlt, activeOnCtrlAltEnabled); return !pluginDisabled; } + + + + + + [MenuItem(dir + "More", false, 10001)] static void daasadsddsas() { } + [MenuItem(dir + "More", true, 10001)] static bool dadsadsdasas123() => false; + + // [MenuItem(dir + "Open manual", false, 10002)] + // static void dadadssadsas() => AssetDatabase.OpenAsset(AssetDatabase.LoadAssetAtPath(GetScriptPath("VFavorites").GetParentPath().CombinePath("Manual.pdf"))); + + [MenuItem(dir + "Join our Discord", false, 10002)] + static void dadasdsas() => Application.OpenURL("https://discord.gg/pUektnZeJT"); + + + + + + [MenuItem(dir + "Deals ending soon/Get vHierarchy 2 at 60% off", false, 10003)] + static void dadadssadasdsas() => Application.OpenURL("https://assetstore.unity.com/packages/slug/253397?aid=1100lGLBn&pubref=deal60menuvfav"); + + [MenuItem(dir + "Deals ending soon/Get vFolders 2 at 60% off", false, 10004)] + static void dadadssasddsas() => Application.OpenURL("https://assetstore.unity.com/packages/slug/255470?aid=1100lGLBn&pubref=deal60menuvfav"); + + [MenuItem(dir + "Deals ending soon/Get vInspector 2 at 60% off", false, 10005)] + static void dadadadsssadsas() => Application.OpenURL("https://assetstore.unity.com/packages/slug/252297?aid=1100lGLBn&pubref=deal60menuvfav"); + + [MenuItem(dir + "Deals ending soon/Get vTabs 2 at 60% off", false, 10006)] + static void dadadadsssadsadsas() => Application.OpenURL("https://assetstore.unity.com/packages/slug/253396?aid=1100lGLBn&pubref=deal60menuvfav"); + + + + + + + + + [MenuItem(disablePlugin, false, 100001)] static void dadsadsdasadasdasdsadadsas() { pluginDisabled = !pluginDisabled; UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(); } + [MenuItem(disablePlugin, true, 100001)] static bool dadsaddssdaasadsadadsdasadsas() { Menu.SetChecked(disablePlugin, pluginDisabled); return true; } + + } +} +#endif \ No newline at end of file diff --git a/Assets/vFavorites/VFavoritesMenu.cs.meta b/Assets/vFavorites/VFavoritesMenu.cs.meta new file mode 100644 index 00000000..fe92d6b1 --- /dev/null +++ b/Assets/vFavorites/VFavoritesMenu.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 65f8f6586ea4740238f667f8337cf6fb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 263643 + packageName: vFavorites 2 + packageVersion: 2.0.14 + assetPath: Assets/vFavorites/VFavoritesMenu.cs + uploadId: 874224 diff --git a/Assets/vFavorites/VFavoritesMenuItems.cs b/Assets/vFavorites/VFavoritesMenuItems.cs new file mode 100644 index 00000000..8fb6b187 --- /dev/null +++ b/Assets/vFavorites/VFavoritesMenuItems.cs @@ -0,0 +1,6 @@ + + +// this file was present in a previus version and is supposed to be deleted now +// but asset store update delivery system doesn't allow deleting files +// so instead this file is now emptied +// feel free to delete it if you want diff --git a/Assets/vFavorites/VFavoritesMenuItems.cs.meta b/Assets/vFavorites/VFavoritesMenuItems.cs.meta new file mode 100644 index 00000000..f7ecc6ed --- /dev/null +++ b/Assets/vFavorites/VFavoritesMenuItems.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: e1b1b30cd63e14da9a295742be768842 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 263643 + packageName: vFavorites 2 + packageVersion: 2.0.14 + assetPath: Assets/vFavorites/VFavoritesMenuItems.cs + uploadId: 874224 diff --git a/Assets/vFavorites/VFavoritesState.cs b/Assets/vFavorites/VFavoritesState.cs new file mode 100644 index 00000000..c4d1a52d --- /dev/null +++ b/Assets/vFavorites/VFavoritesState.cs @@ -0,0 +1,57 @@ +#if UNITY_EDITOR +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Serialization; +using UnityEditor; +using UnityEditor.ShortcutManagement; +using System.Reflection; +using System.Linq; +using UnityEngine.UIElements; +using UnityEngine.SceneManagement; +using UnityEditor.SceneManagement; +using Type = System.Type; +using static VFavorites.Libs.VUtils; +using static VFavorites.Libs.VGUI; + + +namespace VFavorites +{ + [FilePath("Library/vFavorites State.asset", FilePathAttribute.Location.ProjectFolder)] + public class VFavoritesState : ScriptableSingleton + { + public int curPageIndex; + + public SerializableDictionary pageStates_byPageId = new SerializableDictionary(); + + public SerializableDictionary itemStates_byItemId = new SerializableDictionary(); + + + [System.Serializable] + public class PageState + { + public long lastItemSelectTime_ticks; + public long lastItemDragTime_ticks; + + public float scrollPos; + + } + + [System.Serializable] + public class ItemState + { + public string _name; + + public string sceneGameObjectIconName; + + public long lastSelectTime_ticks; + public bool isSelected; + + } + + + public static void Save() => instance.Save(true); + + } +} +#endif \ No newline at end of file diff --git a/Assets/vFavorites/VFavoritesState.cs.meta b/Assets/vFavorites/VFavoritesState.cs.meta new file mode 100644 index 00000000..01037cc5 --- /dev/null +++ b/Assets/vFavorites/VFavoritesState.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 362e220c378db4e97ae41fa35a6fb58f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 263643 + packageName: vFavorites 2 + packageVersion: 2.0.14 + assetPath: Assets/vFavorites/VFavoritesState.cs + uploadId: 874224 diff --git a/Assets/vFavorites/vFavorites Data.asset b/Assets/vFavorites/vFavorites Data.asset new file mode 100644 index 00000000..69e7c975 --- /dev/null +++ b/Assets/vFavorites/vFavorites Data.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7c96bf1cd9605de6849a27cc11f7b5a10dc7096ad5055f14990ca0c570566ae +size 1803 diff --git a/Assets/vFavorites/vFavorites Data.asset.meta b/Assets/vFavorites/vFavorites Data.asset.meta new file mode 100644 index 00000000..0f0e01f3 --- /dev/null +++ b/Assets/vFavorites/vFavorites Data.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: aa1f24b2761bdb042a8ac0768dbda1fb +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: