From 2cc46ebea69cfb1a79f26031b6ad7fd4345e5c27 Mon Sep 17 00:00:00 2001 From: "DESKTOP-VVOCIJO\\PC" Date: Thu, 30 Apr 2026 12:55:27 +0900 Subject: [PATCH] =?UTF-8?q?2026-04-30=20=EA=B7=B8=EB=A6=B0=EB=B9=88=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/01_Scenes/MyProject/GameScene.unity | 4 +- Assets/02_Scripts/Item/ItemData.cs | 1 + Assets/02_Scripts/UI/ItemInfoPanel.cs | 2 +- .../Poses/Pose_GrabSnack2.asset | 3 + .../Poses/Pose_GrabSnack2.asset.meta | 8 + Assets/08_Data/Items/GreenBeans.asset | 3 + Assets/08_Data/Items/GreenBeans.asset.meta | 8 + Assets/EasyColliderEditor.meta | 8 + Assets/EasyColliderEditor/DOTS.meta | 10 + .../DOTS/EasyDOTSDocumentation.pdf | 3 + .../DOTS/EasyDOTSDocumentation.pdf.meta | 14 + Assets/EasyColliderEditor/DOTS/Scripts.meta | 10 + .../DOTS/Scripts/EasyColliderDOTS.cs | 18 + .../DOTS/Scripts/EasyColliderDOTS.cs.meta | 18 + .../EasyColliderEditorDocumentation.pdf | 3 + .../EasyColliderEditorDocumentation.pdf.meta | 14 + Assets/EasyColliderEditor/Icons.meta | 8 + .../EasyColliderEditor/Icons/ECEUIBox32.png | 3 + .../Icons/ECEUIBox32.png.meta | 106 + .../Icons/ECEUIBox32Merge.png | 3 + .../Icons/ECEUIBox32Merge.png.meta | 106 + .../Icons/ECEUICapsule32.png | 3 + .../Icons/ECEUICapsule32.png.meta | 106 + .../Icons/ECEUICapsule32Merge.png | 3 + .../Icons/ECEUICapsule32Merge.png.meta | 106 + .../Icons/ECEUIConvexMesh32.png | 3 + .../Icons/ECEUIConvexMesh32.png.meta | 106 + .../Icons/ECEUIConvexMesh32Merge.png | 3 + .../Icons/ECEUIConvexMesh32Merge.png.meta | 106 + .../Icons/ECEUICylinder32.png | 3 + .../Icons/ECEUICylinder32.png.meta | 106 + .../Icons/ECEUICylinder32Merge.png | 3 + .../Icons/ECEUICylinder32Merge.png.meta | 106 + .../Icons/ECEUIRotatedBox32.png | 3 + .../Icons/ECEUIRotatedBox32.png.meta | 106 + .../Icons/ECEUIRotatedBox32Merge.png | 3 + .../Icons/ECEUIRotatedBox32Merge.png.meta | 106 + .../Icons/ECEUIRotatedCapsule32.png | 3 + .../Icons/ECEUIRotatedCapsule32.png.meta | 106 + .../Icons/ECEUIRotatedCapsule32Merge.png | 3 + .../Icons/ECEUIRotatedCapsule32Merge.png.meta | 106 + .../Icons/ECEUISphere32.png | 3 + .../Icons/ECEUISphere32.png.meta | 106 + .../Icons/ECEUISphereMerge32.png | 3 + .../Icons/ECEUISphereMerge32.png.meta | 106 + Assets/EasyColliderEditor/Scripts.meta | 8 + .../Scripts/EasyColliderAutoSkinned.cs | 1832 ++++++ .../Scripts/EasyColliderAutoSkinned.cs.meta | 20 + .../Scripts/EasyColliderAutoSkinnedBone.cs | 97 + .../EasyColliderAutoSkinnedBone.cs.meta | 20 + .../Scripts/EasyColliderCreator.cs | 2176 +++++++ .../Scripts/EasyColliderCreator.cs.meta | 18 + .../Scripts/EasyColliderData.cs | 125 + .../Scripts/EasyColliderData.cs.meta | 20 + .../Scripts/EasyColliderDraw.cs | 160 + .../Scripts/EasyColliderDraw.cs.meta | 18 + .../Scripts/EasyColliderEditor.cs | 3112 ++++++++++ .../Scripts/EasyColliderEditor.cs.meta | 18 + .../Scripts/EasyColliderEnums.cs | 158 + .../Scripts/EasyColliderEnums.cs.meta | 18 + .../Scripts/EasyColliderGizmos.cs | 185 + .../Scripts/EasyColliderGizmos.cs.meta | 18 + .../Scripts/EasyColliderPostProccessor.cs | 55 + .../EasyColliderPostProccessor.cs.meta | 18 + .../Scripts/EasyColliderPreferences.asset | 3 + .../EasyColliderPreferences.asset.meta | 15 + .../Scripts/EasyColliderPreferences.cs | 615 ++ .../Scripts/EasyColliderPreferences.cs.meta | 18 + .../Scripts/EasyColliderPreviewer.cs | 714 +++ .../Scripts/EasyColliderPreviewer.cs.meta | 20 + .../Scripts/EasyColliderProperties.cs | 100 + .../Scripts/EasyColliderProperties.cs.meta | 18 + .../Scripts/EasyColliderQuickHull.cs | 1238 ++++ .../Scripts/EasyColliderQuickHull.cs.meta | 20 + .../Scripts/EasyColliderRotateDuplicate.cs | 28 + .../EasyColliderRotateDuplicate.cs.meta | 20 + .../Scripts/EasyColliderSaving.cs | 397 ++ .../Scripts/EasyColliderSaving.cs.meta | 20 + .../Scripts/EasyColliderTips.cs | 26 + .../Scripts/EasyColliderTips.cs.meta | 18 + .../Scripts/EasyColliderUIHelpers.cs | 918 +++ .../Scripts/EasyColliderUIHelpers.cs.meta | 20 + .../Scripts/EasyColliderVHACD.cs | 478 ++ .../Scripts/EasyColliderVHACD.cs.meta | 20 + .../Scripts/EasyColliderVertex.cs | 60 + .../Scripts/EasyColliderVertex.cs.meta | 18 + .../Scripts/EasyColliderWindow.cs | 4630 ++++++++++++++ .../Scripts/EasyColliderWindow.cs.meta | 18 + .../Scripts/Interfaces.meta | 8 + .../Interfaces/IEasyColliderPostProcessor.cs | 39 + .../IEasyColliderPostProcessor.cs.meta | 18 + .../EasyColliderEditor/Scripts/Plugins.meta | 10 + .../Scripts/Plugins/ECE_VHACD.dll | 3 + .../Scripts/Plugins/ECE_VHACD.dll.meta | 96 + .../Scripts/Plugins/Linux.meta | 8 + .../Scripts/Plugins/Linux/ECE_VHACD.so | Bin 0 -> 1153232 bytes .../Scripts/Plugins/Linux/ECE_VHACD.so.meta | 70 + .../Scripts/Plugins/OSX.meta | 8 + .../Scripts/Plugins/OSX/ECE_VHACD.bundle.meta | 74 + .../OSX/ECE_VHACD.bundle/Contents.meta | 8 + .../OSX/ECE_VHACD.bundle/Contents/Info.plist | 48 + .../OSX/ECE_VHACD.bundle/Contents/MacOS.meta | 8 + .../ECE_VHACD.bundle/Contents/MacOS/ECE_VHACD | Bin 0 -> 841168 bytes .../ECE_VHACD.bundle/Contents/Resources.meta | 8 + .../Contents/Resources/FloatMath.inl | 5437 +++++++++++++++++ .../Contents/Resources/vhacdCircularList.inl | 159 + .../Contents/Resources/vhacdVector.inl | 375 ++ .../Contents/_CodeSignature.meta | 8 + .../Contents/_CodeSignature/CodeResources | 150 + .../Scripts/Plugins/VHACDLicense.txt | 29 + .../Scripts/Plugins/VHACDLicense.txt.meta | 16 + Assets/EasyColliderEditor/Scripts/Shader.meta | 8 + .../Scripts/Shader/EasyColliderCompute.cs | 911 +++ .../Shader/EasyColliderCompute.cs.meta | 18 + .../EasyColliderMeshColliderPreview.shader | 103 + ...asyColliderMeshColliderPreview.shader.meta | 17 + .../Scripts/Shader/EasyColliderShader.shader | 181 + .../Shader/EasyColliderShader.shader.meta | 16 + .../Scripts/VHACDParameters.cs | 208 + .../Scripts/VHACDParameters.cs.meta | 18 + .../Scripts/VHACDScriptableSettings.cs | 74 + .../Scripts/VHACDScriptableSettings.cs.meta | 18 + .../Scripts/VHACDSettings.meta | 8 + .../ECE_DefaultVHACDSettings.asset | 3 + .../ECE_DefaultVHACDSettings.asset.meta | 15 + .../EasyColliderEditor/ThirdPartyNotices.txt | 11 + .../ThirdPartyNotices.txt.meta | 16 + .../Prefabs/RackAssets/GreenBeans.prefab | 4 +- Assets/vFavorites.meta | 8 + Assets/vFavorites/Manual.pdf | 3 + Assets/vFavorites/Manual.pdf.meta | 14 + Assets/vFavorites/VFavorites.asmdef | 16 + Assets/vFavorites/VFavorites.asmdef.meta | 14 + Assets/vFavorites/VFavorites.cs | 2041 +++++++ Assets/vFavorites/VFavorites.cs.meta | 18 + Assets/vFavorites/VFavoritesData.cs | 225 + Assets/vFavorites/VFavoritesData.cs.meta | 18 + Assets/vFavorites/VFavoritesLibs.cs | 1893 ++++++ Assets/vFavorites/VFavoritesLibs.cs.meta | 18 + Assets/vFavorites/VFavoritesMenu.cs | 140 + Assets/vFavorites/VFavoritesMenu.cs.meta | 18 + Assets/vFavorites/VFavoritesMenuItems.cs | 6 + Assets/vFavorites/VFavoritesMenuItems.cs.meta | 18 + Assets/vFavorites/VFavoritesState.cs | 57 + Assets/vFavorites/VFavoritesState.cs.meta | 18 + Assets/vFavorites/vFavorites Data.asset | 3 + Assets/vFavorites/vFavorites Data.asset.meta | 8 + 147 files changed, 31909 insertions(+), 5 deletions(-) create mode 100644 Assets/03_Models/Stylized_Hands/Poses/Pose_GrabSnack2.asset create mode 100644 Assets/03_Models/Stylized_Hands/Poses/Pose_GrabSnack2.asset.meta create mode 100644 Assets/08_Data/Items/GreenBeans.asset create mode 100644 Assets/08_Data/Items/GreenBeans.asset.meta create mode 100644 Assets/EasyColliderEditor.meta create mode 100644 Assets/EasyColliderEditor/DOTS.meta create mode 100644 Assets/EasyColliderEditor/DOTS/EasyDOTSDocumentation.pdf create mode 100644 Assets/EasyColliderEditor/DOTS/EasyDOTSDocumentation.pdf.meta create mode 100644 Assets/EasyColliderEditor/DOTS/Scripts.meta create mode 100644 Assets/EasyColliderEditor/DOTS/Scripts/EasyColliderDOTS.cs create mode 100644 Assets/EasyColliderEditor/DOTS/Scripts/EasyColliderDOTS.cs.meta create mode 100644 Assets/EasyColliderEditor/EasyColliderEditorDocumentation.pdf create mode 100644 Assets/EasyColliderEditor/EasyColliderEditorDocumentation.pdf.meta create mode 100644 Assets/EasyColliderEditor/Icons.meta create mode 100644 Assets/EasyColliderEditor/Icons/ECEUIBox32.png create mode 100644 Assets/EasyColliderEditor/Icons/ECEUIBox32.png.meta create mode 100644 Assets/EasyColliderEditor/Icons/ECEUIBox32Merge.png create mode 100644 Assets/EasyColliderEditor/Icons/ECEUIBox32Merge.png.meta create mode 100644 Assets/EasyColliderEditor/Icons/ECEUICapsule32.png create mode 100644 Assets/EasyColliderEditor/Icons/ECEUICapsule32.png.meta create mode 100644 Assets/EasyColliderEditor/Icons/ECEUICapsule32Merge.png create mode 100644 Assets/EasyColliderEditor/Icons/ECEUICapsule32Merge.png.meta create mode 100644 Assets/EasyColliderEditor/Icons/ECEUIConvexMesh32.png create mode 100644 Assets/EasyColliderEditor/Icons/ECEUIConvexMesh32.png.meta create mode 100644 Assets/EasyColliderEditor/Icons/ECEUIConvexMesh32Merge.png create mode 100644 Assets/EasyColliderEditor/Icons/ECEUIConvexMesh32Merge.png.meta create mode 100644 Assets/EasyColliderEditor/Icons/ECEUICylinder32.png create mode 100644 Assets/EasyColliderEditor/Icons/ECEUICylinder32.png.meta create mode 100644 Assets/EasyColliderEditor/Icons/ECEUICylinder32Merge.png create mode 100644 Assets/EasyColliderEditor/Icons/ECEUICylinder32Merge.png.meta create mode 100644 Assets/EasyColliderEditor/Icons/ECEUIRotatedBox32.png create mode 100644 Assets/EasyColliderEditor/Icons/ECEUIRotatedBox32.png.meta create mode 100644 Assets/EasyColliderEditor/Icons/ECEUIRotatedBox32Merge.png create mode 100644 Assets/EasyColliderEditor/Icons/ECEUIRotatedBox32Merge.png.meta create mode 100644 Assets/EasyColliderEditor/Icons/ECEUIRotatedCapsule32.png create mode 100644 Assets/EasyColliderEditor/Icons/ECEUIRotatedCapsule32.png.meta create mode 100644 Assets/EasyColliderEditor/Icons/ECEUIRotatedCapsule32Merge.png create mode 100644 Assets/EasyColliderEditor/Icons/ECEUIRotatedCapsule32Merge.png.meta create mode 100644 Assets/EasyColliderEditor/Icons/ECEUISphere32.png create mode 100644 Assets/EasyColliderEditor/Icons/ECEUISphere32.png.meta create mode 100644 Assets/EasyColliderEditor/Icons/ECEUISphereMerge32.png create mode 100644 Assets/EasyColliderEditor/Icons/ECEUISphereMerge32.png.meta create mode 100644 Assets/EasyColliderEditor/Scripts.meta create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderAutoSkinned.cs create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderAutoSkinned.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderAutoSkinnedBone.cs create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderAutoSkinnedBone.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderCreator.cs create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderCreator.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderData.cs create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderData.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderDraw.cs create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderDraw.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderEditor.cs create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderEditor.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderEnums.cs create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderEnums.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderGizmos.cs create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderGizmos.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderPostProccessor.cs create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderPostProccessor.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderPreferences.asset create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderPreferences.asset.meta create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderPreferences.cs create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderPreferences.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderPreviewer.cs create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderPreviewer.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderProperties.cs create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderProperties.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderQuickHull.cs create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderQuickHull.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderRotateDuplicate.cs create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderRotateDuplicate.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderSaving.cs create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderSaving.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderTips.cs create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderTips.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderUIHelpers.cs create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderUIHelpers.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderVHACD.cs create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderVHACD.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderVertex.cs create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderVertex.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderWindow.cs create mode 100644 Assets/EasyColliderEditor/Scripts/EasyColliderWindow.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/Interfaces.meta create mode 100644 Assets/EasyColliderEditor/Scripts/Interfaces/IEasyColliderPostProcessor.cs create mode 100644 Assets/EasyColliderEditor/Scripts/Interfaces/IEasyColliderPostProcessor.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/Plugins.meta create mode 100644 Assets/EasyColliderEditor/Scripts/Plugins/ECE_VHACD.dll create mode 100644 Assets/EasyColliderEditor/Scripts/Plugins/ECE_VHACD.dll.meta create mode 100644 Assets/EasyColliderEditor/Scripts/Plugins/Linux.meta create mode 100644 Assets/EasyColliderEditor/Scripts/Plugins/Linux/ECE_VHACD.so create mode 100644 Assets/EasyColliderEditor/Scripts/Plugins/Linux/ECE_VHACD.so.meta create mode 100644 Assets/EasyColliderEditor/Scripts/Plugins/OSX.meta create mode 100644 Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle.meta create mode 100644 Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents.meta create mode 100644 Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/Info.plist create mode 100644 Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/MacOS.meta create mode 100644 Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/MacOS/ECE_VHACD create mode 100644 Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/Resources.meta create mode 100644 Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/Resources/FloatMath.inl create mode 100644 Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/Resources/vhacdCircularList.inl create mode 100644 Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/Resources/vhacdVector.inl create mode 100644 Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/_CodeSignature.meta create mode 100644 Assets/EasyColliderEditor/Scripts/Plugins/OSX/ECE_VHACD.bundle/Contents/_CodeSignature/CodeResources create mode 100644 Assets/EasyColliderEditor/Scripts/Plugins/VHACDLicense.txt create mode 100644 Assets/EasyColliderEditor/Scripts/Plugins/VHACDLicense.txt.meta create mode 100644 Assets/EasyColliderEditor/Scripts/Shader.meta create mode 100644 Assets/EasyColliderEditor/Scripts/Shader/EasyColliderCompute.cs create mode 100644 Assets/EasyColliderEditor/Scripts/Shader/EasyColliderCompute.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/Shader/EasyColliderMeshColliderPreview.shader create mode 100644 Assets/EasyColliderEditor/Scripts/Shader/EasyColliderMeshColliderPreview.shader.meta create mode 100644 Assets/EasyColliderEditor/Scripts/Shader/EasyColliderShader.shader create mode 100644 Assets/EasyColliderEditor/Scripts/Shader/EasyColliderShader.shader.meta create mode 100644 Assets/EasyColliderEditor/Scripts/VHACDParameters.cs create mode 100644 Assets/EasyColliderEditor/Scripts/VHACDParameters.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/VHACDScriptableSettings.cs create mode 100644 Assets/EasyColliderEditor/Scripts/VHACDScriptableSettings.cs.meta create mode 100644 Assets/EasyColliderEditor/Scripts/VHACDSettings.meta create mode 100644 Assets/EasyColliderEditor/Scripts/VHACDSettings/ECE_DefaultVHACDSettings.asset create mode 100644 Assets/EasyColliderEditor/Scripts/VHACDSettings/ECE_DefaultVHACDSettings.asset.meta create mode 100644 Assets/EasyColliderEditor/ThirdPartyNotices.txt create mode 100644 Assets/EasyColliderEditor/ThirdPartyNotices.txt.meta create mode 100644 Assets/vFavorites.meta create mode 100644 Assets/vFavorites/Manual.pdf create mode 100644 Assets/vFavorites/Manual.pdf.meta create mode 100644 Assets/vFavorites/VFavorites.asmdef create mode 100644 Assets/vFavorites/VFavorites.asmdef.meta create mode 100644 Assets/vFavorites/VFavorites.cs create mode 100644 Assets/vFavorites/VFavorites.cs.meta create mode 100644 Assets/vFavorites/VFavoritesData.cs create mode 100644 Assets/vFavorites/VFavoritesData.cs.meta create mode 100644 Assets/vFavorites/VFavoritesLibs.cs create mode 100644 Assets/vFavorites/VFavoritesLibs.cs.meta create mode 100644 Assets/vFavorites/VFavoritesMenu.cs create mode 100644 Assets/vFavorites/VFavoritesMenu.cs.meta create mode 100644 Assets/vFavorites/VFavoritesMenuItems.cs create mode 100644 Assets/vFavorites/VFavoritesMenuItems.cs.meta create mode 100644 Assets/vFavorites/VFavoritesState.cs create mode 100644 Assets/vFavorites/VFavoritesState.cs.meta create mode 100644 Assets/vFavorites/vFavorites Data.asset create mode 100644 Assets/vFavorites/vFavorites Data.asset.meta 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 0000000000000000000000000000000000000000..cc85857bae0f1d36a50809c42292e71b0766d668 GIT binary patch literal 1153232 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 z zv_!jzR08^mi6t4Ddy4N-N@VX%B+NW-BX$n&x4O8RXQ}kBge@Q;=BSh_`vRkk3Nd3> z@%x$`$SmISyb;tl9xdm&0^@`?D(lw+)&&(E6&!sXRc1TTGX6`X+D5eipVaN6v`GRq z@Eyc=UGa_TNK&wxQ{12*IZ~ocG@aXK05=VzeRryTv_|4Y?Yq|bOe;HSAlJI?X6j>o zGrk7}6|Ji1NaSLhc#c|exCYcdu6baUs~Q1LB=NBj9tI!YIerP8GgWO>&za0M+oVxi zezrQ@QBFM@s0V)shji4+SCQO~p6K9)QjjOnfp1;=6@c1`1(z_Z#p9MC-4JQwYJ|wG zN|x$NbU05sxF5;w;ueAlon683xBfr&-aS6*`pW+&kVw?vXGQ`|TdA=fY-m+N3rZ@j zGm^kWgGNP}=!j{t$XXRjlRzyZHUT;tv$Cz-?sn~M>u%k)yKT4Ku5E1_E|P#Z5K+8T zgV)a(ZlVY(!uR<)@6Tsu62Nx%yZ`(izb_B+na}5aznu4ZpL5>leNOV9Yq2sQaIDky zxLVBUQ}R7Pq8oZjW^slk-%U7XK(fVnAS_-?1VyNdyDa^$VjTog1f^1_z`1i+U>_YX z+RT)J`cQ7SB-_`NdF8k+Jb*$95$7KSxZ z?Zb-99aHu^P=6lZ*Q}9a3x}FcTQ5t`9r9civ=qOlUWe{_;*b~&Sp>CotVS1%YOdif z`B54^D6wmNuJ9uppJ*?R6=j!0c`tvoH71vX;MNrFs?BU`Jbtq;OT4$ObeeNci$0Xsxds-oP7RRn&j&W>W- zR~htqE5C_YIlEM{WgXDGBA6qKigqnd7=b};s=%U1qECLZuiF@O@G3mIv99a#qT0;k z-pU9t&H~CI7uHT9voD8WC`p))1`x2@60V1C2Y)*hUk|P{d|uym+t0NUh4O{k z8@f&bMS*iK(3W9C#{LlQ1SCmyCowq0>P0;)sL^NDm%Uh-FO;*bi9$q`T zxuef8^?|L_|DeltUDprI+*xb@<$9aU++G|28>4eU=Jv8>Ot^JZoms2H4Rk+BHBh`+ z6C&0eK(#7c&3$W~r@`O9>?Y9m6+@{SxTC;@EvhdwIkjvb?4PQ=fOu)>0xcTmm;H?U z4Ew7e0)5*l*0lRf!|mnTDcbwEv~zp8c8c}`r3**%J>k*J=Jqv^QWL#;ES=Y^S-88F ztU~s6nXTC&Knd-A#9O&UxD+8t{TwV71^X?-)U=hYKt}Or>f^z}4urdL0N}J;doi*q zn`i;R8uA%^0#B_SpXt7yVbP9nQ?8js1@3;hV8b9_j9PYan0wGhZKf>d-?j#VaSo|+ zQ`>+ghSCeWiZ3;L?uSulp7`*43q!AIgqY-03udQcd%)gG7(IK)YJvNuLW+c)p6FUg*rx115I{s-rZ+o`^!B_4zs6gYuENT7JCdeYuFx7Tc3SD?*#{v1NgIs%>9yMrNjV9U{!1yaatlM&*|Fz zQWjo9UQb`gr6aDXnSe1vzbaNWa^|7ibVIQvHW^|cEg_^NobtYIB1+UIC8fN9%ZIju zvW|M7Yv699c4Gs05u~)3UfU@#fB&xnncZXxHu<2-3f@Ahh!lgDI8V!VH7D(RIBVtT zg4?oHS;8A)Q>271Eq|R=UoA-uSlnF?xf&g6WGP@qXryZ%=9X+-&AXVQfkc2Cm*^x- zaZ5`sz18|P;sX0kqvKfad*v@X{b&^6B>{iL1lxilOc<$`o+NbMNWeOpQkTH${_+2* z-Yl)F=ApY1oD7ZBN2HGKaq)wmMC(xX8lZ(Z84emP#GH;uUS7R)?53r|hIq5Oh5Bs3 zDQe7pgk^7{5B|nqBO4oZK4F5$n-I)#YL6=A;g>X-5<_Kl{?`LbIDz8a`E2BL7LgsL zU(<)>NA8iVvlBGVX>`3h;a#(h;3vq(XiJwK0cX+Ud@?WBJkA7bLiWONX1Uq=b2v^0 zjx#jSYGl-*TQsKWecnj7ZR>=OG2k8o_YWkqA(5!uK$nUT(Sl8vD3NaJMH@|(yH=>1 z6TE%>KQj20cJ8}KaDcX5z;EMV(3OfN=2>DgE}17D718CCrY;+Yvc1Y{rCymn(31a80Ig~KVn5_(!n_0K^LXa7 zQS-UhA4df_e#P34Y=j$8oc09g4)3U(_8p4{^kbgX(@V;%IrPLdjin5rCTNM4`Vl%| zqO0BhDA$RpWu^kQTWv2V@hO&=7hNfH4X^OCzjUod)jZTPl!o!*KxpKDnT`!;Z>V2e zHK2VJIsMuyATSt-E)5DJQE>Q^yB~TQajVq-%QEl$aWU=bN`J7Z{ROVS?}+Txn3noC zS<^c4KKf**R+ECyR%yk5`GC#JFHGiA%3a@7Il` zyr7NbSgqB5(; z)d${GD~gC$RY5DN9f{&v^163rZ_s`kL|ca(zlBaDBZFErVEuB}$m{qWU3Oq@hHWqj z6K_nOTk=ze##{a`8jsGB_fa$6GNG3=Jb~qyk}rOOQ2XMZ0)`Ef=T2{wE3xT~7^P`W zo_otYecduI*?IM3834q6P8^7O4$El%nZKaRm=)Y(Nqg!htLUD(xPF`oniG|x^OmIh zdiUpiwXcH_+I`j4+1-2vbUnhrWtNG^tFAuWm&a$${)|cDb64&27wkDD2Pd4QwXYYG zLq{7y3c3a&{Ghx0-io^@JLAks1$37Z;CuXHkO$x6H_n^`+SRBb zP>awFiO3DB+Q<>_nzrMuc2J_~0i1}4lVO{SNP%nDwy!U$+3zj?qT5+$OZpP3Ppe1A zxa1V*qd?z(&JHN9v~m|)wcO1i+(d?z)P|ZX59DZ^ToCRA^e^f4B1=du#Iq5j_O%

A++4=)Sp-Gj%(j!ogJVMur6V`an0pq-azI$tc;vRD&k3XP$#=p987X!1M-@h zIq}iGblu=cZwBE%!h0{7Q`{t=Xc9?lgqulqx|&q$UHRJFT5tTM_b=vV$LPJZ^wXnx zW#_5$#vk%lzRMh{{gW!%6w2#t4p2~?p-m4dg3}24u|u!dX$+s#=L>4>{dzzEnBv76 z7Mx^i+)+S>W~J5!2dHatF&7xmApersK^iO}Joor%=xSR0oLI+a^mQ#mLd+woX$kLy zze_;+X;s)q&HW0J#thamNUuv3AkoT4u{ne#WC4X~T(YAh7OQk}vLAAoAB>?o6Y=DM!YuoYYmYv^i- zmD}}0yLL!LOb=5iLj^E2X4yo#8EYBSG5^`3jrfvojuY6;ncXrHBHVq!>6I{)kS>+p zNYuytaX&e9xTCuzNj_@eDXhnunrW{UWE9IZj4T{>C>9dkf;b%30eq!mcunaDH!5WS z?vdtIU83NN$(%;IbvvQE|2cp7zvu9V#sguc%nfbV8b|YaH{^bKTSHgzD$X-HBb~T` z!@eNqUb`l?CA-SL)!BFe(|`X(Q%=(a&MqcOFFFH^{1xL>{w-5{iJYy_dSQeQ@OP=L zfEN^YDJh${gYu4M$R1DXjngN)+MpZL1`uZH3g}O9DtJgKL~ZI8gxL<@oJPhgbn2E` z-B5!7@nXXSO(ib?;Mx^L-X8~M6g@Mmr07e-hA;}dQ7_4FNb0}$v&Z2=-j_R1VM%uL zN2Tj%t<@o$2BN*v2Kc(gqZLCGa4N}qdbJkeh6EUG;k7`%@;{*zTD!L3Re~%cq|pwO z%Fhl`slzWRf#o|x<$&$%E}{lRbh6p&;7B07g*2OwuK&&U^*6UOd0e2OcL@&OE>C2( zcFj8M6kR6l6w?vuKTi8j=KfmKmIhLE`1TMUe=|7$k^(hW6#d`x&0L+xAJLgtX@IFS zM>recUFsKZyc0F+TYpJNNhhOf+k8Mv2XJQNZ2DC?pQ_7r*JU>3Ogc*+liU4MZqxIW z3}*k!WVEVaL5H+rcG=1fT6HPl; z?MlA{?6YEYcnHJ1c)>_?9NQ`Cg+0>-2ORzv*(o%dlR$*#?5YO6GO z5jUe)8(Z{26;hkoQ&>t!51@FH*^buE)&h}NyZFy(}u{?CXI zlPL*B-M~*9~~c+S(UziE=fi9>{(d3otD^TJ>SM zGZR&jrTtwYe|YR>Q$o4r;(&5cc?!0b%5G+uII3dni~Ij+007x|4aR@uxKdd91pcI= zVamv-^RlQ|_d)HF8ncwsICz=&hxudUGYeL%EVClSqLV&wQ^}(FhH3Nq$ae83?3eUM z-2G6S6KOrW`=O!!$p7E(S3-|?(7l0%9XgDn7P&p^jr1Tc!u$29!tqSyVO-;x8mx~q zoB~K6TvyX%D;a7#g|LvvFZG%ILe?`f4<%;K$gI2Exc@CkN+nsuG7RpjQD8ud#p1z* z=YJWhEa!;-P(xsv28RjQFUFaR{{hIGhRk9A>m+K(yu}TkKOAPEk^padei<8%B=lud zyQW+!WA`0@O(e$sCmimNuzw^n4a>z{71bR0(#46+vJP17E;4WIJPD_Ot$olQb(tkD z30JDi>buJSt~T=-3Bc$it3NtbxW*0vrwY~B8Q|1|H3t^{LKg)O{xc{J1*tdm+rF-% zHglj5ZLU%~3NIK0<39_ukT%ONV^8=3gzrpluWZ!4tB?=7EVD(MF7<1hef)0F7W&!j z8r&T1ZPETZcJuYh_#m$(D|kh_L%dF!Q|gTiS&B|bj&hmt(0P0_K;Hujb};2?Cs$y= zu>bqtm)W}M5-c|n9&S}NG)pt!? zndm&u-lnYd7c---V~O^IMJyE=z;UdB1`+Op0I z4~m2=j&p=SdEc7RReVQ%*OZkF8JvEt@ne)}ceg`@OUa=}?7hKcge9YI95%$iun!>}jRHLK(j+B_E zLZo+o|F`xbVfbU5+3JzmFreYz=nOA|XAA-*OlJDZ5y zJW1uAyk8CS22VfI9u()@iI6-R)S$IH2$ez}>{uhST}9Y8bzH zk?5omYvzG>H<;h1Aia%MHcBFYJNRam)~nkJ?|0mk!xcjA)vS0nnWZNT#=tr>Kw^Mx z`29MsfFE%h#kTbz)b_8tQ!2P6wbZ`obcvk@Fzgr8{^}4(hy9q5@i8}&%y7XyuuLg5 z^<4@=ba0mhhK!MiA}~^tv<;#mY`olkUdM<^E76X;CdQJT&S5I!H8H>_czv7jrWn2k zK)n_&Ec5UL+8Q6i{+;YWy#zLJTB&}LMY|F;hgx4T)sz~>{ejJznMVI7)TJVE1Ahc$@rgVzkpXn3ng2d;X?8u|Ciqq&JDiM7yn`5 zH--6MCjiNPm2@C0&uj{P^(dYQ*P;ssEmP%KefH#m0A@A7LRwlV2l9F8%i$=+@|k-#U&{4i&09N{(;_0~M$E!P1aDG@sEk-!cS%SO`dlh~20 z<;Y>mpCdpvJWP5cY3znNuly}C;VD&j>6+f0+u$wLiRVFmJ*t>@c`zyx$+Ew^;SyW) zVWp8qW@^74o9W|CG8If=Cib)_wc#oGs-Lpi&{N!cf_J>+X+dE(OPOXOg{7?^L;Z|tyTIE2NOuZ)dt`v50p|;F)1VIQ2$3L`aq}YX?mcP>bALtf{4M4vH>0H%2M3D?R?QC+= zmL|#8Xcu>>P`+KiflGhtn%)R}72)${(%;WeKQD=(3+^G2b(|h^2z#QdV;Mc+Afunb z)G*X$kQ2)+T~mK}ew*tivvWnMnds93jDOB8z^^qUO6x4SjUR$X-D1B>$9$Pz3f#uu zW~S>VEwShMdp$de^}VWqd<*1ll<}SShZN;XYnECTogNou$ZsPwF+a`xEhyX!a`~sH zlAW`flfU`NkKMoIxHZYuZx6#+=jN^p&=FkPoSd^c!_I25V*G%WZ{qy$!~9(-`ENa( z;wDG`&t*XpjIZdNt*N46B~A805y=cp+f- z({pvZp-l%Q$0_wzygQ~=Sa>N$KsTVudU^Sf((qfaNzjZ8rI`$HdM%A%JTM~S0wPgM66IaGcs80-!tLqI-C#9HawoFc?SKKM9rVjdFhFl9%%hcO*(c;%jEhw z#osf1erc>zE&hu->o};L7~=rpfH>=?zM=h6yFUp!Aa+pXZ)MZMr!Gv~k3_5)6<4r@ z0f!16AP3FSlgRtTKS?y~|2y=L&ZS4|TsH3L1kQ?i5034R$v>Tw=btLzpO#ZX>&I~e zQtH3@O_lkTh`(Jg;+noj55oO3q{rZ!g8j|pF&+!nuFQ6WPEO*_F5McTeg#C5LxB8( z49z64S>NEItDFD8Uh6(Pl)7z4qixJ$c?u}Yj#!dlY%Oi<6wTLabmN^Cm?M3?<(oBs zx2;nN8c&rk@oaE`^$yG00zZ`t-0k1&0)LJsgVZ}9RVw~knREwO2?O$k$++v`EwU&oT#&%E_pV$xb#zwJ^m5$4S@x>$!* z>(nz`%_f!N{W zgTu)5|3|@&>ei>T|HO%tlZKjQ<@Oy5s8!-rY zE!SQ7nvE97Q(ie+cR7*?*kbXARR=lTNO+?~P+~23O3bLw^0i+1Cdf`AaIZ1?fJ_m` zWlI?Kym#!L_*=64Hu@2*mr8Y3_WK|9Mz8foZ`Xorl-0Cq|E4>PAw(w?7Mf@72w8_6 ztU($X)zPumrVC*bFp7*WCQ3eQVVT}7^*UCt?r1sX-_Rr(^yF#1m^kyEj=J9zdf7cNdxd;ua^u-I2V&jk~}VMB+(lF*ukUc%Zyjp zp8uJDC+de_hz{ZRd}=b?pTNJ9f+F~LQfS2dJ1GQ$E?V51_wS^zc!A5Mz4G^i+H7Ny zVv@LAav|n&N&cM_+(^9qI|YdY|4xcYIhRWo(aj|$+yxc*cT$Q%XHo+ztICyVmC3)8 zGE^V4vdKqO7Wj8kwq{)toAa8wyQk}m)|+`)&$n-#IG&PF8u&QBuA^YD4h>Yr=&vDNJ70`FB!= z$^rhJ@|o}+s`eE3D$!*1qPMirzmtN4nV>+W+@qich1nLnZ1{IlRMi0gP6L}=eUxUa zQZ+m9@1!E;9Xa(>=%!xroPL;pC*|DqKlSglbkvKGY%nV$MEr@sfHi|td!HA@hHP|t zHGgr8bvE(~*LLv&fT%;{kl~-B`F8?sM9$%FeucQ3iv>`(BGRsU8FaCek5ONuBJtg0 zp1OG05T!Zu{g?8B+7zjt5?^%xUZfFNorIm@X1v&-ZpR+M8g+xxwh^_KkePan$2Qvc zX%9(v8QY{1Ug7HvN=b={O1(nAAbltouKEdHHp&&P+&w9PwCRV%Jgw2=L4w$yXg2mM z{6)7O0=&^wCsbP%DcH>@;Q?!qKZ|(Ey6F*;#h8Owe}Kgku}zFd#JTh3yrPyWJKJsw+f0hR zv@q4P*PH!1FT(OoyVa{l^?>C{y`-1N3D#gg%dg(o^Y^fZ`7t}&A2O{RUFLa4KO{Qa~XIg(oM~^Y8$gv4H&$|-#)9Zk|zfS z6nB8XHw2pE@9!tUtNcF5?+g5D@$F)o)h^@nUkVcVd_TdTGCj+fr3gTR!TspDUinL2 z^dtOi^r9Dvca9quIS3w{KPXzEL1fJ7K;7T7BK%q z)LQO+SM~9?iNJXZR4Wyz#q|h*%@#-(_};^J%D!1iZ}VX)(lq@~yqkU)?~)xoL|2+L z0DDx&BV;|O5a^@yySC);1;y`+%+JzFV&5j##5Jk62!515ZJ}4to#@iI@Cu`wdWSz7 zqdhJ@^|T6S^dg`B#Dy#W5Q z&8^}E9231r6AbdwEJSA~719{+Ct80Yv>tKPANY&9XQ>1gW^a2ZSWiT<^goM@C27@t z0M7i^`cW{1@iUs63eAls$P|UAvBs)2+5#dv6!A24-I_WCV<-YKDv+dv$z`9(#F{qK z_&)Vm@Fkonkyl#t%8(10rM4-yn-4aTStCA&pU_|sdcx8T=OI{Y5}r)0(Qq;m6isPc z33B2z+YvSq?(-+~siAt9Un-2A5O(BZx@7bJ0I|eG#=9K$4jAG&D##)!Sko+pOjjNe z=}o7SNua`BmttW!a@x*~jlLIIA3csjr_bX>=r1yTf!-nm8G>Lx7B?AaJLdgGEhf|Z z_`8P5DY*K@y%Uwz$iS&@v|bkTqQS7mu zMvtG>`5Orr ze;eoVhe{rQl-yB%q^kHG;71BK#Nfnb1k=~lh#~Zz2A5PI$)oRx&sj;R1kge49ajL1 zk&CP8;l;_IHzz41u8>F&V#`tFT>9e5Fes9pC@I+lFtCm&Q3pjRAbBimoOVQs605&u zNq`w7fUT4;mZMI^O2swXYAp@mJ+#~7l;m&Q@qKH5WO{P}+;+tNo=}1A(U%1cAMT&G zSR1FKrwjh&m;%^mi>PEU?Od z*1jF_lIl|$hEFt+zs_O?7Ix;!DTlaVi|L*Vj`haf7h7g+5Xm9AkMOwAFXEyg(fu^e zgh!5y5VCHFH3yMkJ62J%NsRo}*niMHZcS`{`}i2*M&dc!;T zVgJi|LyhmS|Fc`5!}GdMp*(^%_0s2(?mEG}(IaYxe9{=cBz}|P?{V>Jzy5OZh&5Rn zG?W2>?)SH~89i1WBNKsS*Um~72}7K}DI*!*=R(Cn{O6Y~3mk;DB?u-~c|}-1#mL{3 z*KsTTWGZZpX@9_Z2cPpRX$-Vc)pkD4VdTUXu%(T9k?&g>XQ(KAJ1Kc1G1@^kk(w^1 zW-TDOk^<5q(3&7iQta$9<{!DZneJT5jHOniryT6}1W6cV1h#}Q5hF-0m2hVnt+VkW z)cMDv5Zj8ZtrrhyYhOWI!JcZXH4}srk=&?>LpYA3nHgHB1l=UywtwmYGF}r8TBZXu z*Se73{0rOXQOzuxp9X`aB^ZlLXFSv57MirfL;i?|Olikspf1VjO$Pji=ts-Xp6l`p z1EwWPv;5>$8BY3DxCIyFw;alExxySr@jpdf7cmVORp;$|y&BD8saw zk2Gs3GJYr*m~B3EY^W|A?4RnJ(AH)=h}nQVAnZ`7mEL$sQQ2L6Bz zIv)p&8X_HZsEV%I&mSa?fMpbg+JZ*lphe-_Q1RPKQ%Y_q+G6BC1>Xp%B(CfRJ# zFzYKdy&~`rLyYE6$(qQB8QCDc?P8jZf&QpSYhY~ibiSHt=aiHgRXGBh2yoOiR55($ z#1eIALUYa1QfYz|bWspxA-oD6I89x*jMURLwO+v{RvTNad)bj$l{~JABdw=k@TAgm zpSeZnUD%8;A-lyiDu&bZ=K_IzI@WCA3;dbde8KhXyU+Ujzv%Yb#d$P}#m*heu3GEg z&PG>&B5iz+?Vswl9rv*23tAj*yW(3T{vWB77LEAPXKdDcX$$z!d8v9``+b3zs;O!d z$FZH{$$P1Cu;AQO?Zr(MqsZVIp9N79q99VrRWZKMu)#vrqg?C9Vxdex` zLkZ$stYqU9U65~)=@N5fWo)!|>}LNC6gVBd#M!-bV%y*#x2fFIdxn<+$OAj!a; z;6Lh{8{vsH@Nq4%dq~qpn*kPr4$ChPrA6j1P`cUGWNSjhiBmCO0T)^DeANt`(5|gK z%C}l}YTl{r6=c7unT)9eO0kaIGf|l$_Z5vio7OAy^E~XLEk^4|^o}ClWpCWBK0Txq zkZw)tNz)D$1tXSv0a4vhrBl~4tYYvV()1kpp411pA7P+G zjD(n&^ErYgNw~nq@nKhRsd6hi^^gK?pxY_{9UE&7kxV3J|Dsni<5f1BS}YR3vin=x zRSz4nJ{~8A!I9-ar6PT=%2$SWs5q_<$x9_-x|W$`0|~QkRU8wjp3_q{wDGDC>zfG>`Jti28yaH%_I1^E|Ha8eZrkAq z)f_9`Vb-~FRuWD&glAHbSD;XZu-uvq1tTZ0K2(HcBl{LgdKL0NvrGWXbxMS>1^&pK zw9ur4PNL`kJ9^CSm83VV2Q%TJil>w9wMa+@^HAxp7?7@q6i#-Cxe`*qit$9e=+7n{ zk!dTWR)WRnh)IWp@2QSEDHh>Nv6d1%MPUqQRUdB@!qX~llDrT36(bL1NF~ZpyQDY{ z(k0$8=q@+F@AjhSDnytUN~B3y*$dLI1jPXEv(Eg^Il{Z5B%^Z#zuWnMXfRC0Jl zk(-bAIx-})eJb!vhD)-BuAj!15ex^GcTFaCOg!0z9JR}Y&iu=od0e$%kh?OWz_{=c z$&8f1%%?w#=Ft@6YA;9Dyqk|1rG7IA+BCemjl17pqsc~XMzbBA+N@XbqTc7ZcV;>| zb%Eeoz0Q*wa$%-{2{+=k{`(dPy48h{f6#bOYo=OlanWh>^rrHrEhrrRygYXi$s*_R zcNTwhr7LIYBeThrrupfYHR0QU2uTp(EyQ0mK`)EbsY0gJB)Jyv+J&{5_T<1>Vnj$+pj&4o$P&X(gY0e<{%>ak zukvBWhhR0!9}?NLl=&bt(yvn&&F6T=uve8}nZJ}s5O|YIXf1qb0YnD$!b#-YM6(!2 z32jX($*)NxjcO3hJ0M0RZA1jH(X*zV2wfw>S+d3f^eSmIu5N_IAfm1a--)}6a|k`1 zKT*0!DW$^4QJJmN*(wcm>|C1V$|GRSmkREs@x5))Z~AxT=_B25IEsI>IdJ)f{>?74 z@4SDrseB*9zZnf2PCdshJYP-at#=`4&NwD{GdqL#3ra2cGzF`J(;^E;DOm63%5ZLdQjb^ZQ!#o z1&+_GNcK5e6}UdjW28L2_+IrFRCbao%jYjBwP3&b3;dt`a?W7?XFmu+^8U~6un_R; zpY?x6f93y-pZD{}C1kdIS;8Cx-rx<}z-VA=F@BUUd~RLB7jL4qUj^oHg7y(KDhPSr zTUp~$PCD%qCdM^4Xyi-^;EnbFoJN$wg{FAJ5!z6fuC`I7mC+l`ANxAroD(!gSA5EW@=POiSDrZ^aA9 z#RvoE;3VyXBn-2vt@Jv1e1kgnw}G9|&~Prh^6CoPZLM{`s8m~*j_AJ4FNw1ic8qHF!0Yz;$d?IB(u z#>R5Z9)8#NKc2^5!hhNSnmd4=fS*j#_JRJ@@LIvS)lRz44rdhW`COJl#TX)nh-X{` z5^Hi|?@=lVlD$;&ncxmFXB75b?O#A;IFCfkJ`A zLcuOXCNkMu$XVfT+ z`01HygpM{v%uAGFy_9U#gl+?3tiL8q*1!ymk_0;7kB~}?x%=ZPmviD^q-TL*XR7yQJ!h+7T{OSCopGfDko96CRXRv>6`opAWW>tGmh3E2_IG5V+wxEeS$gLbSZ7=sE0qsYLgc> zn9f<^P-jVV$NaF7TYlL1d6{t&B@1yS9;=DJv~G4h#Ttj>4Lz}yFhE+TmvB6wdnSO` zCse75IKbZ?DMGC#L>e@yrF19C)fF7E(G`TVA^{nXqtQF8uPTHT3}WQN>R18C!|hTH zYUC!M7P1E+G8ihIqrpk7s!1^l_Lh9m&><3W<^C>QV)J~OZf^3PB zS9{~i#B-YbR)rzYktUgr)-^ohU3r68POall8^)%#vF4g>t~XK?hKbV(6^3ly;V`zSLgOmoRLDm<6sPl{Phd->Je_NxB4!MJ1`Rl5lD6*k4fQfHnK#1Zyk^1Vd- zddgvsi+qT0_Ip#`J&kwj;T`s^k`H=g5o1RJdVH%Vr;^Fe9u@)*%G$&hgW3sB+7z(I zKFGg7U$jtBmhl<|nAH4wjX!Ew0Iw8sq#2VV zVQ^r#>sG7O3325^YZzfJ7V)@oh+A)w5Atyf054_A$d~j&=hOR1E2SYk!12Yl&;6 zA9J}zK-S@G1SP^s=ESG5tti*3X-;WA=!zX zu46I)*fz3P>EC=v(C6GHDRSLN37sfPNNa*Sje*V|swrDqt)>&kh%j?;zj>DWzS~dP zuU(ZJVNlCmIXRltUY5-7Wm3H3V*XnH@6#feJD6i9f6Mh}G_}~tGt!;N$6TkZKZFa< z$~UA$tZ6ZDX*h>82G*V%+*#sRTSqAXU6No;(($0#|4JLx>>t>#HTw#tl?i$8W^cbL zuP->9vX0|#C@pg$LjWhFk9iZlC^=S0eSSL%3yiwDL&n>8U;2%#zW2E+{$dDVHkMbc(S4Z8Fo6vtqo^%g? zqE5hK=~oo!Yji}?_EPq1;oyi8gK&|^71DrYM-I#xtsnbK2S#*kWK?5iZd@Q+W<%(S z7Rrqa4j;w8O5vFC*lXhJ-jjY`Tu_k||eCyBf~KXvn5N;}lLB ziCFO0^!@~_B9kYAbX*>pQ@9Fq`^JXocM&TE#j1i>q?5Re!vV+wBAwo1nR5!Ka`H~$ zK3j~|t2r*>WjW?x_wbu{3ilRYaCq+2r3SYuo03Av20~xdIclVez`kY+W28|kW08Ts zqW8rPp!}&6C&T+Z?Ty3N7O6<069@^dN!YLWtAVTNuVO2dI*gq3iB z*pgK07rb$I#dIRYnbBQVypY0Ov3m*f#{D$bM}-W*UqzH9St^_!r#`8oH~q;#4kN`Ln&p|xEq0cEWRO@WTEw0!xes~4HkpZ^gt5lGQse;@lq&W=WdVr z>MFf-T~=!VyLqmQw0*le&_{1zk*h#jqVN&IuBfLoImdIfTAIwqY_N8Q-H%#()er#& z?Q7zgnI@|dntO%7tOSE@b3z9PHD|4w0|lzF2b-<9R3jPsrJIPqlVdl-Rc|$_lE6*f zHsD!K=ejk5&V$}CIgi8W(S5}NEOQ>WWGGe{p1{tE^F*gbAkS}@P|7B|I%0lvP)LOp zBKLU1GLKM5L+9;N?j?{y8Zx&}(K^BKQEkR|&gb;q$+$f+;Zc)g;G+zhW;mvIlI=Ye z@;hgDgQgzZE zkK%96wDgvMVL^AWtD(oa+YLu2yYQmLbkpu@O~MQt%R5>>JniP2)X$X_z2fc$Z9W~b zRb-g!&F^qI!9FIwCS5YauE81rZ<$^_Usg8C7?cMOMA;GQ(GJnAbmXN;_DRC*^iRvUCpjZg_jW1_8er`6?N2+#ftwdCkk%Hsld z;Gph%ERI44bxUp>BC}2#^jKrA>RuT(*Si5@j(1K``@z>!dhNGkiu!$F*Ax%}mpw9k zQx*Hmlx|h|1whQvIn=j3SKlLHeSzb;puX*@@6mjHcxE|feW2sQ`bekxT3i=vZM@*0 zJ6Z8{6CTBtosF7MH2&Pga=86P{_Hdvcg3{5;Kpv4ZB%aCVV}XD-JZp6zJi@5+>C9@ z3EkT*xB&lVHnG}r zU-1STL7;pJSO;G427>cIUwDABQA~ru+TUFdK;sL~T;Sy(TKkqj{AfP$+_>b2N?!4p zo#FuMSp&>@u?>M+1KQ%bU(->hd11WWN^qlIpZP}LN}VdQ2&z=38(W(q0^RPNy-g0Y za`?-^f3pJ|o1Y82TCh57gKAr%22|U#gU^Wpd`?sg4M}_xSdjlbR=zef+;|nxdPyvm z*dD3etNVs!R=4}wAIofO^MUO9%Cre(SOZVk774MzclIj0`3VovKF+T%r+v^TJ@s?Y z0GES2N4`J?$VYSfyae;e|ijuoBD9liDN}Lr$)#)5ku1a==%_>00Aj`oJ%cuzvpF=Z`K*Y|&p1|lYM90WNlp5Fz z@o^3jZvkHoZWD-6!-$=ua7VEr`^mIJhs~v)-05+0>DEqev}`m)^oBzvZ0b~3n)no;dsD1Y1jC8A(T%pYFZ#>%6#EogPvE*9SuyG!rz zp_&{vUoHU6WIzrNw#2Xy-K{>@@}}Zo<*Mrm2(V*!iKGq_WFO&Rnw2QudKc}}UV+li zX$8*s=E_+nti_@ahTizr5n?%<3%*^SP~r7(XgxUIki-^qYo=z{M!-a~rL zB&+D!bu;D67lW^0imUe76yntrzK0R1CE+(H+8^kK|LqY?~6&eX4ryP77n4%sBB z5;uXcF_7<>)}cJr*(_BHpgGzO{P7XS5e3g{3+NBDJN>@{;HiL}fu|&Qz^$E#^ISmr zh>@TxL}QS8FCO3sgSff|iH{6^R%M9+26>aBh0YPA6mYf4&k!JSmC$)P!?JyU8=B5?h zb#u`-4q2%Q!=fqbVQXsiACF7k7^!O}6N>9xms3aU7O?5jK@!!W$H)}q2=UfW4^UoD zNLAGM3bKvubo%`fPeRyjv~lwST~_6B<6k7YnDy*p2~iy}8kqADejUKriOn(IHZ z5Hof>3|D79`c_fC{PXe2$=o%4jGDBmbs~WRBd2t{Y+-1x*+$G0;^Z~_NDU>4E_z%6 z%+)oIGl4cqdl@=&)b}IW_K3!j)|%LP)X67lknz>0^I+ZWu^T!%IT|Ha+t?*h{dVnV zwL(O6H(kP*qMpcSBWHT6sGaUq?d!kC(O(Ad1N`;fwnLjeX-Za=4I8K}`!ymabW5Qb zisTjf-ir`21hqeJ9B6ot=wTUsdd2@{!cJ#E11-AZ!u^+8BX;X5BzcY_pDOw4iJaC_ z1lyZ7xL4^AAnZSVt`#By^!KMYw61FNGhN0^tA!Gmt@UrHqoO=HVDd5DuQ<|w-)%d# zkxTu$mzoQfa!0I}2INl?_WaMp?v)~ldK=p7RO0~ z51_@#k@4!tD?mCpRonIh6Kr+W2Il+0*&^D~rYa2#b1; zL4j`SrFFB<*$L(45kj;;)4Ql3ZC_iIo;aqnj9?!cNrj>RO``&R&jw~_{m^;Z6)zG$*;K~}v> zc}<5@z++V6>~8E-Kb}x^-2G6Lj$n1wApIH*Wtd1Pn?7c6#4M$zUeKpW8I?|!aK=Bl zXcA4)8AHiq!?BIdkx0{1O17StcS)53?WT0@(B?thdz;Qhr3NK)rj5b5 zIqd@50i<>v4U#|?sO&nL@&Ax={(rE_vDIGBL{JY;($fk2yxDstd3CTbjFRdo*6sfm z#2K)i#4Co^l#XgmLtubXocz2cIc{|OfcbelRR}IWHsN_d;P34<;Q%yw{*1r3C!D`G zCNy%ICU|zKjLh<)Ge0EM$*%Gz7%`osoUPz`ni%;2f0t^xHEEh7my)!JV>;AM39^SK z4+zmgJTJYxXo6)P&+h-}9ME7)gba3;JTv-1C296y9jgT{-x>@@G?m=PT!;?Ph%P!o zgAmG7W|jEGW>k0?{Z*O)8s&l)?`L}(D6CGiPNI$7%s6oYoCFWK zNPyd}CO9W>EEK%J&DDCbQnW1W_->uLV#Ub7Nm@#3(kt4blo|=0f9m=ZxvNN;-o*D? z{AsAtdq|*@Pjr9MTa?J0!L2%)z8Iv6NgJ1vO*rE&l!F`8srt(Do09S45O{Ui^ ze{j4@8N*G6`GX^*TSrDd8OJWrw)ZS#~*pJPt zZq6Ot{hCkQ!9Ap3xr4hHeKO|%frV!?vK(EgW#$pCoL`U}gl~m;gp=9_Z3X|OpyET9 zaOHWIaFLel%q85En0bVwA)?Y(83~}Uz#-hN1TZ%^grjg#=H?JioshE3_@@Y!M>y?c zeBu(0CqCh3^WFb-w@&2`uAK4u-}eXSW)$m$|80M8_k8LY{^0H)Lc)T9{@`$~a6gtt z0)KE0QU>{hTmMP3S&UkU`Gczn{J~jH1~59^C&sC|Ob5_q7et{4{;WT^8?E4$qx*x~ z1YF2E<5%#8G@nwBV@QVbYs!dr03WC=Xby#PFRJ3U|1>ah^nxnX8HA)SGMWMOW#}#% zB~A#=nLLY=BAQULxH2VpMB-I?%lRK6Np!cCA042Jb^!{ zaA^FDLR!QIW*0@lQl{e?Y9K&u*tI|@V}Z0}RY^G1Q5wP$qlyAM??L&)Nf3^c^h@0R zYn!9#48FU69qO0-zx&G~O#a{fW$l|tLjFH@f0>2-J{C?d3qhK-A?_XM@2}?^ z?E}Z~_xCY2Mfv;tClVd$@9&h!|EvD~D)9IBc;N4k?tM1f|y1q0Me$1ZR=MJboO!Mk*K#0NvqNsAOB%X9%&OO;R*6eZjI=k<4_2RVR~ zLD$ia0YiU(dG*7bzduwE|6+fCYjXbndUF2$di;5x5D`en*td`H_lJ!7FY)*H#Eh=u zNAUO86Zrcpr_9jbpOrnCQ5o@f?+hKH(Rq_!e>0$REPsD>?E5+So^m|Kcyg`iAsgay zWIxlnAeWq7*?aQ21!3sn^ zUGkQ7eAEbjLmx~+Z;yz}fNVm)OdP-2Xw+qus@;yQM!dWvCbJU+5V~LPeAtXhZrV$* zutMAm5>PzqqJ4fMq|(olm_#y7Z|ZTF2bxfL;88i$v`L{G6uRG~ z)fFF?%$hFo2v@WTdsK7MJ)cCg-jZ`-z+5VGW9@K-G(Pc*yCyRp{`@{W0Thiu=O!}6+m%j*5 zBkqxy0tklSmL$%uus+3y$-N{4A(`&WH<_ImkQcYG=p`xpDW7wW6c;pg=fL@MvTKnv zceL7!lM?-mIqppZZ|u0xG@D@bueg!~U#p76p|hC}GFkqseCJsrjTa5j^+qn4^Eih;S$}v{^P?5|0&}mAa+cEi z?L`1%Qy;+ql_0N4i-V_fzg6KA!^%i+#Yqf_Hw87Ylp{2X3-r>)59kNyzebWT`LH?P&UoO2*|=|Y z(!XMwZpTQQm_PGhBM$Ur-Y?q2i%eIfrXApoE4-oq8Y&tye(z9sfpf3?*RcFJGt*TY z8T*iVX%%6))fmUWYCN^`IsY}PzQBi#ozJ%Kh}Q@Hn^=8AmgB(YRD|3l-C5feyOfsp zO^^|yKiT@n(W5+_pnbN2x^A%9Psb{uN$R_n+&`pisyVM=&d#I37dy8$EeapnzSi8= z5Cx%}x=2xsbPrB!?x(WICkWKlkl78#UDJm%!aG($5% zJv4x)H3pkd4xz|eWy=}P=P;V^PRUNfQ5Bijb%7)*$5QPCQZ zgz=D+(_lLJW=MZ(c=$u=T=l2^9}k4zz`n;o|24Pmka&cSev~Fm*9{uCra8@b=Vzb} zX9@E>d9fK6qy3T71@sVCl57djfP<(I$C%G*0RZAvZwP8JU&BVrDVCCRvQW83lJGV%%Rmt&WzHkKk6AX~mpTQlMcXQs*WPu7ekwW#MQaR($x$%f#zWi-+c#%HeA$I$ZQ)F? zZMw{f?H7FOE?{ItYL@`sl)sr6-6g+-G`g;6Ac-=#T;a$!Ma0?T;xH2TQxCwMaDrdp z+jZWAHnP_{a9f|*n9QuH!^d;ggPgr%y`e5+ z_zN_S7x&1?Ql6Pn^b{U@HsP74m%mpD&iPb+ULU2&qP;N`jZ;{YF@Z z=;!4)39m+{s^B+t(YaJrO=u{Rqo)Xe$zoKW`Hj=2J$N|V%HM+!H2>scUj z>DjcGuhg6Nr5+OU8jsW(Xgqpk?mzL!+eE*c)clnEj;1!~u}zLmTY(gnw@;6E= zJ+|uckRJVZJC;^+w9Ru#EJj`}QTsSUADAT5PeW{?i<<1drs#)bbpID)%^)HARLRWC z@UtBCt5Ee;y>3A3W8^T3yHLXfO7upu%KWFvWt+?HZn{sx=+rWuaL>yfqI=7~qjlH5 zZn3el9ATLpfA@KEPYE1=fsgU`IVK}@WvK@6K>$D#zdd_m?*3q8 zQWcqjG3uYqR5aoG75`fdL@C3)iyhco-`&US!`?_#CE*vmk#%yosYI=%*|n79Z;ci3 zoABeACj#eds0x`=u(}D|mtR)P8n|solHH0UjB;s4G_od88|DzOK9ilWVE}ykfEhT(WqDQq_;e++^n3;LS#`q zAl0;nL!3oZ0E00Hb_^ZTz7v;8UkGsLWJWm$YOU9SJ2gKc?vzg$qa#q7JJ%F1RLCzW z1YA5NM(9_>0|4cEt+dxly;j?6wKr10-dIYNEr06S2G=I##*VB(Am{Qk@tnRV_^ zx_w7u0SwgQ{hHkrLy^C3h%I0Q{2MDYQh4}&&R@uD;E?g04{DG|o$tu;td~Y3x&0cd z%FzQ>-xUe-$z?7Yr96YnKj$A1-;y>QIkmTrm_|cT_|uroCW-2f*JphH#rN6p+rTd! z@h{n-auyUB`yKbvbgdT3Q_L^y9J4f&PfY|gPuyF77^k;OC*4d^xp~WL0GQS`?yFdeIKsFyyA^)!FXcV?Xf=MqzT;3?~II3AHYnfNCWu$ zgy<97i9%G3dNk{?z`!aEXc1SgBwR2yc$QN~bUU zswQvb?Y4IO`xsYA(QK@M9l*JaI`|Q6y9)U+pV_puWj=|ADuIgUN0oVzC5EcOz*Q}$ zd*h#AxaqGteXX}*DAUN<0PeX*UK?xazrw*c9=6F>@+St~+TYBosf|taR`k<4#`J#c zdVdwCmg+K^i26aN&|CQrxsh661F%?dEv2y~PLE3JQ-F`Ox*8bRJNfnZeF%t!d27a= zt&rJE!S@2EBj|6{cjigH3@^Om$85RV-r!&|59*&~%?fOT-N6Yn*&~)wkxd~c6|U zpX!9lE7kW#t(3_u+}mSvuc^C@RUqmCWV95+COn#X%Kuim785(9a9M3^iZpCie3)z^ z0OQ`uS5yK+C;C$eR5Nvs55|>)$c_Fzl|l+Ky(bKMJzvXrdL0WL<%DbP$fsA(CH!bX z&1eO16~qvh+sMm2Ym5tV^D2ogR`h8zM?MhuR&3>U&`^yi1E_7N*7&zTv*(BEJiatk z(Lh;hnp8Q6A)mOIuj2W?;NuLcP7ZExpG zZSQn2VQv53^G9p@GE%&E`-g|^*9&<1Anse&yIt;~Ye0h{`Ax*N_3= z1Fqq02m5)h0Ui>GqFLP+_Ur+pw1>K#G%l`1IZ{{1dnTt3kA{hE(0df z&r^ByYA7hysfuungz6L{L4(A*B}-u>_>I%JJ&3Gi2SDOcQz$guDS6F9j>|r|m%Qi~ zij#1k>YG>z9t9TFiIvqPb%=J!2Yn;;T z1SM~NzuN!K$(9LLbT=%i6se==>w->tfs_JtBK1YkiI7alR5<)j#(=M&DUq=^K4&7? z9PF$TUjb}J_YyfWMfLSr1ns$MCQi^_d4l{vxGy00SqQZJGKR zuo%#7gISD5k~$V6X)H#SzvCohG2S#5;|}nnDDH|GnLV7#1uVvwg)Lwx&emgTwVre1 zdS1~ePNJJC_0vj)t$D040*pg%yS1DZ#VW@=}*9ocC(B&vAg;W7PV;*HTG$e7#6Ft(+FW_M;ip|%E zd)<7&pCGFg-eSVmFmVs~bE6_hWah^I2@s|+sRk8`ayeI&3JeOnyB{zp6)-3<|57H+ z!7sJX1j4-tGtbi%jJVdcM-hHYiBo2^;ZayGsWDZwqrD#<<+^}JNit0Zz$+0Kz^i#K z+zTC^V61PEF7Eu#$F-kM)@y4c+{kD2CR<3UF+I;3|1vjj=@t;VRI;e~w}^phQ(`m5 zKmV3$UguhTwZ%6ps@YV>xn7_)cF5L~8^-Ltg4G8`(d|5b9mLbyfbEkP6(e{j?g==kAN8R(3zREdI0ces)#L}_FqynoU3A3{~uC9=P zV_{SOmG{)~-N42{@i^R#-~?fou90w<${omMw2}|x`?8}aAQ`Wdy(&~$qz?)dEtcW= zH#t{LcxP?O@i#=jA;1=u2#-T+(Eml&{|1mLimy@(&(Ez>RARrsiivQx=|dWDpM7cV zsR;dQj`X|9HEACxRp+AMZQTJw+S*KZFM4-uo_{2wFb#o<$K%*+h zTG>lC#o9|BFa88%GGcw`B3bDTB4u!}6Q0`*C5E(`Si0qPe1j}9s;{l#%$Jz~V}#%< zKky{dN%C237`bQUbm}(i;g(M_UJRwIkTvsU3-O|XNsSdTYjflog=kmkpBwB4Wa-w> zXed~wk~goAKlmY+zCC7s5G2QRCH zruOhMfK?I-6cH=b=l`}8@aIDf2yo7if*=(YFidwUsE}dW!m6PfxBCgGa|opM29Sr~ zejvjX?tdfic1^Ul_fbPO#S#zC6NG^`s8<@K>}QAx^Ud02L*kV`7>-ExRHFUgf;$JT z$D{L4U!%7<{%L_$UbP_#A?l;@Pl7Y!pN6yK0RuAd6wWgSY8=tL83!dh&Kgk-HCw#S zE=o27D6j>zdZYi(hHgR@>N*y?XzM!|BzWgfKk>L0`0l6S3rvDQmMxBlT9xAq7;6wa z&+=HvKJ|IlEF*j(fj6=gHCs9pv+MjCd7^ zjGKvO!}{`*&!9&}J@!P{lhyt`v_0RG>qonud>v3xPo8@0m^~@vpN}^GBZ7Wt(*MGK zG#;%V?-)_Jj0PDP3jDIi+f)ezl2#2b(~0xytL6<{_PTA%gov3 zb;*n#sh@MOKJ#XxD;+yrn|TRi4MY8ZWm%Y4$@)x>zr@HXqzW{=*8#eOA6Qx((*Dj+ zUMRRW!-4qxk_k2u^GDV2+mJc1Iy)5OGsO)>Zwc>`pf}mx&9-r9jTcSsN!F}x?XS)B zwf7xNyt!^z%bCeJy;y>|gA4dw8pp0=QKII-rK8)shbA%&=OE}_TAZj^x3naYsXecF zeQoTcL#C_IGcs@Ko_$5;*}BYg{gHCQhy8spPa*+j44Dqx?9gnbWJ~8CwBEWOkg5|g`Agb_T75V?^xMGCcGKA2bJmi zA^kr_?$9-RDCe>@{k2|0fB!$sT{UCQK`x>%TSEKlJFkdb&{g((k`9^PdCI@mb)NBy z8C}K4*LSv+)^$d_j&34qGst_AHII88Uu8&ZGiTLzBE|1oP+XsBE7g7W+WHzO^$tvZ z5<7ZeY&wi(*%SMr`u1t1#Wnr6NbEf4opH~0U2(Ym?V)X>xZ?AB9o{9!ZBDkoRqCz$ z7kv~VSvr9J7P7to zJ4e6OW@v=9e{|4>qqlz@xsPZ+U3a}}v)Hq&{n>Nfrqw|kvGvi_5IvaLJE42^tANw< z92Lz!qX;VZ48XY?ZH7&)J2QI(L|x^@Mcdk69)`4t{DP*=iJ9b_jd0(I%)|`vRP&Jc zHy^jS4aqr=0$9neDF$na&aw<}_f6H8$dvtU&Hdir3?*Trv$m#~Apqni6o=!m`~dwQ zyu8k(?^<3BnMd-dJKM~nw?(qsy2m29>2-!&Yo%oKP%JSG<#p5vyw3F&yo@~yTP@*! zJ%1aqFt&%*xOMaP18&{)w!ToXZYJSWY)PNny(;`}PS#AH*D`di z-6zW}PVkfV6VfM%Z+wqP zJ-xKOuZXaC*uHq?O^}r9V!raA{g)*(?<6~KE%iMS(Vbt3T$L^1EmwXne)n(}@4u{< z%yU;|yGja)+x&RMljlxPwiKggjM2C0DZZNRtC_C_d@Z)G#ZDg|)79^4E0*7b+7-Gb zKRu$!Gz^p(PL?XNR9lwnWaljuev1`;3x!`@kzD?C%PC?1X;rfGgZ{o^8o2!F);*TI zr%s&jke<3q{ou;BrNrN&6rHzJ>FZ>0OX*ctXa7i802eOAwNLux^_fG-%>HE8q{BV1 zTF9KXCT8m1x|BLf{q@JGx^Azd7XYl!tgG)lF4@^wojiBcMEc_`7yhm5TpCZjIjt0# z!7FM-XS^bLZb^E%;^!(`=S|7_nwuKEj#j0nS*Z)rKx^olbO%*7NL!2X74gm=$||~- zk2Lm5Stq_y=B%%*gDiL@*?vE2_*ZuG$3erD8~y<59C9za+Yo;bG*d&(yRE-7lX7OX zCE0c6bSwRgb_Y+LQ;R!Kv^=QUlyzQqkRlQJ)po^;CUnu9xzoCN2d$u+&2)2~*P;EuD1jT<3xclRz23UIoUS@}b|jJOGySDU zb-?tMmvahp<&0wL&-ag>m-%HuAA7Rzqtv!)x%tW!lj)2kV)e1#<+%2hwTlI4+Pc{JMcXauC2SswHBb;~@K z$7C&!F-)!-&tQFQ6><;OM>96kC#(FPy10Ja`uL@xe^yw*`uHAUM_C_*?G>5zzp#EM z6|^r-`;J_{BVGG!{g(J8`eG3ykz^h3XA3F7?}hDaaqSy4o)6y^PUrt{JeO1BdynTF zrRa3yS>j)=uUvbl4{UE@7R&(DNtIuhS(CT|smnH$KxbqgkfC5?-Ir(f0ZUuJJjsU6 z`(n$Ov--~AiOyD#$`BUY9(v;~|EJ>>oZH#pj-;t)nt5i+97Z}V!x!mfr2XBZmd_Xw zT2hJz+RoO`vg}@j5$^Nazi@&{5&RjQpGOt1gs=L}y3*f+q7YUfh3-!noCg`6_u5Zz z8D4c6Ua?k?L1@Tj;6Nu1jF6Wgu@H0+el66ubC)2ge4DpIbTk)jgjvs}p!NeG&!N0D zWS*L^q5ovo3%ChtI#RRRYyTh>(?jPvt?gwDC1C(*lQ(sJ&FU}36PeYSp2WBfaA3uW zuKL3>x@yneBZ?5>{M`C+yGc~j9C>h|jMkt@ zkAe9F)|9@wfi5__ss7Jc{-W10shHD;8)8!unVk(8H#svh&(&qNG;})AW_{;SNavWF zs=8&Gs{Jr|^_}O3M96zh6^fXQ!Dpz1H@F6~FN@$|`BenD8T$ds=Dhj!osvMVxPG__ zXS74}h^T>|%0L}!%zu#|^Y76v8}ncD(vW%k|B(0Yadu7j|9>u%WZH&(rb4G})Ra+^ z3MOr6rl>QSNwOzS7$g!Aq!k)!m1dV;@Y>Jk!QuLJ+IF~} z52|;A1`~2K%iSo};M_KzbpU(B+lM0@hUXo5*e0Hjp{;G`H|k?`H9|U zTQHYhHSD4MVW=IZu#DG--5>FJcYJ4ttx53vo8tF=+roDU;IaxC3gF<&0UDEC&w4fH zt!q{iFtF8lFPU>sk<&QjXbTSal&O*tI>0w_>#KM?jm1T7oyr7Gx%~`*`mo8_Wt}Dm z5nu6J5326twXLMhOs={*ya(T3`h%drh_3LL6prPOYxr7!>j)DzuaNsXAJ(d4t)X{bzLa!S;LITeONCD3TK8{!Hnj4@UXTDnGKMyzGR^ zJHZPtvIZ8CsQTYj`Laa$xl>(PqxP{@mUGb8d##_-@D`L!$d>hjy?;`Y`R{-A^%lKE z!3k`k6mG{=3iW@tYK$-I|6&#QBjx19U_hOC$FQjWk9aKt@;TvTvG^&K$vzSDg|QUN z(ODAVI12ARHJq?!Bs!I`83`ZPk${UQWcTVx=Vy!O=qC9`(f(=%j=#Vk7dJ(Od>RsnqoAbYgto#tp^lEHm6pFd zE?+cO;SqNBVPQPr=sB|VyQ_=e3Hio!pXa;9I9VL}Qy7#iHag!+j-zvRFLeITWP6U@ zV~yV5P0;%s3OPT<$p!rjZvYpuoH+QyD-1o*y_jyyac-Qh<7=Tm!C_*+CB!#P3AxEv z*oI8_Al!e8YgcqVR7hSk_wag-t#&%$K=^S1I$>|ov_UlO{DRXvq$T;Jzw%-m%2g2u zb0p-L>@MN{{8e>x-<*&=)w9n!-+YlpoISrlNi{e1x4cCyC)6baV3sxZ|-m&YHiE?~Sv&;lKeE^W=Br z%n>dGmRJqOUVzk51YDsbOTlPqS~G9cSkQz!{tff)Ucz_79XHEq-uQ9&xVUys7B^d! z31>Xjd64AROk95}8F}Fw;*w4{kjKu?I=&DcE!cvVqHBZ=jv;Jd%Nkj{Hf5=~;lBg( zV2dGlhg|U*YBP6b6Yj5b#XlF@$An*4{{O!}ep^p}e9y9v>yI0;X3P<>9(N9T90lAS zv7AG`53M+d{8XNZuXyd_c`%w?fPY+c$B*jnj-y3w+`kRn|GpIW=fp1gWd)afC+VRC z-n0Lg{qdjSMa2I2N`zMaI42^N_~VA_t+R#eufmh~=^qc*a>$pO8Iwc)W1hw^=(6~l z;d&Bn8?NR5^??G;uaoL<{Ju>d*R^H|9zO`EjK}}Qx6UQ+j_2rJ=i+!XG4AQrAFoRI z<5mAYe|!Uw%O7vF#ik=8e;ikwxCahq!X0OMHSTzAu-B&89fz~0RgW~!l}B#*D)`|? zn6KDJemKO#4|m?_UuZ0on}OwD5&Jjz;r}hgY2zJp2d@Wbcnr?F{vMbV732mf0vDIV z4S(3mMPB&VqH>Y@bd+mf{^D-k{O^&G|6OApcActn-qJ`-H`GW8TF=N+r^4$$rvIJ7 z|K^r%^S@K!{@wiVS%BNqe@gh@-!+&Qf}-C2?{UR^)6xGymy6!F?hr5WzU5!NV2)$D z7%##8uC=r_HN)q)-pvJO{|`CO{xSN6d|l@~rwFkdzLzUB?Qo+x3`%8mGrmYH^9=)pQgio!riTX`YgD2wrt?$J%Z2qjyJjJkn(W6`Lq9Kzk5>TcTd=!-);UG-JKRW z+4k=I?xvC?lY;{UXVHnRc@FRRE4MlCTh8~|66ZS=4(wKbBjvZ;S}dP0alTXGYS=|))ix|9^TRxLK#cgL0e>}+9}BuW4AUklXj<{>#yYO z>$XLa(V9(mGg`Y-GZsv?@Bt8ZF;TRV3Br~+TG@~y9GO3;gL%Tn{O}H26Lf`okZmZ( zn`ud_Mi$c8j2>=6KYH0`Ng4IY*PQ!h)1Q9sD8^D+TR*i?Gh!8G{g{tpRmsvhFBmEs zUZKX-4aO_B+9(*IlpH8&)oGfAA#sA1_>scD_MbfEXWu)D6aPxf=X}-n7*71EZ9kmx)oOOG1m46~v9n(5w=mVbuVHQN z1MHe!!x{ECmi(X6$p)|2)Zl|8tfWzfQr!R2coj>@IT<29vwv%+?N>bQLhVqV(m}Fz z)K7a=WwlpVwXCEqd$cq*o@l@Qv<~*bEY)W9<-l7p<0@@YPgP&Y?=c(5dRq@~DE}Y) zOj*UxwC7c_>?->Z99iMi(BG(#mc0zrC+o<4tMKiH!naS7zE0^+sFKF3*c^AvKY6d< zdm1jgko?Vs?*!PC$9bK$TE!<`CG=I4U0C;)_d{*v1Eps}-eVqCKmQ5hK;Fc^@^>A7 z|EANPybE`OE}(O%(8~X{s$4%)9;I^j&2f15{rcQ~ma47S;DYfZIbs7?-k^!`)nIRv z_P^?Twt3RIRWGnHcGXk*>DVYdK1lD`pnpuj-)B_ceLS$6eaeFnBO;_oao(rBRrhVQ z_JDz=rmg1U2zBdwHB|FHiT|L~_;QN97!^ys%&yAaDF$sHpwPbn>S0(EVZ2|mpM_}>zc~M!r)8W*QPxzzK69_jqssiCgl7wW7hJ%?(vd3n5IUjJ8<$tkv;f;ZI}q7 z4v{#1nX=b}kK;<1zJHW{wbx*R*xc#2E-eEw{k`@zxW@GDNcxZ(&F)?n8QXqdSZTPf z8mrmhU8I$;3bY*tRYnh2K|X#1)N>3jZ{tBKGlNP;PgaZx>H;5EOx1&6`ZRxiYN(L#sj3RGVak`;OuL$uX0PJD(bk*cf;=1yqKq5DcR*lc zc6s=s5LvU{mU?tv1i|j(lU-lEK^vxP9^zyYl0+|Y25}=P#v@=9lp{>9jqcjHzK)Ap zf5s=;z|LnYNTjs%l^xw7H8+Kw*)@NSQ~sfpfATX8-kSDm_$N7w1@FfB3z5CrdK6nv z?0(_%HMevoc2u!q9iF3&BsCo6pPI+mP&R9^VlwTQ@QYmTdJzyMY!>DAY$1tnI8dtQ zZ`w^!-gvP;-_JMu>>{oJS?BG#?0RiB?eJHpXGuo+YqClVH$B~1=8Bn9_?#owDrU`; z+0u3h-{*Bc8!yrvTI5$LDLsm$=5&em`m*8}+V{vh=KCqcdEAn_`uw3?b#Hh-)C7xv zB8z}(*82J3Ppf?aS-l?6Gizvm;$wQx)YR(Fv>GN9Qs>n8+6$s{*(a{#M>hojl4hvH zt}R%>Ha**CqwS7N#v@lcppV7&W3-e=KRg}33_#o=?SY=Of3o7V^)y+-Jx-@^;ZbKl zwRzeaI?CyK=w*SzDaR6Lo~W6g*eUO}_tI`u?NHi#tVM_}exR%lb~S>Y6?5>f?gvz89j7f;&mQTHvJw$Xhn^QesV z+OxTVZ!_z)tB0dz=@K<~sljW6G3^d@jjo~OlxNhaos>9jz2^P~uc=3~{P0k8Ed^8l zbTzG`3*8a;s$!zuV4-bK6Q8EV{Wf?Po(2U$xZ&aNYQ;LKAMRWR>pC`Z>V3kxo{qQM z0CeUa$A$qnTXQR#X98ifuO8^2#VKp`%lTo-IyIi^CUQ>PM9x>!p5BQ%-fs<3Zx-?3 zf<@E@Ad2hwTlBNaM`hVjP%J-*QuU?>MdY?myT$;>mYHngM#ZN@bRC7x)$Fp5LSu(J z4N6CQ?Z=)ryQBi~(kW54FmEm2H;T7$C>DJIi^x+#%A@GTLUop!sHNs_!nTQzex|H| z;M4|pbcjXND6$KI0#TQdgOkvX@c1|m5(~FZ8RN85u{qVk2X=u&PoASfsT^oe(e>d) zW<}9QEMyTT+P5%TGef$J(-LGz55|zvt!8?aN;lJSEa|VT11TRhxSyKCmg-J&-Y}*R zH9bXzso)JZy;bm8emPQA*QKIxS<0dwJdtU;MQW z=h7`bli68hw?w633476b^rC z5HRDZ!%oS!!|i?5IKGx7I}JarCowNW88Xt%{PNqL13%oFiq5T}*IMhvTxsxt-cUX*Z!)ssb+JRwZyX zhsAQL9?W7p+NHiYV~?x{;v^}TDY{35i)XxPZ^^jLp^C5!TkFQ-O&oCjXF8yNUSjhM z1K#fvAKNd(5lq7Zf9cl#>=^Lc4xvN7E<5{xy)T(zuS}?W(QDJer(C<-hau`&r|4|6 z_V-8`@f4jzTMWwN8!O5u414?V{6SUVudKpvf8_jASWA5h1mg4Ovhwe^6eHW{R{7{cv~(Z;E&@LKcfM9WYLYeh*#>>jlh4(IeV2DG zw0Yd@!*y%rSoTI;Q+EZw<|?> zf>Q!6h9Zd_bN|3UV#nNKQnNuEGGJLbdVqDhR4;|Qi`T9TrFlA8@^O3$>bdRShwE{M z_5!o1bRm^!vT-_pneREORxdN{rJW-z2OxZ~YDnS~OU$2I#dbsZKg{*hW!NZ*11$Fy&mGlI?{V*73NTK6V^BE{q9&Sq+8 zB@kzF-z%@b$7GKKOQU!tVZ{;XFz$g!`?X%SL&jB3+KgWLwk}eR)xqFHc^qiJL>nBf_X1 zpQxgmrHxvw4+=GtQ*ZwEB;CCUeXQ2kC6+ zb{x$ ziGJ=>h(5a+_$G3Ih8RiD)2i7_FnfyE_M!1y#V#-gXAR9lP-i#$b;?=ca=y|t=Sr9J zCBLP`-g4y4_$kJnmmWoD&(anLzx zabUhIO})4rm_e0FmwOj}-ijSoZh&P}tZ$;29h%AD;lfA2pbyNhc@>MB?93H4hS4~t zuA_B$u^IC=$gLXS-CRxhp-mB`MH3U-!PQUOF`+}MH+1l{Ve7LSx2!0e^X}6do*r6F zA?>u>_bKm^Xi=g>f7+uPI%uIf`Vv}h^D8#T($ci&smS!j(C8bupvN~_{Iu_DwB)|g zaWN5B-vZ2<-!T~u$KmQk>#eqg-;Dhp%je5J;a--Hd~reD`UpQ^(yz*WVxTXawaJ24Plg!WXbhvPyy9jJ2*);%`Q>)_rb%XR@bv~g7nqu8@9VVcLWl3 z=qRp2E64!y5WWp|ToXqno0w00du!B+>u>jVmeG!@+!We~N8%P~B>W|{O5;t@ev()I z_Cc3TQ7WaG6bQ}^-$zMy`T*Vwheu!dF5h;P!+F6i#Zs1*V#(-B5CKeR^q?)>*2rc6T_bH-}ol>-)y^U?7!W0 zDgN8v@B2vqt#!r1g!9%|J%2=pJhzq%UYj>{pzd9rF2loAlC~~$XdtYBBiGejF0YLb z5-!^#a@ks6UAPT@jgTGw+Vxlqo=#Sz@_0oGCst47@o+@~XH-w&cUtwd?Ko@ipe3BI z)@C4D3(P`sSkDD2*1m zXrK8w{#kQ(|4g14P6k+V-Wdo&?7O>Xq;euI!uFP{4-v` zbGXJiXU-m*I_T9uYnEy*@z2aX`yv>^hB>e6m-JcalI?jy)h?EQ=Iw`jb}d;2?`%<2 zF0y$yn|HQQw1l0SWqw*{&B4d7@q|E+owMoAIh#qBanAUip+ocFnr57{SzcRzXvn*q zvzgj#k8?H?=ZqW;708iu)-C7fE=SJU%(4>aY;SUk&Y6bkpU^q`9G~EvO^=+jOJYyy z68gqwcll;zRwVMx+SFj=n_WnmG&3LjrOUhvzhtE%*Gy~8V%N+{l}8TTdT>|p%yh6o zp}o!@#r~L4)8u$&cx5mCST$--_L|mBEMcP!jnVm(gEw^OIpHa7umdUKCJubXDf=F6 z@}cBgPMOiC`1!C~EwPdq%m#66LONs7?$>4t()dvT=n>R8Wogh9mcU#}cL$eYnL)66J;5{?n zQ8{Ty62oU>A8mN-qwNvEIrK*Jdgh|>i|@tnX`DYSrK_rGGk6yBZWUJXYei7P zns&1y2G%6WT20YME7UbDQJ28H%v^=YNh?%mrK0Kt_ru^J>7*GTMW7~9(fFt$^U|m& zSq1swQB+a%@14J<{pMSu^)Gg^p?2uEzRHO+g_GJi=7|m22aX%%UAP3%V9f{TlAl(+ z6Ol5uXyZEI9$2Cd#(kZ|FGK3HtBT2Xw%jWub85>?2umRDrx8F|bx_Yc=JD91lWW7bQ@I<0hx^a^G{0-SR;_E}Y28@^`CU8v zH1^J$e>~vcyiU$hjjZSm>M_*yT9(WTS#Si}o(-Ok)Ba&;?D>B-xR12378lgTMfQ7i z-jAtwxwq(IR6x}9?4Nh$Exu7`>nLkWD~#0EWB2!L>-&ke-iz8lPHn9)bL{kIp}ziB zpU#L^eeH35!1-Qyhyk~dZsX`otR6}SgFRMI)7jJ%jXy~zkot1)8PXX?)r1q2w^-$e zDE+PFQ95VrcLf7h>VypR?EjP>cPsK^s)QY@B6hUY0ykDCxRD%V#+sNJH*Sj=f441W zJa5~~cw&9w_N z{y2_j7MkmC&Bgq9QZLPYJJDRsj}z3~Qflr|-|d$O{Fonij_WgiJgAf(Ys6odEv2S; z;yL3h@m;mj7m#lJI9_?ik5x)PF;3@XI`lvF+jqK))7A#+$k(@7;r?K>6~4#$U9IPj z>gQdg_3BNz^DA|3-RDfbm~Kodjca+3a;F1LHAbm)N~uHZImjb_rfEiU=1DPDWtTS{ zI1l}I5aTQqqc+$}F*arA)wqlUaa0lQ&Vu_%?og zIk@g#u*9iTT2N@rcsU6sHJeC4Ikxexarg;EW*kxtFM5kEq1_@g4hB3iBdp1GDD9UO zFTQlKj@{twd{-s?4;ngPK5*FIyQqe591dF|9KI=QVc4?c7oQOh`On2A#bjr7hEB-O ztIC#DM>!5lLS$pE3}IivoGJ?knavrT-LV8`b;9xipAj+#mShtgnwXE63M=P5iYWEgs61| zr83#jE)7A(yXxJEFF(3NY{Pw1*teHl8kxa7lEZnbm-9E>jR^yuXG~~l!ZyAVjG?~5 zhsFB7T^{D@BBsZB>43OOOX0;{Q5WEd*j!6Fy^D)qMadTk!knB;74~mrY-X>>WtVyBYz3LTNjDH?Hf26yLE;}H#AyD_%$@1N;|w1l*Q z_!oeumc{%#LjYrEo=b?+b1|R=Q9`F_UR16f{2S{p1S|qj+RNJd0c`{rAT8YrrP>L=j`D3BWXj)VPy`bwSY|z^Vy5<9CcU530{<~R&5^^pt zz-NXBa?0?87c!bLKSTbQfqNBH?R z_z@o&ey-)T4bxi2PXmB8hU6axFims?AO5aL{{*BjlN|2PLJjeE>^5uTuJCfT@N#9L zodc~MSr+l_)dqgCg$N}-b?umfgwI4DHGjU^d62C(#qqWifnW?4tb}hyS^`=n;+tyT zd&NtFZ%$9}O#$BA`NE4T61=ZbO$?8Nnw+lSrQfP~u}cXBA0|1xnOckR7V$G8GK!9G z3hjN-+7sWXy>qP_#g-!c%ysPCpl^Nepy!9N!~NHsA$MXCm?Zi$n_*+TMe3HebhTYPeh_~eEH z9(JPM*gtwMm}B4;YmD%42KB~jFTp31Ne(B!Eq+vewI=`PChHSExDmn#CJH)T7JMmD zUqX!zQhkG>`fylO-)!nLg9)A93?BPKf&q&4J#@2@BQ;vomwzHT9H4zz@p$Ro;Oo>7 zzM(b><169-?*?Ba-R#S9Yd_Mkn}Ysvx?i%NOZjG(p1(c%&Ldx`Rl!=4!d+4O?schN z4?&uuI#JEPjXpB1{d=+uKI$XyC&|2X$t%EFnZ^CLGG|0(G#;=z?qG9Tv7D6|pR8vF zN~1vXs&gU}WTJK<gU^NY;haL*w--cZOF9uOM@m3Oauz8sDCTJUMB#;9n}3Xaa`U9!CQkNYFw-yRembu1%EL z@(pj%_4sAjr$PYmS3t!3ItvIg%vF4d@_XL<17wZ?k46lpa9&}Coz-2$rOSh_VM?N* zBxj=+hLR4wHk1^yWl#P1-4nM%< zmA!f8&4dGb9Zl?J8AUgfvH1|Jq_xgK2v8w$0#sAp8sI9dR)iEpQ%sOnP(Gd7p`Kn{h zooh65?UR}xkt^2YA{Y0?5ce++T8%*>r?fZ}9V!Fs!WQ+6&yDf((}Wr*{$vsdm|9V* zyO2B?;f10!tN4%6*P=fKA$iHd52fQK8wlJI#U;9jh1#D&p@c76?-n8ojt$|-0ejDj z6p@X)Z$T^fGVUI0;^8`*7^!~R>^W_J=9K2(WxV$Kn zx60rJOVIc{62M1ENXVPkIkOiU@;xw5VRpvo-$qmLRFJx*?!6#&>lhjH6`_0$5?INj z+GXt4NZYzW?{N&A9E}NCQTF{9*mSr5SVXd{AHrQDcC_{1M}M=I=^q8 zvBuL8ojXa`KAn&0rd_k{y+G$Wf(Rvajz{YoVWEi5nk!GB4*g@J^^bu%4cI|lL4Eat zy7>xl^hBLe@#<|Tv9>i9`Y7sBA~1c>$qvZVBKjt*s>bh5?I-9cYY>}MNFLq~0gCR7|Vc~~~!*I3)~b%uKQQ+}fS z81bx*btDI~!FP@7@-@(LMYpk!=+SdDA8MR98b?awQWI5Isnv&8dNcSOB?@k6(L24> zCA;BWv6-K#zT+Y{Q9*P3zjHvsr8hbxU;{Cmq=c8zkI0ayz^tf%%bWxf@NeC?bqpRf zeJSdzk(QuwYsANUkgyFtK2`c^-bws|>I0sx2FvhH0q1we266=XYWS%=}*GP!Ozj#!H=pF%!7cr6hGv21=mSTBwGYg{FTH{00y^-pLhiOb0c(eq$RuuBVAiDi^sIsRc-BK;*r45%fWz~#6JoA zyp2SfMT+tBJPF&t&uoBvbl&Ow!as*aqIlNnk)Oj#yqJF!X0joIWd1o2uB{#^hbcF< z7n*)RkF@+V)jdEoos4iX^)oI=;_1iuc!sB+@VoY`A1X#vXj6SEw0TC{W}&r+od#H& zn9=S*qF4DM9BOSA?Z&&oV~Apj-B>|_8}CQ_bUO*#z}K;W`RF`$C#X}(PZlj|1~!)*9%_~izmQ4`4#ys`!|@x#%YP;0EA|+l3yw9)_mHo*N$zFj z|1`*v0jz)H~`GY|c4@EOw0FTPQE7N4+j z%v-;X(ld&a6xZ9w{`8}9{!YE+%Y5Aym2l=u5srjyySyaxTRL+m(jVrp>-+DQ6zOM? z4_nXS2AXC=Cq!k6HuQifJTp8PL0&i_YIbAWu)_4fIOfi$b~I+k zjqy0y9bTJuZ*tMcp}Q-82kvCe zjZJ4eGWqIs)6y6EYp}J%SWfE=#N~`D-PIr8wi-Jt`8+p|3uNXsdu^}T8%OOCTojo; z`0o$wTDj}!IygU_pR!iflM4R}WSqR%4W3S`ZZPjC;jgj?47EYY#)R`Me-(ovm(^$F zp+H4EQn3MUCcDy&m~I)Y5eJuLb)WQ}W*I#vz1vPCgAd~64O|_y+-rLs4XqnRcG6`c zw(pUhO!*;4f}N$ghW9i1;p^&ilSh%5$&Xyx&OX55EFCd*w<$ORwAm<2>%W+B4bqk% zC&UsegZ)TI=-evw646Uk-~--8k2>Q(u;D)Uge`hlE~UBAkC7I3@B`&8trF!Lyi&^b z6XibqPbq~G%IP+f-HdWy{+v;ct9|Bin@NMc6)D$nougbQO1K;4u7`55ed&dM(JGW~ zvkue&WwS0MmBdD%-cq&>I)#95pnpX zj&c{&=f;g<)p*;!Me4oviR1p=2dI~UnLH@(e=05yg zL!tfdpS?#X$8R<&(jU(c?GDUxuH8zO8Ti@?s}DVya0($l!VPLlx7GaMYP1aD2KHR; zh!{DHH%1b(O!CM+$jW$00LNWMa)XdaIA{g^Abp zB6>iBy{hhUJVW=pk+9u->H^e-W0LX@hSh5iec9 zRFLpJq^Y(MfZTN(eA2!?iu|$ccT=$R=hE~Fb|C8QY@(9iD&xm}NT9qaH@T*1@5xog z>l6K@@AdQFW#G?<-e31hmQLupmCNI8$3yn4kz};*`*}pBw~#UPPx?2D35)Pv2t;gT z6o3jCu>QmzJTM7Cj(Ff~q<&jG5XB>O=4nqRh=Uc~xj^YeC3<6nqfsiovVp}WW|G^D z4J>`WzKX;#0(ytu*`NSY5!#Z7H2l$>5gfE|M}ZTvP!DNoETOK*#xPFBKYc~Vm1$a{G8p_H z1x3>ry!NhWs$zrkl00QCCsUa4Ah}|u*uhMkAqJ_9GI}VPS`<9zVZ&=uG6(f)Yy554 znlH;jC03weYp#ic@$%K>Pr`Qvko0WEPmspOPqqfuj8x3Z3Hk4uJ85Q-Vg(KJDq4Yj$rOagBaZIL@1>9q=JJs zNrve(*9#d8#QS#K1{rD??iR@K&}EQeR}Fw;|5aJO6z}b1#dgqZ{~hDb!uS>&@osRL zKK+#iKO{%`u2Ja=qVK|ElxO2{tMA0PJ~KDJws1l8oz>T%`hG%kaU5;+?H$*bQQl%S z?!o2L3j8AFWAeHhc|8YtoksD9UA~OP*%rGve+$2$swf2#%cOG{kpPSrrJx(Lu;~Hk za5qg7!Yo{U+RM`BtD#aiW-&pTYs})D^a$cj1!nm-JBLre4ZU!R;AnpyIz?396x@Aj z6gBy~5j-tJvHz5*-SLfluEHL*fba*33UcOl6lBEMy<)Dh@cWwz_SFVW4@g=xoAd<0 zhAUjKsZHirbu$ot-$Ym?o}#{Rl~ua1n189ssq*_P|MjB%w$JZWuu-{tkr58zr>GBf z-)4Bn=7L<>`>U@P-krYK6tw<0;+&h+R!O`KIcEKPoIkrXU+CQwT=64Aee9mq25pug z)SJIn70f0f;a_Ua6{r{HoqzcPvQzZe<|C3t|MGV*rUk6{NSHU0V(Nl@0m_zr!FjaR z6`cN{`8%i-b8NbTX(WV8=zgic^I|^scMQDR;BO>2eISJc?_s|k}-!B>beg$}A z|I)#0fn@V8tAZbZ3HdoyW~>eDJO1kY9JE>NU0zF4_?9CjdN%UY9z?#4qn)2t8~lX? z2Y1BcH<7S?7GLV7!3r$?56AL_VcTu=EcVlSLjQ9VbT{bNJlO;KKa%8>;-Y)tSOa-e z;2E%lOq^3$8+^Ffpf3vOYb0zR`d8c}RRQ`gaGey;Y4olb+)VJwb3f_ED;2U2Tnc$^Ma4s$&(fkq9M~{-E7QzkEWu^M62*)i^sUjR^ zGrdy%=is;?D(m2|a$P0;%LK;@jyGMR)8CQlxgslkiA5rP(nf-jyP*F_*q%OVBn(p2 ze`8F$#KE-BxL{hr=v-aYXVwP$KV17o{aa=UFi_n7TNiJ?{T)i$PnEPk8ksBAXVe?9 zaaC}mx@riP)-|4eX?3I#V)^O{9wjOKn=^glV-?&Ke1Hgmx1vUP)e;1Eq|crrVfz~4 zo5W(!00oUO%;~p+K0C0iyFN=o|3Ivb&1M+%TOaEI{fi`pm!OR$Fa8lpd9nD)bzs+t zM>qsgE%GD2!hrS7J<3lq^GFormGDUMdV+r=eet|LvcY*lU!-Xy7Upn+@RwHqmu7cvaSqq?$H99HdMy$ z?~$mgTXqOUqB`$XUB5_Gv>~(-iNL7h( zs0xmvY&Sc*c!l^9uc>Gp&Lt^)5LQSS>~0mkf_m*%(dQoRQBfI5VYyia^EIv5kQ$>vFaHT!{_Qbr=*@2OEK#-RYCDB#u{`} ze0Q&lFl)-dw8?Ks3J-^m6147C(RXRITSfo1tVczck`(>~#6@1P0aLXu!fr&h(1 zZQde~Y@y?aW^ zP9!sls|o0hB;cp>p1_OiY=OGJkF+gr`WxYMXqH4DyA{2jX04AZ`cEhlOrkS|`EUQu zn||Z|)lW_w_EwY0&13p`zf5+sKg=E0>-}!2f9E9kZvaQ1d2Kqr!_U6KJ@bC{Eq_>; zse8w3+e?Z0ksRH-%DZqG!$do|@N1-UY4m5AAm>P(uKK?)cWpqoHf3xye`*(YSr{1Cs=dBDH+?Ca{9UP~wqw863;SC#F?H@QV?X}$ia^SlYv|4-p z+yP{dOX*!R;~VV#a4qLPp^En>?R~Ny$JMy9H9Q|)>&n&gJbBc-Ix22%`PRwanJg{` zy-@ZmewHJITgUkz1V{?Uyv%75x;Z4+*>f77%<_qO&2x~?=ls^y>EOO|Kj`Ybzp%db z%(A#jmRn?*5M=!Y5Z83(g7cwf(N!t%xAx2w&!hhLpJ!N4T#wVJ%c9c^v+gXz(|bFM zdRO`)PO-D7XKwc_YKIU1E^NB~op0;*bRPAm#K4$Y7`XTIsJ9~Ay`N{e<9Si@oox|5 zyPZe?Rl{oLfV zn6cciTgG|dQYyK_r*O0pCwcgFhfi-Q^RtIfhcg>$_&Jnl*|Y{2ciN%i)}u!8bd>mX zskdlfzU65d=MnS7EgZbuuX6$V+(W3n4`TI3z4srh#&~yZY0AEHSbpR?%Jc7u^`bv) ztsPeG{caVPa?NfH`i{>J{~Bv+(wvvFjDwUkt1Y3Q(D&VFT%8R7VgeA)o}L-D`Asgq zDMQFOe5&f?(cUg)HQq3NbKq>2f~=t}L}i+`0Vo*Ux{~4bSY$?L+fA>`aBoxk*zFV|;>RO>pQML$(V{SsklLiU}EH}3U}H+D<- zkXwIkjG13BMZL&m+4lVDv$4!|A7pZ6)n5CH6pD_ciTwAD@Vjt#As$%2L0m>dUW@NZ>_I;ZO-cYdt|fh zj^IVQ=~Fy5eZ*6CzLT$`?O~tbFw}l{59JE@qZ*cLVgc4dtMzjSS{G+o>U_@l7vsmN z^GW?h^+Nj^J^v|+{xE)ZYb-3yJ@Q>!^z?-7ay!cl8y#f2dxfc7UdXzw#yA6Q8>#Q9dpZ_Cv!O zuNfU|pSs)tpr2klEI<4; zWrR=aqs4-BEwBE8ZPn}fD^T2%{Lz%3|AS=*BU_{?e&NdUjmrANxO8A^KfnDcuk8ii zO;tR_!=I)1Sd+-2^GbBdGQ22s$-r!ft|2tu-xcmWfGT*AmT`SE>Qm-V;Aiu4k4#+8Hbbcdek zfW8{MMk)NH#L(%rJdCwM>-CgU+D4vqD@Z!vq24k#>&Y^?eDQv-eU^xCLVdECVKy00 zfZyy2R8d;8pHPlxi_#aY*h*?o{n)5^K~CP+-cFhs(DE%^BLdC2F9M!o-Y!gIxfer8 z^yr7wA)0!N-sWip7crqn?>5rjD`HL{+Jp^)#$jxi3qE<#_AJ$sIUNmT_Gjo&z673!=j=mRA1G*S_fX0Kvj=YzEw)8bAquk%WZh)k`oVmk-ywN(>|&C}ty)XBbg#*_N#IN3XGxi<_;v~~{W!aK99dUaYr5>uWD zzE*M**C>0A3TnU`Kf`BxH}9b#Iz7{_6HCRg`}`UZcPK%{z znE75s`^7xEirR0r)bCD!TYSIauHXzZzf6qNM3=k4)QAU2_SH(%SlNB zvgCVefCGK<1^V#^Pg8Wlv_{e%_Dw(f$!@;UVv=vT*z&q9eVaEj#|vs zsn_uT*5L0jKN$k6PgCoweP`d3$=)J}a5Hd`inoG9qz%_$Mcwo2@SVc*^013%^oO#R z7j3-JA5;D>MwC)@?*<3X{-A4f>#3=}xrP+J*`Q!`kx@Bej{ zeP7P^<)z<`IyQW$#CsJV8~;tt%y-PG5prGUH;xzthb!^`g3c%=GCP#A_Z1=`3nKfIxVi6` zRBlWP^*cB>W|owKTmY`I&g4%o@7&Ai=C>};<>XK6E6CQ=x}tB;hu$RRDxqeUtk13tPl)t`rl$Iw zsY0wnEesC5_GPq1(qPypdE8w=zm4cv)WA^tyaA8C2^6^n^R>tVK&pH$Y_9Tl8E~1P z%ca@Kw8iIMiyHMn&O)ss%MAd?qb^Wr)9oaG7IRaQUUaPumy;qTCwQF4Jm-My>fmRm zJ23TG%MK-9f$HcS_qm3h%^Uycj=RZ2XU= zTlogPLd*HtLE(*vZvh`xGB@Bl@{P@1nV{W7{Mlo}!8A}52sa*Uf25@fMx65*Kl_al z9Zr5)-|IK$Ek8Gq20GCMpm4yAYBM{C62%IfKAt$q&!0Kp+9RfN3YI`ze?7SLbFId> z2ez)|t!0Ft%XJ9Fxhg+@1VNm&EeFsI1Q}~;C4+yRra<0CSzI6yS1V5QVo7SFD!yw8 z6$J+w&)5;JswB@Lw%N-}ymDzca5m~bdztH2t2MNY z4#bF~KW{Z|}pnp?qSKmyQc%D?@W$RU58ZTCbj(mGxW}SDcFjI%*4wTi*<*u>%FB38{ z+%7U~jZf4^N_N<4pNi*BH1itVkN7ra2`WUuXKQ#e*c|gXFbmA%TnKFL{n*o`UPY_G z3U?6fT9?+;y0YJ)`Nn-b80p?kp3q^yRYPP-zbW zHz-Lo;MNEsx(#hz!2TAFGbjQx>`e?rx%Z>ztxKDN-Av)$bq%jU0~{pfMJ0Es%doZ5 zGufNg@P|9t`y8G>XMf%u-kl^AI`asfSw?5XKuzpu=$8E?*~{a;u0#zM^b5DMu@qk$ zahmdz%l%<*Hr2f|dk=wm-jSLxJqJY>&I3F5R#J8Sy;7)lm))U2!&TyxtM`XJ5lmD$ zg+QB@ZaTPSSTOMv*tLB(5>Pq>Vf%y{;Cb!C6Dd_Z2Y1a-!JS<)vA?b1r+9|p`^^L) z`Garz2Il6cCl{0_+!$Q1EOhGm;IF8=jDFIWM=dfgG39L?Enn=M3+hb1M`7~(+_@0y z8E60OMN4@7>~b00zXNI}TV1^hqT)}Z*hp;lvCsK)eI+u0ekSmc|IH5Io)wqF-D6wUX2(# zE3~rnQEB9_L%>MC7&dAWVHCqeZXEvDoZZBu#hT`v1t0|13-MkdJ%4_s_1VudBHJ6? z!*_@K*%xtK(ow%(4|e)a4CF0wzyB+E2*^9HF2GJa|AZc}?-|UL@hk0iC*59mQrxBG zoqtisr(+w9zLR7DvLA@Z-MWPtT*eNqQZZZvI>Y{Nm% zGIm=S)Bw3of2du`yieyf;@>8Yo!%NeWSG1A8j|(NH}Dyqdc0bIWb)I?Gj-3;{vv$f=MDs> z^5AA_BJHeAe$GA%ey*xZd0>Hf0H#hmGZD@NsyO~%$_!sZ$vI|_5X8*zOfV1@HL4S4 zGAGRh!E4hA>%pC9i10j~3ViWw@ZxbFbOj4xj^J47f}YH<0u+h%o%bgdMC9+o4=#k* z8$bL${Ahj{3x%5)4M^ue!}J3CHewkE~pGfCN*pjo% zIE|W0^vAKP8rcs<18u1y@!D-(L&_w(DbgjwDK7n0sz)A23Bf04I%8VYBlke{h)pg{ zvWa*m_&!R;ZbY#9N>xUcGh_AB>W}N%m|&xGkAZ_K#Y=Aq?$VEBbf!U-h>c9X&E6FJ ziT%18QRhzJKm3pViANztBr2e@=ug}=HMXk>e_}3B{-663^I?|+PEMK9opAUQe?tb< zcg7gbpLibK68jVPR>D+feX+M^JumrLd;_fR=b0G5+7gLcY+=6v>q%9LMfzM24A)iG zn2;Y6@)q}7kLX(5j~~)TJc7fuI$g`Bh+CSe28;Mr@ytYW+UNfimsZY1VOIA}MdM31 zjLbU+J!>NY^2#Nst6LYVKK`AYz3fW$!2Y1^-L=40H+F zycKMgw6-4DXU?baFpdrXA#<65c!>yF9DlR&9i`=OQ~9APUsx^Z;tLS*!Hp&H2xfPQ z1a-9Rh?tgJ1jWyOsDKR3K8XkwYp`Cmo?qFgWd~ma4($NWKGC(YIe0`Z89el-Zq@7q z2=L?%A4O379ih)3wwcGJtik|={rKZEp*lXrLo<{<66Zb50C#k40y&a%wP&Mae7|x6 zBl{5;Y54-tkd}j6-|Ihz(;aMtuw$?sChr`y*o8=-0=xndcpvbyXJOdvphB9-xu4tj z-uZwP9BK;S%rQVkV`okPDqm1d0;=XJ9*(Q!2Ju=RPD*kqPO|Zbnx_Y){XeNJZ zxm}&^)_HZUW!167(yKU>r6D`tWZ2W9RX;nlI(UB?jYQ*|C~jz@-a?3Q&S((Le0daE z2Al@5mM;2!Hu%wG>nt(OU?*(5#x0-GUyYj!HMEQf#?lqTBy$a))k9NuMdu&m6x?q& z(7M+R+&cfHy1#~-83q5eRJdzxXMXX=#^)HI=#I}32BGlztEc~c_&gTc__q1i@Od+~ zDelfE$LF_>F5q*yDgvK(S%uy3xdp1l*!#Hnd=87y4WE%caI4ZX`7@`-ESmin7>7~) z5=P~MtKe0{OTnz$hzTu`CSBB;<=3Xo6&EHB%(N)0W4yN0DWd2=-6Zvd9w^?`y4CY8 zdV()_>g%=tk*9j^jv>_-vjEYSr7@aa#pQp6FwJGAw(HPcgl@E~s%?YXt9#FDlO{)M z`GV+_70X^GWsf>931d!uTphs7-=ak*b->OkG#YfM7BAg!#jN=BrKJk|9kr`{M|9h$RS!>`BX%?0AnUZP?x1R>tz+V%gWbapL~ zpxNWMEGzTcZ{!`t@I~(qaC~va4gI`!H$2Lz`e;)-^Q^@4E>laZ%Cs~9l!XuCrp5E6 zBvkqNAFz;btKa(aRzKS}%YL_sgLssps_ z9K%RxKhmpPcGjlOi`_WGyMy{JW^zU={ai>cJcmy4UX`t=r{edi_k9Rk05Uw9{Ce+C z69;{GGI<}CBv0VmEovICFdQFziUj7oSufjKlMnnLG5+@?gcXCOyc5d2_(o_Oaw)oYeZq zl80L+etdcOPqSwxiaq6FhLprd%7akgRpiv0{|$LC4BMg%tvirBsHOiK@*qgIArA#0 zdy$7?-Ztc6*WHna8(_fTcA1)<@-URVp7Nj-uk}Z3ZEMLq$IMBL4bFQ^juDC;dojPn zUd$ZPp+&I45=2ijGJHV6W-O+ER9anaiz*~7&3^W#8%XuDW!0iB&e^qB@;*U#2R~jW zg1;NQewY&-bYo;nbnl@vC1>%3DcJ=}(i~Y5Ke8mZs@}Yt-jL+1QpeL?+a~dmEXi0* z6-xIswxs25?=UEA7R0N#X^Gw>IZvr2GcmL_+<5oEJ@l~k{yr-^`_S5NY#%j zyKQ)9>%gX9pB^39JMKW+ebgqvg{pP{Qe!ny57r8b{AC;Ury%=PUfXeT4_@Vg9<)@~ zpcbuI)@GKxird%gBlTcH)PpM7!rH=YZPbG**Mmcka)LNBVGA!|mH}JH%yn|+>9%Ip zix{+Lk}o@`@L*&wpv0s;kfK)UUo&g95I8U_xO$q%#y~EX*>xB-g#cAqMq>tYOK{W? z4i2mXvvF=9`=NhVhB*YkWfXxWU=u?}K);$6l*~IcAgd zJj6D+`QlwtyG>C++^1<923GwKkd(ZPYru zFByS9jGNGNbSKht>P_56Rrn8UVL{Xn*a0S0&Ag`Mw(7A`MCKO2aRaT9#kveBD~uGT_`x^ zUo>Bg2QG5BNpi;oq}MvX14*$t)$n~jqg-=#m&orJh~H5pM0l3VuPyaU3P=p^Aiv<3 z48$)CvQVP&-!@!xe}$-O+Q}=3JdzV%2*uzY`hui=PLB`HIypR&xbWRiT()vETxdt zZsKsX+Zz2T=x4NBPyGzdY=S!y3-oh`K1%9m)B#tzY(u-9j{bE*jt)wj%$Mlryx+Y8 z+t<&rg^~Qw1M^i2`Z?X3euf*JOPjR`c=YqexCgO*PD?*;%fj4bjFDs-uf2EuTyf~f z(9bfJ+tSaQ;R}(e7X`Z|xSiU?yr!S4dehIhMW551qJDPj{q>+F`FXlG{VZK8#z5D0 z@TXMQZbLt#G$X9uwK4i8UC`+T{Y=fDL_gP-=x5_q^fQ%2`dQEwb!S391I@?P&&?(J z*|-V)Oa+mCR&U}q(jTRtoBz%F8E58yML*X%{VY8!tQPchv(wK=idzg4>F3%~{aheK zte=}p^>YD|Z9@w;v4=MB4{pJ$8{`}fe#Pme`EPl2~f^t0x#T=ekOa_^3D zr?7D0bGe3EMKE1#VR>ObFO$FiN^s%bagqK|@EE`K{B)ldj%Yn|7O{xnR5Vz2rPQ@1 zxS7CCvxdmQeESkoSY6zDjpYpD(L5af=Hyy;HK*zq4ad9YJV4;gMQLLjjSExqEze>>Dhnpj)s}E zd3i?y3&f*7y8jU{Ui+m6#s6Xd0qx`WAERsi-+kNtr@n9)IHTJe8VUTy^X;*^U|2Vl$JoO*e+$Y27m_=U^ zOZ}Q|(^LsdwWN)mTIym}x&_gQn%dJfRmx9MT}L|<;;%bA%)!bmz;tj7-yoqOj>vUw zJa$9}D;evLt3TeYtIXv8NI;Xy9-cM-$~RLmh(-#K2&|pK5L7#aQhaGh67gz=es}i-JkFCXR-7DED~FPbatsk zEtCK5DY3e#tY5?hmANyU*^%qDy=5JqM5jN?Im^RViIH}K46JKDg>M&4U;_Ny<_Yax zUYiz|Hsw4&_bI<_o!71@A5Nz}CA+MC(Jq`MOMQ>Dob2bm=I0LZ>mKsjUr^Qg@uw^z zq<1#CT`ivzt#D+@{p=7f__bY`x}(j{oi$yn%Xv6^N}+&7{-Lt+_08(h)2WfIVvG8+ zDcnBzEQi5Oapjs;kBhSF7vba2arrFxit_!{V+#4m1$zY5B0HnmH>b7HTIy3MP*#l! zs*G8R%g}5-ni+>jI#(`6k9V!>{6#^YH97WpcnXs4;+PKqksKez1~_v8bGyy(3o|@! zGth_*KeuWkfDCpN`Rh#XC>T3(HRjys-*r>FSOg}%yZ7nf<;g&{L zDKw)!EBs7n+IGMUKG4goiy$DTDLW?3UuOFzK)*3Qqu&#m?46;z63;xf${WU}7oKKQ zF~Ja?7Nosl=TuKuIVzo@Gmp+Zb-Fj~W&^>y#xS zO7XtTZ#}1aCSW&KPk~qI*O>Mf=A1#GWhlL%+jyclMME#D1z#d{zjiuZ3+J|Taz?M5Tj?UzIM&S;{ODBo!l zh!SC4yMn7Qol?Eb;6iIMo7gHPh?{b57o1MNaXY{Cbh&N+10KEn(Dv^E*@%w%MPoEr z559N^o0Dl+de|Cv#=T{%8ut6Ne5=s#m!pO!L=F3Da4Vm=&K9d&^DPB=-T1#A+|)v$ zp`B;Ad64}6*Ouv|8aOm1-d1pJzyZtp^~s7%Ui-ns611U|I;e!YwPHp}yb@b>GUdFs zBhY(n2N=(v#V{*_M}{|H7a!>S;+MPDsy*5r%Jw+LueA|Wi;kDR+&`H14UJL8j2a`Q zwT@5xyu%d>+t~-Ds%pY7L7FHo#awoM-OAYqGlM(otUMy7fQzqH5R&CA_#Fb%B6jN<(l zwn^+{?>kE~eK%&Zqx|fA?qYmA&8+VfwpumUu-e4<^0&kc>e(#IhLur`b*EJi_1dl_ zNo?=PQUzIlyi{aa2R+o2n#SzwkY^A?>Lz&IMXP0WFfFa_I|eS!v;-KhWe4__^AI^V zkRv_nGdp~>rhdOcha-B$`gMYBHjdoL3tn~daS9Y^sh$tsxvunw)eAvcBUqhPqI}no zit?5HyPg1aCb#F34k^(wed==~tEFXUk*s&7TT!!SOEKLAi+Cayk?Blkk`PJRr%yAD z{^~u{ATPJ!{UcB%BD5fhMNXGQOa@e)?F&dMGh6|JHuwhgz2aHvl3Xq!iI zKeuCSbcYBoZXwRkJw*&eR3p9UTYm2Q_C<|-@ww73UM(wpq4H@N$0mwrsz>?l_i;A{ zxbSmmcw0{2zFNie^`}TmD7DJEo?V@2#h2YYCIQngOIw(t7Jgv%et^^lW}djXpJNpT zAL0Vcj}5M+uTD<~0+9nFKSjO*ldjt+J&JvUiPFukbj>zO*OZn{yV8~0D2@G3)?ND% zPKKbZQkj-&>(>zJ>8N8(*rx=TDK)%ZcVi5DZeC*{ac$=T=)=bBD-uUPU-p3x{<->GSv5NBtQF`|$f^nKMP%dBj8YZo?TciZ z7E}cfqkIeNL~;vif_q#-jmkCU##T+pojj^3cUX;V$(O_h)BSuyCG0*XcXVb#?s#LJ zv~o`_*Y6prjMvCc;EHKn_{#g-O14aoozjQ|*79`NbSlGK{=#k$sL#z!*XNFIY`hp9 zT%SA7=errzm1A;eOl!P2Q$3H@)2gTNYrDxZ)idGPW@;M6tO?WZCr*Jm27?LiZF!k{ zTiCy*US=Drq=v7g2jtJ<@EP)7;&zaCgN^DPG-96OpB7e8{d1t&2(*XW|nGrDcE!bvhBWU(rt&3w=j;BM;wCY-ZXH<`Bl>1PV^-^@lF1YxP}Nvxe#Noy^cNRS|E*<8UV|vJUt` z4MsY%N(H576Hb;bF}Prb6ty8W;c2<`qx)a@eqX0@Q%$)uIJUd28WoC~ZlA@uOl)DE z%^$P>>%to`QRaz<^JT1XHr9pX3s0Z5r-!joPjVW+%l);U65xN<&%1TiiPdoJudxlH zJLD~XaAjT59>tppJ3kd$s2h{j-W9E=aaifRxrma^>txAJ#~Ujywkxc+VTvRBZvD|_ z)W&32YMH@bHDp_^H8(!|&(^ww(sFRTVThKNTH=YoSi$@77?YnmQb2p4@(o>fs=%Ju zf$RpaMulrA4Erw)`*}4s+aCCA^7ohiqb$?+pI&NrFZDeR3R*_^sbjjAI%~7{*%jVr zAM~dF9be%Z@3W74Q`hr!t2gy&{&aX#H}GezH}x6*Z1ko+$)C-B-Tia^xMg|&mUFXD zwm$bhdv4h(KOuk6MSi|SH@c!=HAp{uXcz)s&2#VZzywc z>#}}i)n9R3l!CB6aBZ0Y|fEulw3qEr(rNxw!^qk%uu^AOsWE*fvQ z_LC{qluw_c*mYU;qSS<(PdU7gtw7V1tIOm**Q7R^teo>c%F~N#C*+Qx9Gm;Dv~q>e z!U0xp^eMJeOhzh|{m_a}r8sh+)6kl7UymaZds^{^Qy8>Swvv)fIo9zCP#5rF{NZ92 zy04HW&$vHGOnOlr-imoLY{T*NEkm$wiBt5>-|^;yx9rTEyxp=T3aHiReWU z7^_uvXdgBes;;VT05z>{m<|JQAcpFyR9!Ci0!q%Nu8cm{{Ei@GA(mCcUqJ(K9TXA z2l`{_{aw181OI~MGLC-Tvn>QGlKbq#r$@x=r!3y|%f+Sn%NMikH7k25>-R{wr-Bby z4ouxY8J%UatLw3ZTBavblxH}Wsp}CDpghc3qTONhgfdfIkK1)v%1wp)TP|a}jBdR( zg8|i4L}`#8Rw5yMfDyGgi~;3pu?R8{GmQ$9p)X+nm|;S~^T`G6@%XKN68g0&+V?l& zDJ_CVzi#15%ul3W$8An1*g?{-PwL>Kr1w0jh7kU9jH2W} zkNvuo=or(P@}I>Z=cd>!c1(~H9ap;`JBgLS*)E|HiaO6GyL8cKhzO-^lqmjiqeO*R zSKW$FW&4gMa9U?HqMU#mh~{Fv=Yp(-@!rKKvI+U~7#`Ly`bJakXp~WDkaZCS{?~%6 zB_gL7X+h+Ws6ih!_EGqWKrA3zob_Daf=G|1#OS&cWWeTn8kbcWfQCW#1>GUV#_pJ$jA6MQvvKM}5dN$_wXZDNu{V^VTDr#3fWI!94kh-~C?r{qbA=ALI8aF~4I?+`Ph=DMD1l??^$+?3}RHG&C#pSNek?+;;Y68yfIn*4mbE#5Vw@>?0B+}&$k`bPb+^q;!)Ta|FZ$_`8K z$?vl$^}ovRmEHM$7drjP_&rGS`_^9hy{gFXuMdy-{Y4&n$oF=oA?rbFISe7esGmbNGT!7@1uI*_m_V2{}{hpJdk-bY4V8QeIY91_fbWDpABjX z{Qk_1CH#KT?h(H~gQ8FH`$lSl-;M2!ukSHKaCe7GH@3I*|90uuDE)$!#`bkR`F$a! z{^$H&*lbtaPs;v)OnzRC`7~RTV@8Gm19qq5pN!|b>G)ByBfX3p6MBBju!!$p;~~NK z0~t1QrVLXo=GL6~Rq=L1Eod^fCsw-vEr05_gFo^eIgyqpRQO*mp-M$#uGe(%iB8Qc z&RleBHeD}rE;Vp$!Ke&* zHE7D%DAj~9p$R<~Z!kiE6bkeeC`G1Pno?y7+@$UG_Cl0FRMe^{RY0mTR46o^3y8L5 zDj>EX!?_)31!)Tu^8b9-KIh((G=<{teSXjTe}3Gj&Dm$4J@2*FUVE*z*Jdk&j9H1L zf8iTnS@Ii=qs~v6%qO^bU*UxW1B+M@)|MhK65sQHAwb^t zd*|MT!Li|Cr_uA2jY_UtOivyuB7O&as5E@o*cTsmr^g=-AA0lnb%uBS;6sehf8`4y zK0HsLAAAVe{4>93P+!fs-V(!EdT9;_3ynEnfIAR`J{5LkqG0gZNNk_z?8Phn?v0hr@?HJkj>n zhZtY<)aOHdc#J?l_z<#1U#~K#uVh^E_yAXQ{x^af$p=1^=?nN!9(1?}Ly%th;Mk(x z`0%%1e1s3-m>NDrT$2Dl#0TGfclZ!vn~q@U^7wGujNbV0^-o9m@M~6z0zRzPEM?uH zSA+X0wvar~?c$f|VeuEa_!)}0dciD<7e1V<_-*i^p4k5>d@x+tM!vp@4t^+H_>l7T zzgC8Lu!ukm5Bkd2kNqzL_gV(T5W!aMN|Uc=$O$G?>Jyky9emeCR4d|r_2z$wyuXhWQ@#BRXl=rG_VELU+&w)c2Ny>oj=aobIPqm#P9z9N4}nCzq=y-J8om& z$}aR44RE#v?sl5EA4ydnfzp~NsZ;}Lau${&J)hB^7tvZi%74V-HbwjYSaX)d-p3o{ z$zp}ehNaWoE1*-dV6nxFui}U)*bJ=@h$#t_OKB6j8Yr16G$fEuHSTkEp z?NPVUYyQF%iyy4l?67nntk?XqrQ2b><}sFhNA#NYr2I&)`M*-H*~fs)X*MGRGE|$` z)?XT_K4t9QSFJp18>j5_R!a||BX2eS;lp;jPV@AIO6e|OAeSMi$y>$!&=O)5hrQds@Rd%Y=ImG! zIor9=Dz3#+4Moe5)B(8ztthz1aGr@E8m18Mj@=#PXTEBdI+ELic6r;LJ{r8Te%Ol@_H^WmGiuoM;*p6e_(;ee!4Y;9IcN>SGs%394yc#KGhZkFY0e7(;i9 z@DVCaj=I;!E({Hh%fjcp%EnTcDRnO^6~`SF48sOjy)XO3944Z(f{FZD!9@P7;FJM1 z&oyVy8M^j}f_|f~y+tB(5{KdroR12$EVx+Q6E5e2u>@FFySbi-Qza!;bqgjeriO5v z6Y!NGupi&W;(_2Vu>uNGH?82;8PaXZOPS$PoIxpNi6go*Z z)!VRhqrn?;^&?xRhfD6;Ugtk~QO`4%>tBuiwQ&f~O+gU&>z z8L-1>b0jH~nd0V5QG$CGI+v1AM@D4aX%m>tG`QW;R=Uxg0jzgPsKnBJRARlIEs+ss z9^B0@J2)mI%=f&`!>obd$ip#qg{A|H;J8+BkVe$XuAr_R6j-&SA$bj z4CUw`2Xl;b-E@LNEx15b&%+G0^zCE0=U2LX5fR(9JOsf zf9TqyV(`4?{p^u{wrj{I-bmnm`*v?zs!75YWS;-FFnP(oEN(-2gUjuxn> z;NOv?IlC~^gmu1ArPuc_600l!ndM@P0*VVRCmz3;JZj*!03Mp!Fx2>d%Dulh|{2=_lF5ZfR1 z)f4O^_H0|YKbKs+;QnM6(J$Qp?o-<(;nOAJ(c#!5vr`IC-%W;oNcd5f_#^<@LG$w@ z>jU;D0n%^|8x5NPaYySR-MpH_z9=aEXOt4W%-(Lc^`6l&_0hcAK5 zes=tMP2!s-Rt;^O@`+@0o?uCDJbb|L{@biL6FEvHQHonh93FDMOm&TsH8Y%!9e7O2 z!w%N>!b5!rWyZ*;xmCymvC?;7zP=Y8s+4gmg`}TSK}PI3T{Tkn{>iHPhLGc_S9#-9 z9w|u{turc@!bz^hYR7G||enMUt~;Zeb&!R?=ac2f+$1?n=h&*Vxv5pJ$%lG_X^d9L zYL!u|GKS_kI4grg7EG)oJ709mB;2Si>#?;G8l~7gi7}Ejr7U7q_i}zCj7DnJRBG&u zj~N3r#QDlz6vlCieJD&HW-)F6q{Hu8kKTvGz)gk}=BwE0^kbhAlZPK$64I9=2|!<- zLms|YKj(ZN;&VUrWoH*%sUDgRa?=1y*DrlJ?vpz}UkOKO|`)`w@Mqs{e?-z?%JUqA#!XY<2S8 z=LtYxUV^?<^+R9IDC(EK+(xcm{L4))qF?&5<`dhcFK-%OApYgCD+=_b+Uu9TU=|2Z zP~cyFNV5Lv%P3Ns*9FJFyg|bL`IntY7~|cWu~zDrzWl=2kvxxaJL%q^z8u(_e;G7L z8G7+A&6%&c&B33b(Xq`zy(M)#6wC{7Em1TI9M`Yg)f z^4a9J1Q9O-E23r8u`?H_t_fa@g4-gW$PgwO%A3Xl6i|7}RKey`+;Bo193wXSM4E^v4eermw-F6U@-=S>FG3S(OtT@bhj2l;7k!;Iy zN=|1ou1E-on_>A6G5wXPo$RYxiCg=)+9@uR+gma@WulpRT@`2e$&Czjq{Fu{5arawxDO-`Cp)u z^sMh~bZF-dx+11$y(-kRwv!AuAieagS5*cNlZj}!&V8u>i za=~1Vg<++bx z5g$7zQMQVoW}aN9E_kb-qQCzyMVf*c2aRaQbV!m{l~9=*WEGp?qTFt7)3DFJ`F^>- zyuP0-v$woomWMa0r)B`T2Ni03YH{$Q7UFmap}OlOqIpF~%AOk%ykCL!|K71uhOgVP zbT1iz2}T9o2Cccn&1c*8#_(DLa&x8hH}$7popEXh6=4386ibhs#v%75tAh{bvSvqe zJZ&KUVRDojB7gAc&IwWGgJ$7u!xMBwpjN*EA;3ys`wmdldkYY+ z7P5-&;CvhglxrDeEi{a+)x4Pwit)=m3_xpMhf!KxRC*qK70oJx!D}kY{ghZ|-6+}R&aBa6+l@e71 z2j}1qEe4zkT3%xr%nmQcapZ1QuY0ZFOrmb#M6h9jV;n2(p8HX(6|M|0ye$0>TW7Y- z*S3^$O82@8CyW{b{3Tn5U-!L8=^$eH zYcYeG%DDi2;#atWHa=>fWYk0w%No--b7YC65?8gW6?boIr;hEiv#(0W)u_=ul6%X- zvcNf*|M;F|Tyv1?V!;U#uZn32{bw6IpEP$FP$N(M`=8pm%e&uu?&sE^SH_E?bT%Wv$y+ta2GJEH+MV=xDBGy12_yS_~z1U zmonAr-Yi3=^ox{7hG|6dxH-zVBqd!<&D~O>i%X@6JJGYsq+>6HdsJ`AnswhvqD`W{G4b4<(96xy_hEe1u}AU z>flGdJFac4O+jj5xd3887lphyX)b@)O!~8m!PSh>7{V0QI*(=yoJlbNr$&rw4xabj zohIH78VUkSFXtO$Vx`f{`2bfty`$PQaas8lg zTxvsKKIV0-v2xE?t^V9Wdit?2r^@pFP#aE8q~Eh6Y&Nft%MGe`-+isjSEF~|+!mhR zeN#_%uRlCN(`ZH>M2>Wkcb%%pMy$l9OS?l=d;d2>H?}c-LDgZ6>31;urMdfMK{w(W ztBy#dpK3@iAw06`DEc|Ns=^ymRke0hIKKD>PNcThi!NVVMA6uZ$<16lh3p+r2;q5& z^i5^fl-8=^wd~AnVwcocYJR*y6_koM(}ucL-t~+rx!Pb>n=#-F()kwpa)6wGLv5QfW0*2cLo#Co&Hy+6plD z8H2))pLxkhvzRz|lFH_^^(T>@qU2$h!khRSwVu7f3^Lx$>DLG_*!*Qm#x@>~qRYEZ zF72q}pt~7U#&+1c+rmj3+MM2OGj&)~`pHE4AJl$<=IBtHBg)-&egNiSqi5ROw8!TM zcgR%@#p`1;M5WBoP|9S68gYYP>rG!nT!S}v(FaY^)M%2X&g?Zw--=CAb)QK(aoskP zR2lr@DK|-sI+MgMj!9x6pR)Za3Nf-Wn8Z>MpCI$+uxoJDi{aE%CelBPO-DFKIr*y# z;oD|ms7=jb%u#dty|GPofp^{IZa$8zVjP))ru1S#{K>y5E=tUa;LebWm3YwQnvlJ! z)}C3gFZI*UBvS7JR$OyuU?sfC47-N{ggy-J3QZ07v>EC&rpGfiTy-9w5)a0vRKoS%-kN~j-N^O#+|~n%qmPv;B}sF)3Tm2L4@YIb>20LS%toqP2O45 z3uQj}WG|HQ)BiA(p`LB$gDbGCZ!wg439R72hN257vum(^buVN|rmX`koi4xiA-B|~ zrGt)wo2i9%s*w5H+M;6`spD{8Za}T`(~qed{1NX^5Xd%U1(kd z5!>Kh_unC^?!sIORU2-#)$f9;GF<{wF49@gs=@q=ZpP<7q?@zI(0lSHS2d)T6vq%# zLy#w(jdkzl=YNTt|6lbt|KjturJeA!Mm*mSFS{XheTZjeu*>7U@DfN!=u(sI!$HKM zR|fw80pn0$c!H`OYitsqbW*0#f|7VjBluW;;ILMza!6abAu}>X6ncvV(zc&dCJ$GZ1dC8XMQrn z_V`sEKR5NBqA^U-!s1>Cnx~s*Ub19tJe<$0Xik*V?1@OdgasDGjN?AWrlYmj(8DzERnSZ8t% zGcS4mooQGc;R(I|7qhTAoP|cO`4NcdU%l(TN)9b*s~C4-%BNsSXhR-T`qIn3^I?Rt zdcQ8R^_~ofWomI8U)(~*3L=Wx#A&4I53uE+VIjTTk?9DRv1mFRzVtLotW}r|8$zy# zW==9MyE);wzs(6}bAu=U7QuF-*bbwfIHKD=&cOGx(XmQg(ZMfQ^)A8@Q;F}(gWo*2 zwZ{rJ+FI$SZ;#Q>WZKQsAA~=VKLop{Jo2l5^|~^a7WwV3x92TYzMjxQGuy3bd{tHfLY!8>8PI&2+tJhv7Dhh%1^7BulxR6DdW?e2~WF8eD$ z%WdaNinKb;EyHF~Tf!%P4s0yxl*mCpKU>e8X`Pi6q@OpaQ(^}m)v}#PmsAB`RynQ^ zT}Pz!x>SZ)56WOf64|NKSU1DC%HZEDhunL?-k==+yk}s7<#l$`i@D_}PJ>sTR--I? zZIF95Ov-vKIcv)}b)r7g($b0X`8z0_bQ$k>wX2E49oOzm%a{rcIPpoazo+K8d81k=P^!k{{Uddj1O>ilo=%*eW%GlNWnHx2R6WL_mso|By zc4d-_2L1?4l1S1UB~p=o=FlpweD`Qtl(0OQu2dHKrp8Q}4c@DR6L_}~ref|3xpEO> z=Vzz83pcVi*tH_tyK1DTWsJtS1#|%0blb?21l?sF1THX<$*ooduQ#_dN^H88j`cLN=3+^8ax*ZQ2)Gx=ZwpD_%^{k`&~RaszLP`CvQ3QMcH=AKL9H zv95*jh+D?kTAXAwTyawUEYTRl6ZZ`NSF#^od+DdyA3lG-rosD3H`l*$J6dbq=&IzE zC#Y|eLA|7|K4~X`p&hqBTzt39J%Kr49fR2acT}w=jW?*qPd8M0gT5mM(m@UZcXx`9 zm+-n<>?#=V+JMv%i0WBERcL=^Yw4=@)AKZi(~Q7mOMc?Rt#X$gcW}763F|5tS3l^% zk;bWASS0EvKwZs~s0hLc&fHz!EVddH(+|Yx1i3C&_yiBq}W4jP@Zz)a#JXOEc^u5%oR=KxSiEx znli8&u%|AInp`Ftn~x)T@KxKHs8z8q=bc_8Scmjg`Na$Tx|I{Z!2TVQ8TePPwJ#!l zbLgF)+E5m}A%41Uud24^CE|J6$jQ#^JiGnuUSJ$_$DjAlomCRVjlw?T zaP<=^;rX_8h~t7=jXjq%KQ+0!XyRa%(Bj?Oz7){z;Aq#OP~kj<&9}Nzz##%1yO`$R zN_q7{tq3tjE0WKt$(;LzCgmFW)8%J}{THN`%k9#ot54=uJ2=7NC{sE@YTxa3ejeSG z762EO`y8)@{m@(q1%i8*(iLbeGZm54T}O(f*3cM7Qm^MWcO$6_f}6v>j_0nRLWXBg zRE9Sw&Sm&!A;Y2M2 z-a{J}%!6+fd*G)REp`{p^pz;`B}sq z`>EZk0IPK?wcMresWvU`Jg@U8Y7?Elp(2o(1OSl89z*ho9Ry_-QEoqWzYp9>@)SK!~W*@94*qnZgq^1~X zDQnEsCs`Rs{k`ENBC~1`Z%?pBs~)1P)4?@>gP-YC&qtjBA+OG~_^G#xFa20*aeq3b`TL`7)PX)Ud=mRe3Kc8#Ujdo)lDrXZl{Dlu%+D1=v|#+S7e+S8{k zjQCT*>&#f|scN1&hnIJ?Oe3n94<=G))O6sabD6=7wZYG{3s0D?qDduW03a-HQ2wZT zh=YUue)e^-qV?srA^rwg9SPajcuUwgS4X9dhz^^(g!TD^n}ovR_d3EKSBHP6A$P@R8}N7*)FLVK;tSi@5nMY1#)zz&Fk7qGjC z*EP7Arlql^r%0{Np0n~acVl(KnWwY*%}cE=7RLL_bN(*#g!ky*TDx;S=*7o&;l7M5 z$%bGeL$2{HiFbT_tB(lTdi@*MYa=|?>(6VM%WSvTCeL<#_0xOJ>(kIvG-NcCnhR~s zb+d{=J>acGJpkHexj)m$$WBIDg-=ERn`^NzMa{*V(0%&zf+M`nBBavCWlP=^5pd9C z==7s`3{=_(hhW8EgsbhcgBMvs38?{*yF(CpQ20#8EbB#pLxO#kO{Y!6<+j*ZVa;g= za0wO?C5QkqmtsMQ)%gRz>9soK8zGc zuqW}MKFZHr3S#3)^$2J(Q>`OU2$!mLfYBX zmp9kF>2*9u(@@T9pHZJozbi%6U1nVXGeKN3^DBKy7QYEKXiNLEPjbIf0lf4^De@y( zJKKcd+3z3?;M9FE@F7hDAzObdQ7#jL;YX4~X}Yzzpg58gT#*?r>Ur7F?g4uT4|lOG zz(%8==&%rQDj`yDOK%O#y$+PKKUp#l3m+sCOZeN9o5UXnPpSA-J!sBC{rQ6J-nS!R z&%gcjQ*dWbGVYJUjA{B}%$H*>WhZithDg8QmL+sJ{Td5)@O=eS588+>hCxQ?`I;mb zLzOU@F)DDg ziNV*}ss34wRe%J$T#YDFa+|wy4KZEgE^fpDrpI9X%l)%Tv}LWg%_e~^|Eyz5G81r- zUQ(Yvw2H?y{#oUX8Me^b!6zUO?P|&(t{b)ldOVI=6Qhp|~td(BKkPn8=oNP+L@!E@Cfhs9WN7%%klM zY@#y;EXL-wWy7&+%HEg7^bK#YTHkHTCs$A&a3T< zZ1_bCVjKS8n~S&htbHOlO^-RjNIh;1e!8HrcI*M&&&!j|I3cn+aq{G3=E|YHsEY&* z>|l0jgxSu@?@#Hq!!h8&w=Or6VmdBU9lfo_%kJO3AqdON@7UThNsFf*yAvFWNsF~@ zCMYW;O3D6d8cab=Pw;K=Ntx@1>J25$1Jwi?W^>e?+6JE8SzHv{MJGc08ovAXokQ!E zd|<0mWJ!_C-eH^kWcKc=cD};oC6PHXqv*kd0|W}0(yC)*dbFWiW=9=WOX-<@>Vf#b zmFg*M)kflKy2lb)=K6jR9ae(CUVU&I7sQc7J<||qm8ta8HzUw3C9l>KqTs8$%fd+eh$M)3?u!Cs{L~fIMP7%@Jd!|f z+5HSX*@wN8(q?vZ@SDQafkT=&#daBgxln$KGW9`o=8lq$WV0h4w*UeS?N`)N=-UvD z2JfM^;aa1z_fi?m&W&@J$4nda^K9LLeSutzxbB7mqh$JQF7pQ2#!;m8poJxCJf$Nn z`yuBfH25?4luYBxY`RIa1PFEANpE;JqKKr|IKO5^&8EbVrNJpSCptF(R<_V|vHF)& zXWgspd_FxF&govW-su{7SJLTbWp{Q8|}^)bl!VFH6k&%zEu+ zEv&lw5Gi#HXxxpZSt*9F?r_&@GGzGwz1Yoc~yxYbtxZKb;R{4)q`4{?DUQOl8 zk5=XXv<}^Ov(^mev@F$Ll=fk;Az{-UVgnZ2 zy|H0eFcbQ@7luu+TH~1R0&X+{@}(c=%UEx4H%5_h9D#)@ANU$H+Mr`Qc(NQksez^X z)6J9{j1dpzU>vKd{~X3Avw@7mxNJ51!gyl{)FICAN7atfK}rPU7hUBj-5l$FY6eA! z5b*zDM#w)P)kTa6HQ`g#dPHKSJFB|~m>sGi?yP)D}h~d@Jm^(Bc$4`Jj4uRf||5!YElO=zgIt*Wxd@(O$Nxvbe?x+00mSKUZ z9>VhP<;BY1A|jRPRsPF$YR<-Z`Tq>d-yAI6HvK)8{z&8@`SuLuh5ay0nE*oVs)w@W zI_xk6-R9C@MINBYE0i|Wdx!C#RQyz$=;Ed1S)99*Lrd}Z&!|n7&wZcDd+^)Yf|Wi~ z+!h2c7=ZPSUlu&vZ~W}w5BHEPPlj{4W@QDE)!4 z_?lSwm$C4#V&QSI@SkGkOpnE9V_^xN^7@`g*Mm6c>UA@ev~aa*qJ=Cbu(4WFM*);hSA4Y{?FM_8QjU z8v*fTc21R&_}N>ky20PXk||n%^aEXG>f$G{V$nV+*zfm(iPC+(67B+4h*vIywCZP+ z3F&+}@9Mo3nklXMDmVb|U!mxXc>nRXS*W&T)2&Y5_5gQ>TE4qW)DFR(o@_Ouox*@t zNYtv}XoJks*|dTa!xhwPL^7&Rjbl57US@jLjHq2iY!Q1`MAsmi>nE0 z!rHSaZgu#S2i@RsTj)wJpfik$dQCy)DxGjeI9?h7b_-fyYy`AFQl*MeZ8O98a=jC8 z-`$3xB{NluadhR#Gi0Bsd>}Tg*yms`5;jJ?;!TDnPk7IiqabzdQivBac_0Hfb2;Y z9Za9Q>7U}mJ1-CEg&lYVphAKV4 z`l>es1|+2G0Em`4a$%o9LpNHGrMizr6le5cz~$$-SD zhEW0PQ%eM%wkh&6HyCWAwxDoPfl4J|q^i|4e$2kLQthb?j%6Ea9HWq9z(i1ey5$$w zXfO|zYGG&e3GGUlXqA?%lbURuL7|{sRxx7f+8nf}fB-FW^mtY9n>)o`MsOPDk6Hx* z#oyz$O|z}-5p10M1I&K+wO1L2J>=7gME2TN9vTRBSw+|o)cwut*uvDx^08xQK^2oW zpF<%^4bUmCx2GE%RAWtlwxKen zjN>Jw)$G)i&254%#s{n_tY=cKK*HmrFa*ulWP^}npha3gCi9+52dE_vPol+?O@Xq{ z2(xI>QrGkpTRJ!vH-Yp!@hF;DvbesCGad+SY4Td+bvmbq z!8i#%?x#^jurEo?FOx(%XP!ou!4M=fM~*spC(f5LM#(fbIWqU)lIPR{q`qt00vG=( zW@_9P{qg);xme(vLrh_-=*%PuFh}{R8>cR8E5X%B=mI)Zo5eXcRFX(T?~IXRrPb&3N-eA! zY^QKOKzrEi5zh-Gf*<^n7v^3IUA{oLEUiyY={Cnosn<}qisrJMs#FmLKlKh;@~G-y zPp$a?j2fk1G&o9IEuM5Qta#NxtkGa4E2Z)qs&ZMd8jAh8e@*->n`&1za4=NMnh7R5 z&t^v-4;n&Y&dUgE4fo}}KE2!gMBRqAI~u%urz|A_9qMQ_Ax`N%2Z@`$!%C=m zE}6a6P>Q|w9aS?l4#1)a_(m4n*SsMpTsqfJ{F3Q=zyh_a9#xkH(y;c3mZ6!g^W57jVG zyK#`V&%gh-DywujIhCm^?1mCAB~!4*YVW9O*9WMg_Ww#KK3<8sRkH2*r-$rz>4^O) zOK2HZ7~CY-12(iVDZqr=*W|S*Ugz=hym|FPZGmE%l#pM!gi-rss51|SN!k+P@=Li9 zW+k8o(vln28HKTlykVgzUXpV@9fwHG69GiV0SR`YUdOX)zqI7wf zvvV)z_raoazObQ>{NPZA!0R~GoO*=&`(|Fcgaoz0;$N|uei0!>m#zCAO>pae{|kP! zBuBgz8;4xE+qWk&gJJXqw1vKQZeToeN2!fn{%rJL`gwlwbL#Ik>$N?|mKaeGrw9I3 z1Z_WqKbAz8bwg+XzM>hr+n`%Zsz49YC^a1l!bKYCgx9oDo)xMC9~xx7HsFvZqUKo= zoXm(M(|0(CjRqakq_T69XdOhnLxoTi;~~(f2>t+&V z!7sGbhx`+V6SXjMLRJjX;*w8MkXwOsoAGr~u#fGh|L~T{?19#QQTy-yR9#;fb-mEr zTz&wswu^OKovu*tgRdJK$y>hTr-Mw%G*rO`F(f}x$8TarMCHR}zO;)a8WvBBdV$om zNr<~g6Q68#iMWFN5`u-Ru?O$Jazv1#1y^L?KjOl>nZ*LL=XIW@m7)&^K7FPpCC>bO z%ckja7(Ey^Fl*R*=OG{{xlJ$gFDmk2to>cx2n=Cck3y>`)1K}k$}Hb{&x6oHKG^aJx!V5yR8)XZY79!WU=GB z?Xzt?PZbZ8(Z1L%8zKKK{z=eWv6IaXJEI-pzYhYw3RANEjnSLG={qftLvoxL0Cdu{ zTrtk^-;ec(C>eMFNd?&tm?;wPr7gB zK3-v7vBVU)#nj?h)<#I@+ABn9tMi@!Y{h0Rg086qKYidlVX(375d+bid%(|2B`NEn z%8sI|JeV%#YP>lYup%>u4l>CDQdwAxWZhb?qe|uHd0MY?Fz@;CD12!9mS%d|lUkv) z3qg&-F`iL-+)n^`*Aw3Ui(q<}Y06Q#V&*+~zA$IJRkR~SYpz(HV9hTK zd<1xZ`?*PNG!(csP(Ni_)~WN%1-c#?ocZh{kBeI`!dC`A{hC)3{i`l~&|vzl5`rCu!dcUh){Y+qVe zEvRMm*{X$AU_o#U6`4dui-1#znpC)n-L{j+si2AUTySr)z6-GNb5a@z87=F`%QvY66u?qgdq z_JCAsw8XYzusPP8{Qe$o~-ZNqodv7GV$(;Lw z>Q-xlPpK()(YP9LD_J!;_d?z;CrYSr#9p^8)IzH`cYI6=(HO= z@uSh;UCs_ope;pTenZ^W5Ijh~QT(xjRp)+bW%f<8ckm+{o4)a-!PUeE2mVBT?fWqZ z&gNsJ>}-XpjKU*h;h)9AKZu2IjD^$N311P5Ukoc2m7j^l|2`H!FBX4KEc~Zf_!F`4 zeX;O9u=`Q@--*SKiIsm`Ed0G#ngrpP$#-vi%)W>3Y9VzxKHG9OBrUI;twtHP9p&sq zgkBw0N|2vauX7^;aHJfy#P*VMv;z+m|Bj!%PXrph>ngYxO#Ww^D#KZ3qHtY>=sq;h zl!@hd;ak?7Q3(cuXK4JBu-4-w*_}a=!R#)@mPc z1Hy1^ufD*h6HB9sRkpzjrc)Ga>&73bNLfrWz3GOxlTqCs8I~JSk(&lyTzJeGYXKA9Xc1TuU5XTuVZYfF~5PL zVD$)5iYIw_b2YEyDhFX*XNt+_W6PEh6L*xpsE54_+T32k4BGa#1gu3nWYBh;=B(!0tOnPWl5ofm^ttcUt}rfVMLw@7+;o`;sA$ zgEr&lOvPUq($@_eO4qWH)mUpT;pUtX9w=Tf6drGrQ@UXf#}qmGLIZ^2qAw9qaTahh zT!}-%5>4P8O0`7;f@QD;YW_5Gi=Pm+vuW0o+KQ|;S;O5BQpVDQKIv+X8WUWefj4*> z$#7neG2IALZnpS^eT7#+aJ}}UB~@|m=?Mz*I?mAs4ZW7xr?v8e{n)F)?0(YUYUv*` zgRn6DA9%qY47DHli_&0^)uVt8C_{Tl84OYytIE7VPW==dZ8OhF1Ev0Bn0hrpq93P# zICFi2GtKwzf>tl`m#_Dayqa&`)awUoOzyC+&Zwbgz0-?4AX-!JXNTkO*5{}WZ^vbh z*m8XKz;{(_@IB1}t!#%_XrJ1gJ@k;!nJmZEPF?nf9Ou;L_p_;f%R97^GLegLutbh5 z%7d2}cPPy5?KVfitsN_>2=<~jYT4w6G{~B%@rI=OYM26?DRT03sGHxIxz+SL4MUJ>M_Nm-^S`B&gW4@4 z^9NA7Zj}!th(*?85ggok6M?nJM#m`Q! zTCI*o@Z&~T=w<$Z{j)ausWk`?blU~D-9KxEZ(w%KQR+ap8Su4 zAi*CLzOE1@AF&iw-IL4*bijFoGx80r1fQEyD8%cyTWT4%dL)an*dNN}G!v@-a&h^qdqee5$ig8C!_5gE7O|-{f2q6b*9=VLDYyY>#%j~SGFdmH?Y*5 zjWqn@R8GC?tpo_+m5ZV>0rLyw0+{o-webcI4W43bSo)G&@~k7l5k=FAC{l`^z%7Uu zwS)^IOQaUWql_dt4G$;Q%icH!Dm7zkbKO=ve?Z!r(_3vE62%KS*^5z-lqq$KCmfh- zB&X}Y%dMOxGb}-K@xLjnm~>uhZ>ylNCJxkl#4_}PL`G+$54|YEW5b&Qz+OZ3Yw(l~ zK2R7cPDli+fNM=rze6?Bh6+|CK2){A?S>0^mO*2-%`lDldaq&X{1ii>4tCtSnug`} zJ9zo2N`#!?0(9XiRigB2eYsco@ z6^MzegPeQUbTL?LQH{Gv)UCT@l0TbL_1o7D|IUF!Pi7Cjlggp2n}fpx#08Vq8FXZV6Wr!0m@e4r z=SFqbF?~>&+&x6%#aZLhd9{S2^jiMc#?bO>#e*PloGe3(F*x|o?&jtJT8dS0sCgao znm_NAodgGoy5$o;pGQnTy~U{&HO^ug%KYM`fJ(_wEbGAEc_qoZ`E8Fn!d#35)X$C_ zXcIA6mHL*d7#atg&K$yD?4+~RslNhx*p}_Ic4!2j+8Rfd?5P^A^~sl-Um~7FaOR_z zSFVU=v^_?9ItH(%Si$ae1=ivWKe8XG3nm`!*t7omGT@|}ED_@i1<#!a#OL!D>`e9( zY~gsZPHQmRIWfNxqnNf!1tL2W6`1=xr3jvEKHj{F`S>@sD`GkfSWOjh{8Sai?jjhh z)k?!74ohrX3D?MQHhxR6KWTECxT(nDpBS+Te>U~zzErb?-tY0o@!FU%uSC>~w`%pZ zW94e3o}KkN*>-Kj5OFl41gXjWz&Z&YZVFn}xOoWh{ZPgc9IKcH>gq_axI9+{)S_4sd92Dxe$V8!5&IY?seX%Rt%q6VlX?tZFl>Kw@CnZo_f3F(l6O~@g9?A z?*b1hX*RJjX?6iWvmr^)6$u=`L4M1+5|rAihu=7_qYHK^y2b`Azd4slzbV^=-^=gD z>y2_(!&-XFIwXyoUes_U3O77us<8 zUx|JJM;q1u^6Qqh&4h!2K^pyvE%lA*e^TyfwnOCu4>-&YzsKhhO;t58-FQ%Z#x zXd8pHlENDo8RB5qpj{VPh8E`gYuV8k7SS$Q@36MW<;=W)4v;H`a>KFaY}LM)l$Ftb z?&MKI3nKMV*zUH$2>~iXUSXTCI>(KwMw4a7qQ;Z)2(w-%9aZcm;x?AfOO<}0K9rv= z-w#`o#qa0^uN34%lCpM?@oUVD**bh#S+KL_e~aq&Nj6%H;uHMGd#sI|ZckxGTA4Uq zZF0!QNpsaL$z3RAk=Hp83g~tSP~Cc24SqHx!L7kOBRuq?#g;532u{CTMBok+-JvHp z4>TBu&Jr!cpGoR!1@gX)ENVW>L}JMz)?Q4Y&8txG)izBP&JxDpNX)Kjg6sl{^|Gsu zU|SG=A4xJ&j`IfNRTj!XF}6_d zN7;%ow}Te*$uMTN@W^6{Ek?7pgnpT!%h)bFy6k5A0E2->81~&9a0?oGWCy#R<%iAV zX^D9+?Ucx}7Uy1KmEu_Tg?I~k8E-*Xl;sX1CXu~KLz7D|vC*8-3rCMO<{rGFuqtGbL2Ro})V8me`VdC4xB9R=cDD1&3~K7i#sPSS@ut|l z#^?v_pr?%TWkj@@6q#W^DnhR8yt~ZTJTT0)rq^fkL;o&Z2K8D!mYCnWY-^8m>%wHq z=FsV+oWGagFE+@=?Sw8~ES1X;{GP889Tov=63lhcaX5Xc8$of74FpMnT&A>XtDF!- zy2h0x*AGH$x<<#iBOoW<=^Bm4$dn%ct~+tJg%9qCr*rS3dfd@YteRjJN?b&T6S-^Q zinNRuu;-xum#IIlWmHW>pP^Cb64!+=a>C9&oF7ef~C)KAqQK;39z_$kvPP@^J5t;ZRYr57UYqf;&p z4So(Zapi$P#{4j7oGZMJ_eh(BhLznPnsOd^wACN{oT-K8Y45d=)gv^*6 zDWpo+Mj$n_AXR#pr93Z?x_mt2i9o7@@{*Zz%&kGU>d$hRLm<%3OQc>@`^F0+CKD;4 zal4)-S9*d%#XV^FLdD{23vTXTO514js5(NS5+%D6ejoqN|A;#nsSwyZRTO!h-=`tD zHyC8}Etz4*F`3#Oa{lpik^&xSVnPYsU0JZl=2#b@|1}M z5Y4k(HC1FzYK(394_z&Lm^M}n%Fr>FFn?tNNl6kfX-x3BnsC-ND1ixbPKf}(>(DY8 z4ZGlFHEqwUECq4V5zGE&LeN$nGy^$pPbnhV}D!WG=0?2N6l7Zqjrd45q~I-7oimwkT5nW0f2_4X6gOgf6@`5mV*9j^#NY=@Jo6_L;{4BmVwTMxzG?T&Xm=s; zhK4azsE6z7&oDo73Rr;*jgTpRR8y!0oKU*fgDz;A*+s-~!YiVqsiu`7CFbxRl z<8P&nR(c%=z(10}i39i1v8Ar&%sm@vlq7Z?XmcG6Xyb_K=Agk9Lwn^w=F2Lf)8xw` zZG;lOK-S`14(Ui<{=~3XEBBuu9J#Nm4kri74%_dm;mP|sj4muerfH~11#BO({m&rY z(KVjPlXIVr%d>QDnObtMP?XsZYzSsD@*+KP`+*x6Mz7;DNUV%p$YISuUKo#8gUOb@ zx4psHmVOLWgA{H+PUHpqfk=?MIoQZd;eIZ0i=9;&2qHm1gg3rc%U zRNLOc!v-jQ<4c1-5Fd=5pxN&GF);eQZTzy}axz6?np5G{qVNx5;Q{Q#qIf?R|D{-X zpICTfEc{X|{G(X-wV2rG#aQ^MSoo1JEO`vV5xIcGn}7C*6S#yJvFWP=a5S65(JVKy znfKgAEfn4m_<~^3Sf(_+NLpYNUe2pjffQ;~P-QVWzD9wOa+zG2gn0TaL0nBJ0Fy)`{weD50#-l-)wNi@*- zWD&$nBk#<3$@2%(z;V>30zbQ`qz zBc6Tf#FPla>8V~r7&oU8nxnFylJ@e^6_zzuA(R<|^77za4Rh8`u`3Vqzj@q*Tx>9Z zfx-M(6zx^%HNbmQIxs(#n}Z*IGX(9;!EPqn0<<>=zc@P|JH@zMi+z+v%ofxG@HZ`` z7ac3w_6N?ZV^9tps}@RcOs@gTZ#z#=er(y=6#f}L7J~O@UK4naHt@dE$VQ*>OJsi5 z3$~YABUS}(eM_M9P&ai8Xef_SFKf>HUJ$2ERzuDDMD~nwTUU|n7=+;~K_ZAur0kc-E035+8b)!OK&Xr zKeu%-GgZDq?Vj=QUG62~?>F~)KWh_e#ZuV+j-zHNW1%|37vGf!-!d7=apCTnsk(bc zRZGvAo}bs-uJL!7FM}gVdAnh*MlLM1yHl~ zV?po>W|naQJ)yvsP$r5H$=TzSW=Ut_>q~ zIRLUk$Pq{pf?;&UJ5IbEDy`Qtu_2fuVCVUJt!beX*#atYEpCKL$JVS*W-lo(OmMVC z=Ms+m4!svXeU{z{pH5WK5k66e4dfD2ZmZo6pY~BZV%NlHF5}k0R6qC>bwuD4N-zzd zu*S2FWe>Vwe@A(_Z2kI6iOr`c|i-7L^3twnR}{96Ze_!Qk8A5pYz z;k&dcS6pP{ZJQb2_qW=Hbp2Z9{AiV4n{tvCm%TPoOf|`4JnU;N*4KfE`WzkAh- z)(%HE_41^Tiw{1xl5gvHf9KL~@^__+J+qovNZ%o^Upkt<%U$d#Ly3JPHGcrc87HTo zlDh2dH;b|piWM5I&`y+p$U?o2e6wiMk5sHxQl@$rPWqWjRK%I(l=RM|TPaJg*7(=! zmbd*5W!hhf866Pd&g1GAw40oEBhoBT$&Po^$3ub%lli-D!UU+Vl%xZHdXNspH#jTdVGy+f#>`gw|dcEV}AEu6qLTW73PD?a5~;S>9ecg;(1RrKN+wc>H#yZ`N@ zV@**@)f1ZXTcsn7HHD&4tDVxamP5j4{9T34KhV0?Vuhw=AwskQ)$_3JDvRrf)$6}N zz^&K%L|N<9YwOyy{UE}zF|6jkP3#;{^j1h?(oNSWhQU1JNzKRzth}((C<}game90x z@B!?FI&~R_b~GbeX<2CzaXd*SVaF^=XiTpr!hExo)JraxSTbT9@r%=#AMUuCi9WThy2xQT+OFnzcwl z*DhWg7j3^?l6+I{em|pJYu9Rvpj|<}U5iyy(mN1see2$Bt^u^`D`7M$>DJOS^6e@K zqnBlOOIW)eE6RSyjn=z_3ZwPz_FJX7Q>)6NR&9w|B|(=Pt*aq28m)C|)v9V67FzX1 zMbp^l)T*T}n$gmMMWtSvtb4IJy&7RcGW~SYI~M;|LDCzsC|S4E_l{keta~oujd(6u z_Y`mM;Ty&GM!ZFqgm=sne%%`18@`5=$@B^$<|WeWl65Qb;!ifx4}Z$9d&KvSS)Sl@ zS0c5HMgAQu#vbR{!}IB$#Nij$CsI!oH6MPapGZAklsNnph{1}YW?VU>o}y#&42yt+ z9hv-N*C%;J&;`^>kvrbdWE_97I%9p1d~;d_i>EL}J||fIB@<&;IFv*kRqc!!6voG& zVoK|Ty!BKTmKcr#d(-Z-;dG3s05v+oBXI@xSJhtXw0`3_eVLtcAUZoXN{OaYouOmzBO$2kt zU?!N~zuUGw;oUU?p(-Bq{NjyzX~D)YF<`{_b`BSIj)ib7E-F#u5~&fbVGBFY5IyUX zF$JnEi~#Gf$nrYpunAUIQCD@75{a$XIgU7^Q{yyR%or4EvhG^(k$KDfko3M^x76$W z9!<-y<4_<##p}p0*77EUgs~{~&Q4xut?cL|dg%!sI~D0+wTTov7%`3KGK?sr;nIM_ z99z_=acM*YCcWzA(89(uw+uO?jTTt)OYHspKU<+VFfimv)L>W$mAPgq+kXFn{5PzX z4U7DmXL5hkr$RPw^b3ZLe$8V!*tQ;jZm(RRo~!RSo?ssSt_b# z)3=JL+h^LTzCJjHg^|*6^QvPZ07OZZKBLul-#^Fq)`p%-TWTIw`Y>6;pMj#Hpp=iN zD!CHa=ZCG7MD`m!L4bH;RFw1Uew1^z<$RQnXDElu$!FR|{fX@Op5XUA(*5Fs) z(7vW8xa`MyNcL&T?5}50buj4+1}l3{Jz@5O z$@H&hTf6RA%_I0UBM1ky`?F-{xVvzBZ2G}2P*uAP-R7!bx>F^QZvW@NktLneh!nUd zbvqVE(n&XyD6Vi-EdWlyDCj~!4wRQFiVeBdAKn8re6-a%hgu7UB2gV*AvUxV$<9(4 zG$gbX>1?O0yrD>EH($LWR`uX*=DxnV`C% z8WwlX&>LO#c_J>*D{nKc7oA-$(!R+;#4OB%%YYsUBlOWZ{N4pPPG)RuBaQaspLHtF zD`OK@xF={FvD-OpbP*ON;bEf_V59S9A?F-Vttcoj9GS138|>6sKl@-M4`zO--qH`Q zr1iLRsWwXoT(&d&Ncg7H5W+K^f3iW%C@4svY@3GqC^7|>S zN3|Dj4ca^S=gC~SwI}+iPK%40)9IRH;t$CNY0idVDg&VBUE~SJ(pH$lRX?n010Sv| zF3Qx`gO2-m2QQs2InAijIYjE?*0PsqFXM%6a$MvZDR{UYf#(u+%EB4%e7{p|HlF2P!g(f*AZA+Mtj{WwgY*D;d- z*U;aoT=_$fcEH{nbh`;hWaGJ?EPA+*Q;E`9o9x(>@~to%rNK!j#f^53jtw$0nDKNs za%+gPV+XEsnhewg^C%NIFS7=4OpguMreNKP8ZVn(DK;-DYsefsI+?aq_(`y#ZOj;5 z^Jmk=s!Bo>i^-=KoS;TI@h0I`zt)kEjCU&FJm&}yP?xK5$T(-t& z+f?j3klICSjhm5Zjd4yR!O0I;O&+qr`7jXm7l$E^s^Ky$zNnN2*BW$}PF*1wd9Vv? z#l=Kt$RX5&+p`RTORs&L1R>9`nzm~NcNQ_mDL8>}gDdQj-lT(QF)2rbQ)Q=Kx@&2~J?YXx%I)ey53hHl^1O_wE~c@OxvA zNsp|4FYlkj7T<8u%+0W^DdWk)Iq?z}&3me?!f>_xlUB=cEeJUx{D0>jw_NRDh)`GS zK}h9-J8kxC4m$L?@As^}kG46BzTfC-d`eEvM##yT*Krpa)5}tC)6azjH_fM;U!lX8 z^6yJ8hf?9RK{MHDHxbDSZ@P+1*mT4fOSvOfEni$BzxOtHxqYq8yc%=CVFef!Z2Mo` z;-|~e>LxPDJqaeuxA;T0G-Q*dMgEY*^Kt{;@PD0?zs?9gm>{7|un-{69R-+%LEG_6ZB#hE>FRiy zc?zy-?i1g(E_lGGUo36g%HXzb;uZ$i6DQc(8{jY{QF_7J>%u_pGDd>cZ}VTF6iW*`~is)w-a)8su!7ko%j;+_#;-MRO&f58i`o(!LUe6Mx1M@vt{!x2j;u2y5(a zm`q$)hCZOV)=xE+7sGs@OnOTUc#^lbC8v<6hGpYSEVy;y%1 zk!vA6h#Q9G>1C#=+FHUU1O`L0Y+Cb(y6by;a*EI!BA3PY0No**wFbNc(1}&`);bAs zeQ(g$D4>=9ry>hJy*if!pSt|iRh4vjkvd$j4xcYPanKQh;4t%QgXhJJ)=Ro_syrxlnF%J-ZOS}0%KVdj&HobAQS(O2jB@gV zP2P}+WL|oV)l4v%zCxKVb(w9zHOmxHl&cc%cO^3yS6D}yGi^f!>Q6d&_bzzWZp02U z)Dn)=cvmhb5}CpZZ_sH&6Y10We?~13UXj~Jcd?(=ozMX_Lq{oa?QOT#)O^%5Y zG4+<&(TK9eQ;lTe{teA$c`MCE9a!#*G@Y+MXO*u#5Ba5z^PVn=$*$6p5keRJsQ#e2 z()M8#r*uanzCU=0_q`u5tzM^&I+6gbVxv-!6oL4`tG;__TN^hGvEBLy{~t?m-mha{ zsI-pg<`U`W{mf~#eE7GCV+kxKJFKu8qBeZ(|0C{Q;HxUGzJD%>M#b(E1x;J4X?tvs zXeB(wmQ-pJ2yzadC@N~A5wRd*MWvbuRpgcgI6XbeQ?2^6wvSJ1Ti>>CYcF2vjc^T! z7rdjW;0^a7V(^Y4^8fzU?0vEmg0_A7|Ga!Yoa{Y&X3d&4Yt~w`X3Y#ZHLw7mcw)+E zW^BAXt_-X!B(z7ko+Gi@70knGKCvor^dnd7#rY(##Hvw4``UzmOEU9J^{UZB*}bkX zQ9b!P5uz;T5J^ImXi7^Fulaci*6gU^7S}n&i(Od)j1i=~;Z-$#jea)hC$FF7{4~5Kz#j_Q$Ab1Y zFrE=c59y;8f$4GbzQ)r^=0;!p5i4oHgMtZ`g~6B?E$Kc;O^5$>e|^+W-tmem*yJsI zN+bCc>D=Ia2f=;?BrpC@6`Ut>HC&~zjE9x@rV?u{u}O&=h0}9NJlhzdP1ru7chn_~ zTto4Cu&m!CoJg;4*Ut-r`iuas)l)42| zf&J1{vYH?JF#aL1XNh~kpAe9>*^7qbNgc|pCIJ^={=uH^0%LO7j^+p0!rXX>o43(R z2}r|HtJx|t)~)1QleVTepaG~8p@nIOqRnLDf87(1fc5p0 z!T#tc$T4Bf75y5&Ooo?e3c(PH(3qf-L1hsbbwq^JJ&g9$I`L~swj);D4PEC*$}><{ zF0S(bG=ey6c2-F;^(E+W?aHTwT&>(v z6!i)zfdgZwfE4oGtJk`@J2&8~$eecGWJc3Jj4}?ATG2SpmJjn^fxd1W1|PiE18vaS z3?O;zsEWqn&2RRt`C`60^T_CyT}@N4;F7)B!Fbj*X)v(K%25?fs~K92kD@bk*{P^2 z;wNYF9jojC*}KSGk=ZhcqvL7?fJ#&K(&giJ>vC_wH>_8q-Cn)w_~CdQjDRc-s3!NtYX6ji`T1tqe94~4;{j9-+2 zzZ?V9U|Nlg$$M1L;FAT|s*9p5-)kVh)30xz#xb7^a`ABnl0s@k!p1+v3ec=_9q3_( zIO|qwm%Qux{xZ#0ZJvtiARQtJ;Sl3r^HYU z17!1oQY)7d0PhCbbxN>zT) zcu2Z(Sz~!JGut@;(oCb1nG-7QQ zac7VE-F~>IEQq~JS+RU`Jcvw9XdiP+9L0nU1x6D|;M{6O5z&0j-fxBSZub6*N;jo+ z{uNCroquK3F*duBf91*qv_7dS**vPEuPo>1Ig&1!*@82mOp{R-6+foCDY=|{V@mwb zGtN@YSLx)Uxm3wxtd9wPujNvEXQcA2F?cR~JkRi3fi&435zve2tZ;sxg!8#NiAo95JON0&o&qpjxD$8#LB>sgfoqvG{6NC4@hzWP=(yM77%N&gP zU+n@qzyT%I^_R5~lM$#DG{r&QkG=|L^QjZU+5GZ+ZRGjn$n$AD6L}c!f2lsh+-C%` z7j}L$zn<(S%7)9}5NVNS74%6T|F2)eq|L10oY0oLc&9mRnVM0SYF{Xqr~dIMx2OKe z@f1tt7P{q(xAj`mvA^UgBJeAz$`=}EByGxFk<6Y(OGlT?y@JrfJF12^zMzvx7YGKD z46lHN^=-y5x3eUYnbFu0R!?pAhKW>{u3YD}9B-rNLrjv|?4RVH)diiY%niaPm6^uY z(#jW_a;jgdXLw|rx29S$gNT%l46`nWEtRQ#RHj9b+T10>sj%^g6bBbsW%3ILl_{T9 zmY3f;;OpAH1cR+}*XzE3ZxHS8j;F;Tmk6|fx&M_TrH1g%rD=4+Zr5YXP`9nLMDCK* z9<)2HP6AjNP>gH;19Vs_yF{rfUaqY%p8NFiH$*oZ{J^WtSR+!Ig~}VWEI(ATzOWt_ z#rNnv;(PgE;oB(V1fWbSMP2oCK%TLA27RdW<<2lr%F0C>PNOjr5Lbw5HJ<`2g_WI?knWNS!M!A*vro3^)n_Dg3uV7XIt4c% z%J@#r5lkBBxaA)V-P&RxDe2!Y7NQ{s zsR7Lw;#og-Qljy~fKEy1#H{TNqf=xKRJQv>jH)KdPH=QNKLERgg+1s()kYV`M7Zkn zHBq`GIG{y@f<%+?2o$b7-lrC)HJG@g{oT*5fHYIw&TrcYAr zAZ5qkhQ>k4{eosO{JmbK=(<>83%^Jvhi1&y7Q5(!_C+=P4&QN$xXkV6;yiK48PJg;RuX~C+X2yzT&m?khk;Uj_WBl>sj@5T}KRz;u2=d^N#@mw``CJw*W zYqbL{dqS+^0Q!SXpDg9fy@DeXuI*WS}8x)QzKVwWVTx!TWQ%1wdV<*GW@tkJ6zuhe5n_W4)IVCgtp z>m`4%%;~JVvZj_Jp<5POrA6^n^T&4UyMvv|od#;xS@yA+qkOZr8wWtCguEoQjEhWH z^wpWuEkB_?7@7~G%jeB3V>r!eJd~G2_)^MCMeyRaUQUK!#>i$ccP&Y&o;ZvB|#;tF`vo|)X^v<$z#Vc>)E%2dXF6!gF<>TV+8anNAm4iF~G4S z`Sy;UvZs~tM*2Nrdh1}&2;^+dGRjLqk>vEuMt@@5jfV9{Nego+y*O`8n-(Q;^SNXE~GCA4i|c`dnd?4V`c-d#Vb%u?^!_^AI}5Yq_7Y47RPHSe4Ki zD-Ykw^@hEqZ>S2svnqr^#|7t23lwDV!qIXMI=jeStWcObdaY?uWZnu5m~M(=t{SS& zVgg>p1U#t;b&Vu0e{Yf{W0DsPH33KHo>!HB-lo$vRt`OI9v*^=m}++VuLtEOyK-nY z%zBkWshx*)h|>8_%0}cPOriYo7pZKv2Vb;%*tOG zl)o@Q;AJyu(0T2noW7Hcm(Dw&dwHzl5ezsD;=caTl9HN3GoK6w9^as|O{4uG!Jxbl z5R7on`F^bNBrqspCVg-){zV7bAzA{)vaFbQm2rR5W(&<})%aEOTIebL9U(O3;0V1H zcTsPj{I_lTBKqDB1L~i5Layd(6vZ^-(hhHaRh1;CpIo}B2=-EXx&J4^oOIBF#%*B9 zwd+0=*$`uM1ML`l*5$fxZoe90cfYy)CgW}G6w3U;${AyDlbl#J|JshwUI1O2N912) z?}_G_?W4-YNBQN!`&|z0U0!Y9ts2DhXW*h4n=9I?Tm_7G9wsuVP0lMOBNPGU++T=l zhCK0=&Y^^XY>o2Qqcj5lmTco<|25+kgK{d?b|65y`JwIubBznhJCOr>pq#zznU^pM zn&KU7Eo%{Cn3j*3NTVv0bY>LFTYI)%Z9E;`Fi~kxgs0$$o!4qSiPzyjx8V_ zG(1oij`z0U7| z7q?2vz`r!1uE5E#!vC!VU6vWGru`C4n&Y-1r`+TpAKVmdhKV>DCNVd_aZ-?FUaNMW zVGX}tY34w1(};=e)XpPf+RNSZyFb-!3&mCN%`4=0Qqt~b`k z@T2MJEg<0@iCv0IkEM1cZO1Xr*~xrh2{!fvE&H)(chJ7h*^Z~2#Wnn32l3&czAUmq z?uK>dmn6qP)<$es__JDxVHLh!Ys8PTtT%|>AH*u1Ezv(9U=*rfZq99A_gS0^c_*z{r%`Qgp@y>Id)?EOut4vM??$OIw^gE1&KH;{`RyDl|EEc;m z0Z1GL6<+B@rU5IM9?JDyo3GHU&6O~f2q%9ST?^ryKkQduv@Sjh(_3A9QB~f8S24ns`&}z6{$M$D+8gFU8w#Xg z08I%C2OSG@q*0rwKpQjpgW$m0zHDpT<~#|DnLr~O0|5;l@bOEZ!{!F3aLdMJg=K+) zTr}a5l^1iDzuffQJ(grb@iF*6opA|)pvst_k|4{Up<$X?m%oLbYdR0|bw*;;0so3K z(fylX(O?8c9q^|^cz+#YB6M55&Qvpe?vK|se2zz+kBvMJ37+%!L=x}ix!3vUr`!9X zt>64HrhdEW?lE|qWbTu2hb5e$IVQ$`2^;gyzo9`wAV7k>Hc6+ z@O?{rncr{%AUAW904tzjNqT|7sanQGM#&QFQoFG?k`mr%SB_3$!yz8o4G3hsO5kpt{BZ^2Y!M$$+58 z@GGopslUBDMi1@yi|i@vTEaH5tFJ<>b^6yyoAGym!$5ZRW!TlTF{@`(m!qy zz}ZfC(y9&_VP3&y(X%?+sl9IYUf=c zUZFj_2p7U`_VCt$>gIpa9$szcnyvRbd-!QF2@(h+c2D;3uZ`SQ;xiL6UQ0E6kpD4i z!SyG?ZMw+AUmu~=W4`-UdLugD%@u5&BRMBE(1H`uV3K&BGT;3pWy2ZnKQ`Y@Rd%}h z?i(-|`u{mlFW9yJzuNaz{~P=MRs_VS+xPRy)w_MKXl7^MA8!KWHUHz!L+q$L`ULy_ z$A=U;roD{Ef6=}_#iZP)+xPU5*ZKk-WA^Cx-HZX|<-_Zr*)|3C4~7!=H6( zagr3UTgyf!w!DWehx-#jcwtFC&r6rQ>c#$tC(Qnb`_xFIPdEGDtqr>QUN8k<+UHyl zyUzli2(7#=X62nAlAEdM&po(@iP+mLV0mF=lu}Z*<2E-uup8Y9Xr%*VE1;SMG zRGZDu$pW!`R5*|Ga5(mQomu;vlte`5G=k}fD&v7;YW4Q77gZQjo0 zyX<}0{tW=MU{?Y&2V!>*#9nV8kQD1*;eV7MCa@kO-HiT+`THKmJMn>u7l=)$Ur)}} zim4*u1O6|?SPu+;rVs_gUq<}3f#J`y8U9q%Nvy9K{>uZyAEQ43$bp0`P9R#yl?gy5 zGpAwiJJWxBydp6Cw_txd`~4nRMpWo&zrT#|vi0nO{eGe2!Ll9`gGZPc^qf3AO`4k} zJniNTn-rL$YIb%r{5y>$hJ!*N@-7N%-Gkx(WpIcZ{-bbZ^fdhQpZTW@{|6*+vk}Ap z85n=hhW~L$cp3Z~F!)0s)+lKKd{{VTyL+z&ck^B~9WLemfDHidzl3d}n@Jy>+Rkrv z3dD_CJU3 z$W<+r&UYxeWz%lpGS?bZq10?JSQoxsu0c7wZnpxtd-PsFOlkh+0}U~A>}P)>w>`d5 zqm{Y#46!dc#GLbAw6Z5dY`+k(hxapDIe(yq(a}x==fV|@R?YXRq!81phau+H4DPWQ zypUo_^J_tjt%o=AyvX)^Y0=3Y5bEq&@m(-iM1)>Uj8^P&d0UQ46aV40d;w;#4M!Ya z3(dzQIKPC5+zs-=9A5W4M?`%-w2!N*bo5O!GbWM7f7_zST4cEeb3Y}a`-py4vPM_| zcu8;KS{1rVri7v$EogGB09Weg0(~8;pMLtfOljW-Jq4~4U5k*m?F;O9sMD^8vHBEl zXdnib!1U$bd>#EnwM5>}KZ{2JdR;#|^z)ViZnx;?1O05{r>;{!Z_vac9o~G!u~Sjq z$^b+e?va&Pk)_KqoVL8C<3Q|lR*F)b1tdDuRxWLPgaBfe1qdq(0*H?niBDI% zzQX^M1(T*Mn6y+#Y1_k7{MQBp3;BImsZYxzwO$0YT_Vp2Ujl#Y9~2#)(T{}?zsM7M z3rdYt%pH49c(fQ*+a0nIn2JItf}bB9rG_#>ZIZ0x2nisr!_&q2P!>tb4maj$;*X(i z?PJ^;L;IL=CrZX7^mj^TOht6JV30rmRmSP!F8*5HLte>UkZfmy1BbLbND4jUf0=En z%eONO{;~3_#t?=RmX-h8G>O8OCMul zriHwF3+IEb7sTbcDCHR=-8pBGg-k6c6y9h#CAI>rwe{!?NN0m1XZBn)EfBg?0M>twJ`VxA*|O2lR-d zNk2!ia9q22uSKDHL1WH$<#vxNxF7XjC};C*D|cs5Zip)fL9M>7IF0KI<vP9LDWqY=@k6VaTbOz4=^j4GLI{cU#ec8@s^S{sBZ? zES)`x<&fB>OtzsBm`SZxaa9>WU(o_b=~lpY_`_hjZIn#7WcRy1^4(;O^$ z{z?oquPW2AV0UraSmOyNaCjnjCH~O-9})@QFHdGKFVArA{ z8Lr&!e7!*Bx|k`DkA?e5yMlic{(->4u>~WZSI%)e*{ke^_c*RT_>1Ud+vyB1@R~gZ z{^Bpouw44x^c*pvME)d#an7=TrXShMjdgg-o&sm{*Rro@dS2OgH69U;)6)_zzhgDt zvZugm{I&e;P0uHD)Rn`BTe-|@ndbNg5=kark6q!lY~t7O^IB^8%v1tnTl>K*2L2{E ztBVA-P1_=zNA81t^1NAMEC|$aRP6M9)NScFXBrWbOUyVM)%YD9%TyDtH@e&_-6xr; zCR$*0g0WYP`@5R?!Uf-El1a6dct;Iz{Wiij~PEg(QEc zSGm8(Q-5_;exz|Yi>>rrj+I#vpNkVB19=VBpV#s@E_3T`H_jn`nGv6j5N%B}7cZ$& zdli82V-&7`kr|i)y~pjPwm>%SI=STRKKeZ`rr(PoHT#7pa_HRD@@|AJ3kVCjr5BB` zsKnvO?K}+-< z=g zU-jnSVi`ufO4IuI8|?$5uC{WgH$US(?bIiC>&gf~TX_iiwMT*c5DRgxx(@^z#n_Z(0p%lJ-MA8P#}l;K*0^77w$F*#qGfC`F|nJj+;-^;hV4X9Q|+y10)@HP;$&9%%|z$z zYTPfG{Z2AF;sbgIsXY_Dw5xG{3(?HZ+6f6s$JT~5)#ukz&yekmDg7;Ry&INeolIryNxZ%K4x0L5! z#_8ke3p^rEM-K-cOVd$!TWEyf{jw)KVw6lE>l})a5+^ZRfA4~4!(Zn)sQ9k}FlQMU zv`l-j8_UIE#q!U}kGmK==;E!7GgRggq=x=})s^X5H*#ev@{@3073z};YCK8#X9Ohe zERvjrcYCf=b>J=x_^N%B8N>Mtsa|$w7{^owX!l;O0hV7)5o58v5Q9VCEAT)3l_oAP z!j?k4g^+>~fsRHk|#{FxvRplpUIi#G@SawzAyQLQRFq_Ogob3BZ zFfR7$Q5B=2s%KS7D^%4rdiGQi5UoMokW!Pdv3j7Yfp31WR-Me0f7X_fYLHc57wtpQ zI;9VX@jM8#E%rYjO+QiTUvNy=A~N^!cl3{xdjWx#ALcqM_`V_f{eFGVigCjJ@b8I! zH~4qpyzTn_v*`DuRlm;RO>sDRqb@E9x7^ zacWgi*Hhi=idkJ=GIuF;^;2C3!iT%pvx0iAg>(Q4>RFa==AFg*zKti>X0`?3-{=lL zVc?0WYXbaeqSW>Te--dQg!Fc-*Jd`o)x6s$pSMv!fa{yoAR{WA~wD@k_snUMdv(w?;(5cpBOm5{^cytst7M10BHBF`mwVSCHd zFOnY3^O;EcXCu#tMV=3jJRcc(9?ElMMibnBbW*w8h1z=ZO>Gkx`dG(dp>4EmEYM{8Wub_h0Fe`Uqk;qO8!p*9@!3ndBqxq%=_uGZJY0j@x4m= z?gb0aH1N&$B*3LAd#zAnLwp%|^ffDtQkhNu4ZC*0hKWPs_%$>K$~+rFB06m%nV4=h zPv@^nT`QPm#D!b>vkLoYuH4dJ4RZhBJzpW`$vxxrA{V7xpPW3# zYqEJKgY%CfXfAybKO>p*41Aztu;D+%F;phK_@q8F26l`Zscp>W8va$}cXD+KXr zzG!b*VaS_rZ_PrHx2IfAA<5e#E~l`R6S?j3{Q+ZG%Kh4gg$@I+^I9jN9+J6cQAY+& zWDoP+&T3ljFGiZ%xXfOoZcDP$HoK%;1$6X$Kdk{Pw*CWKPPpHC&TCPecRIU3M8;3L zb*K(LAx1N?Go5LZ{$>#2r)7z>Zn``G?6t0&x}7U3E`3!{7wOJ!`B!y{Gk)<2+xfqZ z|2{QceGMPENi&`Og&1NeecsuY&ZVpRr*qefDN;*c>=)X1oITjQqe>mVKx~ra1j6R_ z0m(_pqU5Ala#9s80XeeZrWZRI<0*19n0wk52%e#-FO$S^NcgDjM@OsF%e9TiAHO%P(peYqA0BRHjh!iA z8wE#;w_3QBFI$dn%Dikf>Ua%~W8uFYk&&)^#B2R5JqTrrOIO)tYfc}F=E=;qF0zMm z=u_xEnQq%7-4EuY(fw+pJ6$Te*F@;PsT&K2w(h)gEcUw&`y4xy& z(cR9w5#6<3P;W~MM)=Q?^nyTa6y51?QJLP4(p{?xEZKx~U)DwU|5{o=18HisOqrS^y* zbyuVeZ6iFL6EoUPh)b-q_RpIJt^Or*v)Dcw?lIOwT*(xwqY$?ZStcH8g5;T&JUvXF ztOvht$BtdN0OD*|O5Jyw`&ZoBHs|#G(Z)rR|IpDSG2`JNyah-|8;e;J=sE5w@?yi{ zHtg7v#NVYHXyOzPMN6m0YAPqk${a5dza2g=Vo1mz<8~HDmSOZAJnNS zJ{QR#l(NkNiv~hERJj$)V4s7Kx5M5)@ZUi;M%16viMu}BfqC{nK2w~Z@THj zRC_z-FQ@Cb^Si5KL2}agINcar6>d|XWNz3`jNlo<)`)w#`WH|&w5R2JL3|bMZOHL5 zj)%p`Nh6EcGAnv!jI8q!ay9bhBjD1x_~Oyp$vB#ANI%GPYjtEkKgGM4voET>i`xUm zI>q}gJNts(GRaAA32;q@SnFKTJUUA3G{t+0>#nAjq$btGi}W4&*&@*z)L1PiQFUdz z*RqtxRCAMKw%K$?_V*GZ^hZ;3@~+>xfBVua>Y@2pk(d2%=+LE``e{;53uc!1mv4=B zkCo5;RTAH*u=FMT#X%Pt#`3}HfaH^QS|?dr!EibA1M4K0(V>(vl=9mcff@xRksiv= zrJJO;tc!?-iWSNCi2e(u@|S^U#}ocN`g5=UH~qQA-}zpkk};=$z>-8LFT&l}I?9yW zJdJ-vJY57Z6s_ql(Mx5XNM#--xMVGLIr|KYVfBnNb!)H+)n;#si}|@g%4>O)avYm) zY7}3@kK*CjwccitMn9 z{s?rPKo#R`I+FOY*E-zx9M=U+SVMk^e(jYm9~l0<&?ObnBwuMfGnagQl6QpyvbHZ5 zc&+#5e+#m*kakvb>5EYA2e|j}r|VL!n19U{Sd$IDoTJIn&28{--FD~Dl)qHB;i_Z1 zNu^Zw4kvE#cnV0Fe?Gm!vXgs|Ed(pG2!|e_}V! znT+`sHD~wsG|#64lI=?)V=@-$k% zbi*{i1mnW=N|dAWS6XA|s=vCWe;K}vt{x-ToPB?TN#^Dy8h;${f8A2WTVB^zU9!|` z9Sgro3+dEHuG^LHtx9JXiWM2~k%$|w#S<%V`!8A8S7CuRP92P2JfPzM!;}6WU`L4P zSob7WVdleBAiINMi&Z{pdPzEW$OC4q=Z0PGti=Hj0P8KPlxS3anlWP z5;|fwA8H$rs$9C<^NNn#Yy%&;Nh{kVT9VmINFVf z98p0LIn_oPW-;6&>ZT|$o4F_;XQkTlh<;Xy9*z{y#AYZuA#z259(J~<=wavcnaQls zo!9{+*_7vL9LY;s>C$`s^@yALc#!fp)sQH+Qv z#x+3PuZ%!A&qRDO^MU4c*N8UMtl66SK6U{fnGczS->bYvpK4@z;$5EA%#YOGr5#5v ze#<)tlXM7s^b%`le1|tqy&8gRG4bJyV|iPpDJgH;SZ{0mCUBXby2%L9yL9gI%kQ_+ zKwEw}p|n=?rOVk%V#R!lHLZa%ABllh^T5TmoNCHGJN^@*TgL>lj&R)s3iISPG^9CQ zBJo&!(rh`v_x6Z>PX&iV?o8%pPsb%==a&2TzPiI56VxX_Yvk~zlvD^$@& z>2ZFt@3YyFWry!TL^Q81%HXNU_P^T`)v`%GJ~J;>)~85sb`c2XP=6B&$*kR{?MyOv zqqeP?_-o&gjTPJ)^0)rmfMm-?FCB;^(HSmYi_a>wT|@&4>D)1w-o_ka#mlnj+WiMn z5W2tZJ#A!|6NPne01FSp^#*H>U_Izy%@}3*10V|la<}d?qp8FGVomkGZm@J@4uDT5 zgYDI$T~$7sWN+b#P3E4&$y-@5Wsqa`{{b?+~O`+J6M)4z&p(Wi|=xptS@%KIwOJ?41B8?xPJh0KY{$BL+`Kv_Z*( zS_Aa13W1LkdUN&+ytxbb1ySJB0^p4T9#Gp)MT=I`SXqnYmu~} zguVX{4%Uo4z`D8%R!tPvi~!ag!3voD%G({RBliGnWEZT}1Su5oIx>Lub-^lgZ5$Th zwUM#jTN~d)_XKSm5XEcbbFPiAA!6_ToP*UjfOYL2VEwKO*5W8$*9NeDCRm?$u+|X_ zrmh^b2UrujVBHdhH70;{l0<(q5fhw&bQy^2nX^?|CGWyM864E{GGjexdot_#*Vb)5 z+z%~htnz)T2z&R{4X))|k&<^a9T2Cy@;is^f9(O%Z3RdRBanU;K>C#+kuFG;0Zu3F z0n(HLr2Qk1P7WZ|3lcLGL8=RID&7O6Lkf_-9D%fV0LcSVzLn+bF8{9FPV137J?`aN z$?>9I%SU7-gS#%PQ{@&*hTLlT_r9M!;S%r_$zzLuV)@IMRFlEw-;WpKXEGz={tOBb zI1>*_U=LfNiB{;_nu)K|&0k;V$?}isJ%5GeKfjRwY@URF+3xrkTh@6lgM;44i|~Kn z_Yns@+WREy30_M+n*X^%{=0V1|7**CQ8fR=LjFa&=Wnw7UnJ6?K+l0LUIl#WcF#ZB z^8Y#t|Hc45B;t0RxRrsD14F7BN%yyMC9GsyKj^b4`@RctV7@}Km45|sk;?8?{-H^t zgJM~_l|M65{(b*eE4x>>{%aq1Uax&5`;XzQcMi{{<_r(<8c- zA5DVu7l!S92hq_>d!PTV+Iv~g^52b=@AU7ovfb~WyTuB=Yz2e+^^(8Q z@}D2ge`c7!u9y5ZmjClaBLLAc@Mm~2_=#Tf548OM9fkkn5dLd4OHxZvG80_*R!5e>sK1e%vbcE0AB!F&HFKuCAM;#K%B9A zsbQa3D#kGOzZjN64|+6|lch9c_z)v6l`V>Erd*r7x2Q1Hh>2D<)o6VW?}0$PVR^f0 zt_>5!7|*&4j`1EccVvqi*yrvq@I!^Oe&zb;M=~ix_d%q;f2B@#cEu0S)sa3*=f1u3 ze<0QT#~ty1_zZt@#UK4L(pAY^tE~> zmL1r9pSe3N0@Qj=uFbILvcu-nZ>?&TKU%cJqlrEsGK)FhD*rFt0Dy?~oB)i>HkKDj zaeBL#n)QjL=duSJ2 z4%i7COavbt1Kfbyc2F+lHviB7@>hTyVmin{^dFOm=!J9h0^pFG6NU8{w?=DxJ^K_` z{`j51Z!h}Z3Z$O&y6!B|cM36z1$x!tq8dF-^ou01jb;996n$?wp9=Y~SS9Uc1 zhJ=or^kn|b-vBS2``SeAut?^{$2kM7Q&*h7LF?nRQdcV`g1<-UuYaDJN=|$(+>SwJ z|I{Gms6tA$QYJOTU+SLq(!o(OK!-2vb=WKJnB;YI~W&C&Cps zt@Q;fl_^eAHT~CESldfyK9DH;qe&LGRHuD6`>@)--MU?GYC1e>pf$laqX{BL$gPm5it+~4BS00QA?vfkk+F&*HzZ-Aqcx^NsA z;HXsLsIr975g|cjiV{@G&<(O1@EG3BCNcs};={C%MkJ;d-&^$arq_P*{?aur*F%uzsSo zZT1PsleY>Cw&@Xz8)AU@MwEDai5qcksydv)W&dlNYsKixR82UJmHtD6|su@5_+OF z%AxP+bnh4i^pBOmn0G`=yj(zuLQrvOsFrw{Ra5+9iYY^dy(Wn_BSIVnR+Y?F`xZLE zb|yk2A!`*+#Oq~e`su#!`$LWSi_aWyyZ!OqD?_$;ta~S~t4w2(ZnUk5F&w;pJ#rli z%hGs*v2u4zSh{;Y?w5d}>wt5G>M;F#0i$K;d;XWC$8s(4+bp|*8}8Sx*|BR;P5hg> zmLzTsuyDJ(rE<~9Pu?JD^S-2B814m@CH`-uTC71#DsUebX{(~S51*tmXDs#ONfw>- z!f)Ei59)uE;AS?Zv>)?~&O4GSocB&ms*rM^!8Lb3%jC^Vh2_zAw#NwohL z=uJ`z3n{l0Zv%1LMl_7-s~NyrWc)47!3J!MFNE4`<9>pSHeqDc{OURzHU3=boM5wq>;mH!>6U<7Y{EIh zO*o&>NQSu>ZXugjcPn0G#hX=}`Q~sAA?Lh|-3tx)q(a7D-=okTIAtyM2b?k>;FRYc z-i=e%oG4CFa3$8a*!ioa7C`exVD$7KwHOc_I?CaIP(<^U z*ZiqgXHhrD?Rd+tDJD5w{tC@E11V$@HK@B%b)P{X)oyIibc(O`?4DlZuOQvO#Kcn% z-f8-h|Jz;*4fL;3p>I*hKSntZS(D?_bq=7P&de&Bt{&jW>9n(rQpF26cR~02&7^ZB4>WAa zQ+%<~1CgP+{G3#;bXTYUC5i^R>;-YnTc{>8L!!EDv=DsHKV1Sb*Am~PHb^OOe)Ifg zJ9cH3b1mR^k;28d{8&bLV*i`?D@zcp-M(#(y=&7Q?&T6iG#-naE7yv3T!0BDSpa$% zr_|X&>O}tu$SCMutHl>)i+=@WTq)r__i5JkPzG0OZuYY0Ct9y1#0v+Q!Py z7fi$zf8zwv>t3gu1Nk@8-vlc~iH*5p^e#SQtkr_xcU|WHhPI2V)#nAF$BD`FR)}4C zTy#GdqV);J%KjJ(`M;QuYf-YAgi(FnwC|GF`F< z8)v61uF3Y#RRewbfSOFv{F>~j?U~iV(?_}LzRgSf>#g#Grl(Tqv$D*a{sXI#umHnq z-V|yjJ9Y{$M!pmxMJ0lZAKN3n4h2>hy>3o^BE8K6iPDaFoXz|B!_o> zo6Rx{Q*?`3tP9z=`Wz%?(<2=l{6ScJ*1wMa#l_q>*eiX1Wae8)-`Vahpf5hNv+`FW zh`V~!=zS`Uw=Jzz36|OqpzE4s8vYN;LJ}g$*;|GFJE|M}LwAS&dEmnrXYJb5g)iqe zW|x$(oNp0^$0Jgc&3DCk$M8GWG~Cx$t)VDFZ?~7zszh; z?KdT}_!OmO7T58IY@>?!K6kv!H}_2aEu1fw$_?Yl*v<_#-jai$|KMk9GF;;13ltYQ5T@t z4tSKL8QnIp3#^sNES}(CtND}^XzLt+F(@7c$Z-1-TkP<+J%#LjEL~J#z&i;P=7xCh zsCCK?t`jZ0vXw4M@{GLj?!0XU4aqKUQ*&UTkbOdGXKquSuA6JTJB^)O4-b8eK5khT z6tsR`Uy~sa*|=l1amQT89ZRPiYy789v%vBjJ35OgzSA21mIK-YPDuP;3Jy=oU zY&{A~29+*XQIX0OHFmCibM`*eXf1fy_S;uJ?X`{$7$x(xjP%Th&96y)Y)wA8CArUs zVwx?qVQYRFUu}FR+cB=>mR+4}hqV}eY+$A$+qV3BECB_R^ImdlN{T%8-^NDp)j^7I zUlK6aowtIs60Q;F=)um-Bha452^<);)F3a^NY-}S&NXb@TTN@nX&BKjox1IQ@G?>) z2?xKVPet@BvGLl~TephTI@_Px##8-EByruZE5!?W_)gfq`=S@Z=7%gzHb0A4Z{v`U zRZah?`1nP-FGE#|6oli~FDwHk@|X5a9`UHRXep7f`h1qY)7@R)+25be)#I+%vA4J2 zWo6@-PMsJ1upWH1N(WMN)68fu(NUYRO}3+F5$d5j%#4X@3T|W0A zRk4om%k=3%{XD_X1$61TBww(a?^RIe+?C3>P2U*-UZMd4dKX~8T~a56o(F=jkGkUL z62s$pJ>~gE_g~P#_w|CpGkKM!-LdCAqfBbyx#YOOQ~;?ua`oJo1wY6}4^H>rc-z~I zHMSZ2>tE+TC|>Zma&lbw^k?<;G5!O!{y8wI>*PUAspDRv`Zx5TPYvqMdQKv*1C8l< zm0A;lDFSd1EkEL=dNrvYk51k59No1UGE(tqZ~9|zygi!QsN2<`Ua0F(+H*>KPI+G8 z2BrFG@x@AgMX6$+x{dmIO|LKLXLF&TsO1W7HQ-zIuFC3O(2vf><#=>{gi>7xUlSYk zl;>&L^9ONkCS`Dpr0}k;tQKWI3k!YuL{~y zJ-tYt)q>bg(y|rGT|CWl?~t0=L8m+cYdkSNew*$Qzb#eynAdtNQj9)k5okRe&K2|; z7uaAB?-DjRks!{G!THJNP1~nvD{mAz~u8A@s}6tk&S_8Zr4J6suXV=h;Iz*G!ng`4VUKIA8)O1%=;dxo)h@ zaPOO)L(#msZ*re)Qv2x4y0-i#LP&?UvE|6A=KN-PP$SVBh52pq|7F>#+k9(@%p5m~ z{p{VE(iVT0EXmxJa_KW=>VH=L(Yx?HXuhU`e*eFa%g#T(BRT=o_lSRz{px8B1p>Bh zJQ#@KhVWV_iz}JgnaaE~bVE9~#Ow&Kbb%RSe7;q71@G=}Y@sZ+Wcvbv(La3(jJSnv zhETe|{f#+F@m(g8sKn!9N_Pwl>C6Ijs`7cSB?BPE?U4^p%XazVTa5;}qV-ca+PV1ZKkbz}WGsoam^!ke4xOSD$Y`aGYKI!u8oSS4L*+r>${yNIf4 zGMe;FFPf4Ww^ypUB`%~4k=7Zqew(A`7;0ZB_r}-Y1UjT}+5I>1%tbG|r%xWyYGR3B z`3LovjzlnL7l9ALOFmE`E`)2cKw?>5yG%_@2w~zcgm895TovID0aKzP z>_}%zwld+|_-rKC`*-fxWf?4MGFud{Wkp#v*}iQ3rDkCEk_y|(giMmcZ(Jg}g_&0=LvZc-g-oS7vAUdUfw zMx$Dd&9VGj;sL`q+gyMz;hcbVXra{JDh|`X6pPp4Gs!nblEY_e{gvQh%0Tkc$=oU~ z@2`ua_=%dB&gO*d6mHRLl*6Ynj@liv&6__$q^o>@m~b8l%QVU~f8c^4t=+nOHCKex z$_dcl-C!sS21An5{0vjYg0QB_UKOuJOX}2-6-rthr5GL`DBzYydF3))Oe0gkn6k4! zSCjo5uh_dQA7_|(E8oTzTe-Zyw*=qF;AKkg>z*G&UE?*-Ax(A7#z`1Ha(&swnAaK) zk#ZU~S9nF{U|^W_lQoXws}7auI!|KAO0W4TxLgr8YGEy+pg=3kgfoh_&$RZiJPOUW zd-D}aU^F|4hg7a5kRuR{VM(wQK#XEg&SZy3lgj+apx0zV(naKmqg(50ut1U|Bks1v z>B>I~8L#=5*qsF)-^Maa7mvf>h540MRZINMzr}|d2ogo6SQo(X;jk`nd_;}}EPoHO zAu2bD{WX78xR;Y83i}eU{DGh&{xUjvpTs+kZ#a(T5L`>fzJ2qLm=S$g4b<+-e)9&d zkiH8ss()S@bw1y-1`c>g10ao*tO%rK!rzakNm`5#To|$l*d;XJXM@zNuV&3v}#)c5#;U+UOrfu?TShOtAL4J)c zz2f*G)GH%l0Vh*MibthGG<^H!mjj_oz=rfl;~cTOTZ&ndL2vXziN3#yQX=ebBSn|u z=t+x6hj%B&^4p`th;o4ExBq!JYShBdqINiHLdB7P-3X$7rJY9Vpdlkz`-wJ$#THY5 z<^J$ryM~lIVu#|HYw4{+5fJL0sQL?gln@Q;N~@~e-}N^Ea?)A-Y9-61{bGK9+F+x2G9w|P4Y%>2Z=<3EeFf!^_-c}o=kup@{u54)k478LrSM-xWSgpl|VbX^<1 zb6dAISPW*Q55hjl&$PigSA$3P9J080SfkWKY%pRL??CK0q-+c!JFtz9-<3v~fp9xR z&K+G~+<;FZ_8rv86C@tK{W6tZwh^yh0>4HA@5T7#Ops4oJ;ghnS!Ya38ake>VUrW; zL$+N$fP4sU0Z7=?*CXE?owTJ>F^D zC_rteuct7tgXG^-k$MdDIt=|%^%7gr#M99FWSbfT|HL~g{~W0Y_KG6k-tm1XQth1qS3u;2Iv9l_VgfVV1vdz4S*yuBDyLe)$NxfxHOXv=EVC}y$?gTPH!10{4=iw;qQbA~K6 z9$H5#tU+aT*wM<5*Ap_>$uK%&kcK4mjrQ=Tev2A`$BWyU7S1eosBh%;8E@eQyn=c| zyj(;QOC@yq5tvm`?F#;D#)ko=JGp~ZHBfD!`XTEL85m#p>~S~ z1>TSi-opN>bhLNIMs>(~Z{gb*OuWNMP@8LzaQW!&iNsT!UPDDzf1SG7&ZrbF5JnzN zhoK#QBbOx|&j*$_aHl5qd4o%gFhU#1La!l@dJBI}t=0(P25pj{3eF-@p`*Pa>%4{I zgObq|aeA~Kq{#UAIBFozp89Z_Wia9yG=Atz zvCBjwCJ3_L;SE{lE!35Gs+zH-HvJ!7txZ_c)H9r4`qJS626DU6K%*ZWh&1ZuAJ7QX1F2N22t zl=JI`pp9vG3J?_{3uT;y(|wxM)&MR;9yc6>s4mZRBbA;j^kz_fpi;!*-w&!+XCf8Y zIO;kxU?9Qt7A~;VbLd;fmr%mwFsKRD^p$CWK1JJfTnMzxidH$GY|5sp z0BDt(^8%1K^NkKXhlphdnyS#fLSnnLmUR`fP>epIu{=pA!So~x!f?`aid0%m?qo(wUiI5B z3$)hiPMg|9e{}LT@NHD#Dv#JPWu;~Wrf!pCDtj{t{(*ngK|w?r{bZS&cREem6}|K& zJYVxxNHNPl0s!@)&VE{*KLtM*XL_&o@5d4ZORQ~cHa)~3(l2z+pU?EjKGuOX@~W|H5vE zx7Ng`vODNI45!6()SKRdE6{?fT}{v$OybGWT%KVQ+N0NBu@|=!DuWw1m=2wci`f1b z02KN*w6T`W_!+XyOs?Xee$P62|1V9mzu(?+k%}_UTv%ngwysTdq6}if#O$DQ0rE~3 z&@T+M=Ltx*qrXtXvnp3PhfL>Vg;QfK7>*LfFIjzDap=_-zd#8}GDwcFx^(1Ky`&Cf z{EYqkSkC&kw?-n?-Vu5C|FtX3XHDLZ#vnM;6~x+?o<6#`nH)mm9;a>La>}I<`f%EM z9??Z)#r#V=!+%>|m5um;IL|I0l-#b@`CN56+E;`8d8IIN?7Ccg!HUwVYXc@)!j>U) zh#WU8Qpk=LhBxcv4C`0snArwa`k1@Sl^FeXZ6t$K-0K9%zqrWh!ud69QXX>QG9)D9s9-%t#fn@G3Q zx(SSARBqzGF5s-sixi#8K1ZgJxSHjWQBGzJwvP*WN5qwv5)^*T3MaH| z2hF`>N-aP%{%Jf4?mBgckA8~~zsj7k;Wo856P7hw}nj z*vRY~0DP%VRIk5SHP24)bGgtXvPk4F)$#uI2Z>blVC-Vmq>)E2#xAbl1tUu@rKj*E zc5!(Cvi@{|EM!!TmQkO5MrE8+s7>vJU#xH`zG5+t-n~9FyfJ^ZKU#E8tgOBtv!hS`B7Ip* z>2*}a|GGiGDmT4h(9Cfnzh>_;B{d8Ko=@5`T-AZ)HJ znO!VbQ&%LR*+;oyOn5stDB6y|qrKpS!RzTHM4RI3N*4O)xLIo{_^L0B+0<)bEC`N_CdEi`RAgo)8L?rDk4T^Qj zsWg;~5-qaH5&`bq3T=FyWmBv=Y@|A74t1MIGI<7@#CV=etas&8Uh8RioYf5W;x4|; z=yKas{mHzhR7wV=B=Jaikpu@o(*qJTM1f_$!=_z6u)u~ z{mA|u@)?`#f1ncDn*>uVol0%>jJJe7 zp#K=9Y@{i*MKb)=`~S+78{Sn;i;y;yGtEh*X0Daw0EbZW@pvmKhE99)w%`$%Wjs>V zRQG45`?JXXxzYX6;3G?$`?Jpd*~Fi-cE3+Kvi>~pb)Wf1I{VuavPwXY>*niS&qrNH z`%2RgZKu&|Wc(e4AZ1D_2$3Z>$A2PX3=$Cczoev38Gql^F5))w>Yt{)t9kZk{fKd` z69f8kbh54U*3lJcD*TB>!Bb5{5<>7K|o{{8~ky3)MATs%u=w7XyCN+G1_xYhH6R!#FjM*c6akc)7;( z$mUtw_bTFkT?JZJKGpQZNN>sb?M3QB{!2;d-j$dOy}*Hczpamwa`mlFpe(Zj{gml? zTWf`lNgBvKZS4{$5?3;G2$61h;{*tYVlZFbhiJu;;lUD)}WB`NUR z?$U9?&E^+2`lJDIxnc`4gcZkDrwO8@cq@m^5PUTlb~X3sNYVM`r6Y03 zsR!oIW^6AO4-U7Z8q(^himq#7)bV%FnX+)IIR2!!t6NsFC@5YMtEqPV)3cKmoSH)1 z07>@!Hv8(n-x56gXa9sF9MbX0L$`8S)7452_=Mg@j&#aN!J!v^cuC9jWcH@`IyjDv zJ~KID;>_u~r3D+wu{X_|W%_a$69Y6KLCst|vD*0+SvIh6nH(E;2_MmL;4+z_c#?aa zf_MU!)HPz@V-9f&E{s@aH&K*Mjd8HkWbYcg=ZZKLHAkylpP75FwUJ{kw-t$6fp04K zB=Yd-Rr{n{SxDc{Zuan6H<4&P@h8?GMdmtxf9R3S5m6)PEQG76Rh-d6^o$S-JBl9Z z-ckFXZAaA1>{}C1qN`_coCycnSCietzt~rQzLurFc00CPa4-%g^IFc>(?2|ESAUZR zrIv&jR1D>^u8z8(KOrf$SUQsr@9)uFTB&TV53Ne(Mlw8^%!%TujMYFu0_W2gEp@~n z=ZJsxQ#iI$EP<*}hQZmnCYAlUj)E!h2i%}L&oJWkRd3jx2rb6Bz3LmT^JL}YUh7nJ zA-8xOkgDA1&7TD5bZ*$Ssim8_!e@4gf679-LTAuSWbLz6m1t{pkIOP{7yj%B=Kc|X z=nYy5z0X=iFS{#zMnJUM%&(OgAfCz?#`!IB^)05|GVgOOE7~BU2+*y?*SrI3p0Zu6=F|9wQvn;!IXu(-BU(2-}~_@xUHy( z>-gGG5v!YQAE0`c|BsDgH_FGdA3s@3v{^p0K_iM{wWZ2%43q^fpQy6nKB8{QVpCLE z4AZ3Nf1@mz;|I!OiWv0JWUjHFl*K{(x0H)vk;Z;f7D{3Q@0GfwBu|HSPm;1wk`+AO z3QAcNU@7Tu{UokoyMBSPD1g=4xG5?dMpdz)ZDDsT_&ywK7D9flO?4Fy<6ronY{uOS zhq$vt{Im34+Vy^d-VZ9gr`lIVx#D_b7vLl(wZv}`N`YvVIQcWZF0mCOy>Nk3nX5-! z7`eB~i8T1wU0pR6Nzc_)19ZXEd~U(0$#M^u-QsW*J7mw}=Bjs^F3KzJi~Fi@+Hqgi zs#Gqg;}^KEYK1g{NvAUSA=DW)nYh5sOJ#l{nMVi*L6#G_hHi9`D#KW6wF@_bYOlr&iiZ^3zM)QRIzOrpm2CR3X@az!>B8WatK+4?O_0#> zEqyq1k7jT8KhD8CZ*jJrjFBTwS7%_7N^Eplqxs`N(|?6Vb^O?WUVr}TKczoE^AE$= zwGE%_WWJ20n(+CTk>~Sx?s1+^GPB)Va$=Ep=j?vSl{Rn5H;`I`zI&<}1~=XX@i8U| z)MkH@`HbaI#v56$3GIzH+vifutyqv%%Ls+^Ga1&se_(JIK13m%$q?`T0|UMAp<5jc z_xiaG&$_t*Li07?)f~Pp3fll;i-PYzlpt^o3W1uZywI>)UBl!G4KuhFhabu#9}J!F z!|*+Q7#I0qO%6*;?Lx9{QIPn`W`;~a8WOf3kv=4<{pg;(&yhrdbinT z5O3dYz9}J>cKa2(O*IpwYpTi3t2%wdLZBaKpLlHspNreVIC|(~EEA?OijY~`G1TJB z@a1wQ-ia#Fyo#CJ+g{7LtemAdp{Ta#e7a{jKf09uQi!ZlO3(6MRV4c4=0+tGcbb9)V(y~YT{ z*Y1I{vJwahYOX|*X%*_xS~0wubFj=xW}UT_&w8yx`K-Gi?OKODUPRez(B5(vCYW=B?0-Kgd#k_yE+yy;t+Np~$ z42efn*#MP{p^`tyNGrQQ@?0zyabFxD)aR`bLq^;e0e!ktJP}YF%9}6UeC&|D%%3Jj zSRgx#v&qc!B0J z&ELVE@15qK6U__zds6KyBU-zI+ci7Vn%CQ2jdWHE8d||9Q>7_p=xBhGW@sD+e=x3dA z3)}yUkN?qrNY9D(gL+ou!^Dvr9}-HZclGc8`~KK6D04Py?8h1wZBaRk%O> zE9UiF&#OKom04G~rFq`#PKDb6Ad$tr>rB4l%hUSaF{Z*F)^UPP*|MuCtzAu6@wJ&( zlbJVbGpjnhDT{T^d~Ic?*JAf2MmD?H4jOaMqn_nj!CR^WB;5WWf{uSC4IJp|;ZoAU z4i|69I;XiY$aK<$j4d4-Po7j1bCXfs+)YO&oj&V- zOFFNhF-)7kEDgaiO)EIiN+&?mWgVvk^U6#<)%>yPY4c5iAbQ%}SRCkS7I@7AwI@BD zu6)^Rc_peZZg+ZmTX#MEUYhHE=l_a~=#0ym_XT>I>!Y7)48}x7Iyd!G^fae(ec<2T z(#x1=Lnfy)|F@f-HdH=IPdAP2O-J`Q0Xcsrly-87p#g_OM)5ToWk6gaE|I?$pOycM z+3<02 z6NOEdW3vO>K(8oCOdl3S9a~i@ZmeA2Scy}I;jp4+K5$i* z_y7NW{viq;sQg1kbt)_@EG#W_R0jn;=)p+IC~0DXVWE!j#Q;Y=_|r6srRoZl1-QhpUFI9V&3ZCq>{SHMCfmn1k*JOxaCDXU&s31i2bkdT+ao$)?^oqvJ2slg*XICg^ zUu<;F!WAY-s*8hY&(&eBqS~2;U%)Jv3w3ka%2lI{RI zR&S8Z4O_GwB4I<#00R1C@HsHYB(ZQ!jv3#M<3aloIZCk-8HID8c%=_0g*P-${w;m|KuJa@vRNrHf?c7V@)O8nqozq!l6!{(?4;*2tCGr- z+7!uRBfY9@2Gg5Cg^JGc01eU+I7;riBP*!ManIGtn5u^h4O^@KGX6Y^(P1v7giD9X z_s*^Iy>nTxaLLK^r0xe6N%O>$G$$@|hSb|YCmNVGH3z%l^QBCc3YVnr#TXN)NLmtM z!JT{URZcY%Sw)MkNZ~W5c@0XDdn%aVHpZL%MamI}9 zx0g4z`vq*95P4&R@@}m$^pjUMprhX~d6~uVa>L?>*BJ6LJYeV5cb3=P z{ltx7^16faZdqo?>y9q(mC?$tq2D`=iKJ-qp8cQ7+h^EG{`wXoul?}y%7?=Gm0v^N z-_w2%px>Jo{)hUVF`&E!)rZyZ}tjzZ)507_}m5 z`Mr#h=>YnjNN+ZPyeIvq@_tUOiZ1W|%EQWA$fVZ*@-C&j96;VBSO16ly>LKzvkx!t z<$SDj0C^vyKaP}FwP&vG7Z(`vy1rj~=IUNctqRevE7(4)V<;e8UD4Zzi`7@KBFeAP zAKyzajl9wN;{;RQ@7*7NK&=XqH>5w_eVrk1QuO|9g8zG1p4VOqnGfAF*FSEJppOjH z&v5@22kJpM{nkKv3#VTk=vTw(w+8yJaQcgZej=RyO`tsvr=L9l-7uLXf5QFC2Pchk z7EXUhW}dVq#*(Pa*|78ShVcY%I6vViIfb6;xJtcF&EP$WSrEq+^8VV?3bD$Jy_Zu{ zB_!@{{_^=Ne#N*ibM;NeKBqkwViy_nDy&l}Ib;cz>xg@2$xXssi>o|3x`*S@v-(Q6 zC*jE}J@Q{ZeQ@`m=0k0Gmk(XY=QeUG*nG@}me=N7x#w;CH_13!_^FGf0<3;dsywev zR&vf!7g=Xrme=rF*6iGdKW1H;8*;z*A!K0MVvr0t^SMDjUG^8(Zzp}L#}|D`YF7_h z!6iX!Ac!w9**p!IDSCdAcS5i1*O!^S*vdJ16W*wtoj2i=%1h+?wV$hWvLVRO`|8P( zo>x7V9$<35$FkjHn_N+sp;gX@n4<@3%$}o3_z4Rd`8@}1;`R2pChJ$L82+gM2Es_or4AV@?hg<`; zwvLc&GSi*5i5t1o>$8OVo$wRSc`LsD$v-@D?|K85U@h;nRJF>wVGDa{c?~a7GDD;CTG}NxnU*5P>NmG(*0EoE8(x)QYkx)bGL4kF_mlv?ZQ1D-O3^k8 z^Pc0`EcMAxhI*FwU33%sD)=T&)h^GPGTL>*u>b5Cp&8E*iN|C4Aa6oPWIP_r0a^w5Sc+bKVqkxxxv_$HE3RDjwr9@_I+Yil=G8 z)hd~XUNSwe;hkK|q%w+X)sR3j#d~c^Iwxr|RsO0fA%%H`Mp#vZPsL&EXM4_5y+NIn zH-@GEw9%cmDg|}cXW>mXym77O?(n1YC=g$B2k^neZ5mygRVNqx{YFF zc{g{$et$gPhHZ5dBpy_$v|Kf6?CX1z{p;D1pQfJQ6g{i_4C;B$4th2ze3ZLAt3-Fy zH$$DO$|?0K#a@BOJ-<{MH>^WAolX%fSb>smx;ICafr? zpX6r3&lF`(32*+rKhLn-60zUM_->c#j{lwSyP*W~8k1%^!!#QA z)j!H-7e&=IdCV1*N~2xO(dxi@tem2f^>8@^VXTX*{$QK*14e()>F*C>w`n%d^2x=h zNjAVAtp2m~4&DhbRllq%_=G=JKh>|mZH=9CC8y)ZZ&bat(VDLNG|(56(Wg-W<;Lqs zfnO1UUl4(xWvA}fduB?{5OQB$ZbNTYfvDyy?x&IMDKz!WooD-dh8oo~9GxU;I*}O7 zp5fh*@>t8KSETErXUOyLEmF4(aLah1Usosj8Je^FFGNI5GrCk?_^;}8Wr(;f#LZ*@yu-yoS9B28kHpM6ADjAG;nyV zfkQQgoB(7WOd=~P@RFkjh1GJFtMJEAFW<@cy40$|>c?eG$q?=G?NaWEm4g<kCnmZ*a=|+i_>sJzv zs!_3*DoNNh7FuG1lF()1mDy$fr=TlI9vW&&;u=sAwIUUe#8{IGDJOhX&X9z|x-jlv zcP{O(6V*bX4Uq&ZMM<(c{)m#00Dn0N(T0khDnqmB_N;zfCPi5*V)2beC5g(S*3KxI zsx;ok@|b7MYPIv&-$uynou4o5tDDB3c=-OMOp?lGAw#x1Of zeGA5zYKk!ffH1}lJ*>X@S#wfyO@&%|V-47!0@@oD(bG!W{wY9mYOs+{5{VfBiO6q| z*ZT;w0=8|jDo_ma9|l-bP`99QBs|6a!fpOsxOqSf`=<+h93RJ%zb2zMKWMLJVzHY z`Z?0D94Ti$mBHYVIr?+vTckSn$Q&iFrK6G3^DR>Smv6_>ZF2P2DzT>;0{JZl7RJ?z z2br_Ry**D5$%3YU`&&WxPTb$xGf%m5 z)i!%c#lnNl@{c&)+$k)!Qq?f zB{C(IDyPDzt!n!$AHNDYKP7uko))voJQa5QJek>gPVJ7s);3OsoyK~Lx8XrWG|iC) zWzg-ZR3ee?z`BuNp5M_zvg>4##tAj6l4Wy#s4T{6*3bwU@=aqt!8M1JgU(S z$a8m*l1CBE=Bz>W1bfQWP&Jx7NB5J*^?l?yG&hPoF$2nTo0>30=uAhn--$OssqscRS zKzYtn3w6=tnb=RBU3uR_p694a{mS#U%Lmd=fw_OPfBm|XzB^n$`QJyLGo#6K-hlEf zRSOK!^z$kGbhte2-tVEG`>0C&^z(4xK>BG?3mehonbc37^zS22Tr_z`3@DGZ@~R)` z*B;(0ZERS3xMBMD(9e&k+Wqvi^0I;S^O9N!i>9A{VzY$HllXn)*-KUGC(qZj2a;#I zTIh-<&$NE>)J^*y`k50=o&^KS^Wj(i{POpw{sSq$!~&BKPkYv+sqWMx2U0%Em4vyB z(BJXd~K zc`~EPGj%|DI=>uHo|&dRVdlmg9;F{9QjcVAuuvk0&|9cU{q*xpfn@ySA?60ghuAKA ze%sSe9>2Zun6e%F**^&M-Rz%e0-c6H(e0o6zKAS8etE1>IVz1hERAK`zae_cI-?o;L|^Xn1xf&u7{D{~G1p78S< zwgKpL4#xU<1pm$c{YMe>*a7Ip{&n~W|E>Q01rhYb0qAA^eFqW#+f_H&Uk^zG&?^Se zzjP`SLK@s>awPOpEBa3@zUZ>q^|nEfFP?Wj&2WjeqKY1`mjna7iCDyh?tWh=ZgX# z(_yBbQK%eamJb?csA!Kh9VFn-FRNQ)Xu^U^jW%0@S>AvauST z)6mBWyWo1$0@h2?xa*gt`!f>OI`Nb<_Ok^!tY58l@qz<0)#NLy=40Q>VWqd>^S#IN zo@r00@05&U4Rk>kCr#JhBX6r^73I>3tcLelL197suU?DnW5~&uMU+fi^>)8{@>Uz^ z^vZ=h-w-tTU6Df}->XV^rV6>9VfseRwV7Y8dKSqzfxJ#BdO<__f`%F^l9uuHDv$bl z)q2uzh;{oymZ|a@{zbZF4VKLkd=I!hB`sm?DfACU^7fpBA9GwERld_@eRhrids?CL ziBzF7MtoH(T(*!e;_TU~8YuP~Sl*K}tFF-R!|Y7B<%UxAF%Gk|$?A~tF_FaXy>4m$ znO{{a(f$%Bt7L4}u2*G8&Tg`{9Q|`ztba>{95G>{x%*cP!G#grpoJF8gzl#>*A-cL zP!&ASES>(Zq4>46=R$vbJ3x9KWl5Efi$qD!@&DEIyiZjcC_R6}y#BW7ar)CEQ(Kgn z+LW^rZttYgXKPJH$b?i#fx3Mg&Oz@#;j5iesrIx$4%RodpNS0`tdstM?L=IcxY};s zzRA293z~cgotva&uhSs;9{RAXd;xa|Fq0SZrF1@$ehiHT3w6>caOiFO(&Lxg)bv_L z;4pPHdoJX=ORqF7&(SPS#HDEa!T@w%eXb{3Kq3fxj(Q-J{&NN z%g$MiD3tH;B0C>Xkta84xk;L1)^1n}Z{kX*qttkgHSXLAo!q-EX4LAboOivrf|g}l zuH{W*ZG38eR$ZpWUiG$!x@w#uL;F`u-ffc)FRzkw3o@@Kl-D_0m~aK!;`(3WE=0j{ z^Lri5iu$7Q!-iiK`eo#9r_trc0qzaF;9Ay_{Jj*Df~+Z#rs1UqwH5{#dCe&sKsh21 z>s?cxQY&W+YY05*M3%(xKK?XN-7hOAz}R;~n3j|}!brbIU6=Yx;Ymdg#!WSnw+W-$ z6yJo=>(p&xB5^xm#=v&GH($y$ye3lbkx)|4sMn zIozq1ol+~E@hIHs5o)a0Gx7LJ5?2URcgl=xsz*(B)9$duBhslHAbpYre*Jqa>`jLMS$(T? z*jPg~oLZ%47x^i1>SK}INv`&RG`zfgm&K5a&yK$=e&o<>q(9+j?r+Q&>msJ^M}=B- zF1t=-x1zkVlkf~)*_@zWleFr%ITDGX&+b1kAPIecj`zLnbnv@BF7KPwK1$V0E#DgZ zipq)q{r5i&{7(b_)4=~U@IMXw|5pRcO6ONCSvqmz+`PFZr3>a&msic5S5Z;AxI_za zSz0=;ynM<0xeFIpmM*Q*uD?oK<{FiL)`SJ2{|_Ca@m^y5<0bC~PuAq@xmo$S6DLky zvZ$iEs&vMZ>!Xpi+2S+cD}U+2MGLDIE-Rf?TBVsGT@`+!t5Q)HFGCX7vL)r!i%MNp zOI#H~KGUTQcAXmZTj>h_DOVD)u%y&gS-o_@y!oXe9+c~n#Zg?u6RuSYi7P@_1n*Co zn_E@7bY9iMC5s6)tNN;vh07LJ^5!ZpT~=D|(x#R!7WY!u{3WELW=vUid3hz$2k2Uu zi%ZKZU3zt?YqIj~@+jYdD3ENhOFS%Ge6=f88y%2v*1|;<GV4X`D(M5)RdMlywRWJl7)+07a-S|3kkPO+$yzGXV1*@ zx@MnqR=TUYYC(o}s%thmxNv@HUfGX2f60=iCAo`BN^4wmTw0oI%-A$-sWz%o8?{gywLrUg$`tKfGbd-O zLN3)q75ebD#`ox#Z{=N;f6=`^li#BO95Xq2??>kycaeKig61ZO-TX5{*O{m>6C}Tz z{O@4EErjOx$8F?w7lqP|=WkDoh>PHExA7-D{Cs+%#33;m^!j)1c}d>DtO!(eu3dKv z-H#TYWXvM7XnsBT5t913O%%WKbF2UT|LrpM`mQP0u7G@o-u-lD;mja@^mAbGJmhD= zU#O3s7egFF9(!I?zC>8{^QTM2zf0G@cnz}PRKwk`w{}y$o)^#QIeD-A9hmw?hWz1A z+~luL{)E5iCiU9S|0sW7mVLbHHwSjVVB&=ojhoCr$vT}NFUp^*?FBD?9p;}G7Q3Vz zg6^3wCAz`lLnjXw-0;n|%@+#(^PL+7wbD1pK}{RxRuE9T3a{me+%;4HTj8Ket(%y|KcX&{VcbE zm%H!!%W2(lXC0;F(cHJYPg=3}lGYR7Hp3k-u>CT_f8A^Y+Y8-48ur9giGRfGf8771 z?X64`+YG)*%3odmS!TL4ZAQktc~@OkwY0P}BP(mtqyipt48O+zLHDue_&wy(ZX2HJ z=KlVlx!3W>X}h|MBp*QclB1VTymQD(_j?Js?&r^6IsUE}|8j2XwTsEW#WPTYyHLOB z#Wy-SE_nR0GcFwR&Z(I#ubzJXlcV3BqiL?J@dwvtzL)Si>63g!zOm_3yccH`%+1Rx z@QfX|V9{J@mP>0?(}WQH-Hn#Ix}5&2pZht4FDR|7EHzyt!=E!dXJ$c8;oRIA**S${ z$4@U2edI2tx0Jp(OfjaC!s8DMf1VLMJXE;F3W|ym(2KYd$6u{d;9q$;62zrPo!Ly2|I#g)engmCal1S~Ra_Zso!oOH)T{ zSIw(jIDc+s)lz!ziP9_cSUq1%2iJn7OBT5n)7iTgR=R}gvU&7cre4BLrb(rhRg=pX zR#dYTIATi}{(;v^GinV33i>{(+Mq`4yC+_TPI zFnS2>I?(Gm0da?^KMBcHX8EtElOP0D!d4(SNo44qyg{6zDq)g3P;qzyVS8c+1xeCUP&%Knv zTEV=^YiCw3X1I})b7}dqob

    KBmkyG8h&chvMq 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

    7JK;KkJMU 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 z zMwBc+*iKK*25|Ow15ai)(}8`lhSjkl{!Rb{suZEQtmQ#Je?U|V6l}8+? z&B_Q#U=dtX$|`~avOd{z2H2q}R%TLnS2~4%BR7s=P`Z9tX%1@JGK^_e z7T3SOZt<&TBij`?QQWxtNM0EFbuT9I`cW7WdM`NEE`r(zu3YqqWFER4co#A5P$&40 zo&tO*z6u{alm$l@_fz^9DYh@mDhqtWAJ1Z#;s~PW;{~0}JOs!CU$qB*`f$@jsX37^ zVPs5wf5#kp{B_1gZgjpz?UEI<2}`0Lk~&WU1h$o19c?z9Tr@L*`Q^(4J~jt9*8;)e z<^zItvlDK3Ia`dDp*+fuqqqU_Txfs~q7TYmH@iwJl=H)=yy}-pS8j&kEX7;wWPV1D zI9D!%+d5nmJI@E?i)8K`0G7pHXW^>P@=6`oMUik^I43#Ke?`bgc9t{Y&*vw!VVllR z@mn|@wsNr-TWKc}-3(8CJV1wkd0x3+06bCe!$oPujch5}tfANX>z8OpIsdTI**?hW zIHl75#V-3NScOZF!cYMrM^@l*38E`+t0Nl5+^o|3Ga!JTP6}R4#WWDgG``;9U#d0hq6KPVJE`V7IywrEBgRxBK^@Wdbq;pA2Z5P zGb&eNnFz0c%wRW8?wCm6vAn=xW|IheIy`MC$z~(zf}nPp0-iDh@=5&;F>0 zG-|dnUycxw^5u$|9Q4^H2h@6WImqd@Vq z&}k_g^lnNg1YxC5bS!(y`FU_|nKIQyktMgxMKO@!7|2k$^u$Sorw(W}S!UQHAL({D+5K2UE#!@jbOu&^IJ3b(Gr|>LhEIFSXR{OdqKEt`+UWXL!c7IE z=p+v!&SNfDc^i4q5v4lGeaJ+h4Av=0G=MQ;?i5tYma809r9IIVJU#cRt4VX6z@t`N8u#21?QFf!c0ab0O?gGD2I@uNTDiW1?xD5`TjF&lbBb1e$qyh?a zD8pq?@&WaF%h#Y~tn@0JlRC-evsp`V@J+nP1rDwcaJ1fEr&}MZ?vW-u8ld6}pavr$ zEklOyk+iWIhGp`RN)v~Vyon=-cn|?gQUzu5^C0*U_@upsjp1ue?s`%oAKMLYqOy1T zn?|j@H<3CQl^iPb#yeCBwYD^%mnb0gpdiK`DtK@LBdgg}9+=sHH#zbgp7BBKbvzA{ z&jev2wBGb8ZD;;0;>_;)0)dYUM{DWX0DM$AOHVG;Ej379P^x15@##ugj9pn@GYNIA z{7emes}cL;3#A5_ZMZbC{&(Cbsplo~#6xfQK4gQmNaBLPXa@*^Oo{P1Qsp9tq90td*!o8^6Krj-U%l-D%5%uL6&`*wHZC_?EE$}~(EFhboY%3f$ zdwySe4`uJzGe%b;^Km5&n1F7?6z17$`r|d77~&a3>}1^^jLVRUk<$L{cKHmbL>FkG0S9!7`+FfK@yn zG`Bn5-1W_3(4uf#A5jrq$F|CqV+30im3~d%VksI@XjG?b^m27Qm!b=H!t+YajGQdT zErFPHT!r7qcrUKT+UEjX>Bwr3vZSwu!M4rSI7}h)-K+*=f+Q`$YJildtwsoW(%X<8 zg=PGx?bRR}8C0+uu{vE2rH8g04INt!w$f^6Z2ofS)_p!$j$5!floI&JrRrc}(HQI# zo}tG0awBhemp~Enzjl+z)-ktAo*sKt&GJ9iTt%VnB7MP?MrnB z-Hm8^89T^m^4eer{Uq2yb_*K6I#{yHd{VISat96;>NOr$QO?ftRPtcqVVETE-{Zl; z(futrSe~u{Q6&Wr4i>&^3=ft^f`g^co+e{?04xDSDy?NK*<=>dWh{F@&XFN!>GhPu zx0gT|W5O1FRAGyXPE@d>B9@fwWm^bfBLBo^CUJS6zrNz1q?`o|LsnLlB9vq{04Fu5K_b}fx1SpH=meY zW)0xL0YzOA)`LtreyGde)5Z6~y4#WSHI5t!aG(}3PaGn*6Sw<;5$GZdAuQmj^{?6R zgxS)FG%x8x{}Buqm-I#uwOj^xM$kEA!|F`tMH6Wt6I@jHNpMGpKoy^!7=B8&hZk~B zUV!kt5D_8yr7u@Q`4ojk^?K3)Cqdd2O^7I(X(EHBcrO+l%@{7Yg z{7gjxkAZ)`eomeCib!H60Qi-ObA34zLQKdCtaK_S1hGKy^kEHh?nr@7CyG>tQGefj z7a3t}Am@wFm@jt_CJpR9Iky6Qx+45^py#jf^BEAi;NS+g<;MU>A5EN6j3{zkiY;Y7 zo^dQpiJ5rtm&iiB*PBG?J?wat={y%II~kU0xUrp6U-uAi5x$+9ssNwrN@tYmIgF9rZ1D8D+H9Je4SFeFQiIBvjR|9Zg;Cp^u zG#`Sdf$~xf*~@%HAfZ@muskpJeBG?Fuz()^COnLw;(NZ{rxj2yL2}|tlQe_+^pLYL zm@bx4MgCqxiqu1H#uc**>O6}pWv(=VVBrRUgp~F3aeM74AEB5904<(-BN|RQ%nkCZ z6wbaBBciAr0{IVm^JOrsJ>)<}h`j;{xO&P9)m1ZamA=tIl`bo)>%g54glhz{{041O zRKPZq4e&1AQ?3GDJYF;3)YHJgSuJaGl%QT70_68ffVze8gRuN_KD5< z9Ot0>A$TZi?I?f6*{h|9ktw2tj9w=jXz?kFdmH~@oFHnQSs$G1WBnFF-($Ac^8km9o3 zA*BXBG)OSg(2hdkD!+Zk^UEmv+!it9V+~l_B6+*I!tX~~6%dX$14NkzL|K$;i1Ka? zVx19FE(kE?tpHOl4l(5|+y`5sieSPG3K7!<+^U2wQVm<0v^(SWxno# zuWOoh^e3*{v@OB?bUSAJ9d0MvV>@F@VUN|N_Snh|ZZQPk-{2N=ozC9i_Z{4#u-4H% zdL6ixb~kCxA??4**uc{nm$67bn28O{o%{zEn`9SsV{}%m3wH3j;OIZyhdl?hu1afe zf%cNz&(qk+Jd3`bO?h33B&e3_0g(l_V}GrBXARXW1LYxXVIB`(K~iL=0gs71xexkN zYWDnc7`5TioFLgQgo1(x}b0mqLZ21fo2uGK8S% zYr&Iq&S@nw}Z&3R5*njM)u5fPQ#XiBVZ)q#+Dzv;cexva zl%_lCJa6z25Wj;+@Ey*n)fNdSwUI5t16h#(Qr_j@LWj&z!f`$JNbl4h8Twg(5RZ~v z$RGVl)DA$D$=^ftH-q;S;E5C?AMZdx@I;OvPu@YaQ5d`QT8KLNvVc^i0Hz15%9k*H zNqdQWg=y~?9}~GEgAX&`A#Ez(!Sv%;{>nSl4M3{M$y)auqi1=t*81kl^3+RWE8l7J z?Yz@rweil$dJMUjoC2l$0K&+wMYpaGU<4UKaK(AQ28yd}Spb40P+;B2*lan{a}s_} zg06~2O%ATxC^F45Z&9LGDVkXIkIE~=Nys{pxLcs#$dTVwA>HIp_(WYofxHIcA1$O7 zV$YTQN{iNjD(17aL@ud8WEjMER*1Y0^@Cv8XU#@RXJEw`bk=Eq*KJ?dWvnap%Msm= zB^VL^nW|5nhuagv|DC44I!|;thyDhO_&cusmtghjCjK^5cKs%x^-@XT$R?4v0YX_k zqbW~TLNclg52-s~g|mX-hpBK4`PZ$maY5_gi_?)SriCIqfLC`YCG6yMdf9WceZRfoExPG3{QjGl|gj306Q?`3r5!71$D8a zJ0|QGuge0jSYAXUPynfg4+8Knv|n@RBf8CA3bM~Kj&xk-;}Za;3H}J6=TwNSZE-G! z$p-q~R^G~5msUuO-A>-lO1Pn^x))mAq4G`^)vngSg-DDj81jdxJLs-t@xnQrQXdo} zkS}6QUO;Q5envyz0R_NI)4Kfu8Vh|VNO{5f1)LQ0IuL|TMlnx25BTqv} zN4#6tGM=h}W^Jfv3<3BcGts^{8$y2yPoR75%~P5Mu$*LSk&^fR3fj?~3KA+1xbw zF#RHGAoK*#0$JU4_aGg~SQ5Y35X8iuhiWCz6A^2T65Nfd`G@fc;5Nr07O&K%8nh13 z1mwD4zRRg~`S>0(6WAyrQT%AN?Uu`@@Ut` z_wV|Oo>6q!DT<0_mP54LngfhNFVBWLX+O_H%^JKwc~e?BN2nj5-2W*LsPKz27h?u4 zeT0pdT!|eJ);(=DFug09hN_Elq#J0VgbcWqy4O}^A$JdHJmeveQW#Y`3}(Xkz{r{_ z$4}e}LDtSDQ-CRc0OaeXmON~5OUM;6T7;&V?Jc0#g!}n@rG~i}HA1h$uSvV^Cog0* z`-LfH0c}!ez*AR56HI}jB@=( z&VR$ce}%sHI1;gj>UKnzG@DOTJQZqs~75F|Lx&hrYT7xQy%qqqlr;#I6Da)(^ z4!311NouBeo6!Y=IESPzB)eeO+9Y-Su07UrL+_xohWQXU#<=eOe-g8tTv4O4@90Vh zN_s~T@jj6$A5980DcM6T=nUcMhF@}P{uw7NMWScwov#ED%WE!C6+alNv-z1g)NNwn( zo{4p_C+eUubF#Ju6av4&0tIQ^Ho75TC$Pbzf`%dA?NPB#kC8N*-WE^%!%@-l9BKQg zXqRK&90|qux*=9&>4uQYYs%%Ho>_HVUQ>_XjkYnK2LjgNE$$(}N=U#Yc8k(o3ewAJg(+gaOqviWN|HC%AY3V1)#%Phr9I7q{n;hk$k zYr3I#?cK+?revHbmZC(X{Waa(3w%m2m4n5ED$Y=-<$9*}nXV^Lq0J4tk@al7*E01o znfnk@&NQh(=DXB-{wRHqLbXn*U${xZ(zf2E!CgG8)3J@L<7^?{0o|Nz; zQ6`k|q*rO@cu22e5}sr$p@b)`N|K;_>l5TVr8Yp4ACNL6MjgXC(1H!&><>s8iYr4Z zy2(f24@jA2xX>}AY>jno$gz|3>Hiv)!x{ermD@Bb_X9$K2mBwyr2l`w%l&{*|3!pq zag#S&ftEMF%o`ds^zGmLnm2rt$3-7^v&@@(rpP{R^)n4oz)f}hg0K+*0dD|Xq+c30 zhb;%sZZ<7xLVzUCo@D6LUWO6^yi3XtL+OD{J53wh#zk^N{Jie_HZG^_N`$~`WuA23 zsYEE@+*DWeoO74)Z!b9z*%f4n{~Lw`J22tDmt(qFMu9lu#lT+bJsSE}G6+Nue{_NR z%|@g_Wgo-jDWAf&9y>!2GHiGJqE~H1ZqsNtF4_pII|P0hL_-pL16vr>#Ak5nfz$n0 z2(f@F@{byra_O6EwLw9RJBMW}p zjfp%qtHO_?jc-me2g!Zmg}8ACNjgWi3PiQrZ|NI3jV_DoC3yxPFXSKflDvYyv`We& z@>Z77OH$M~vNwM*YE?viD4CDbTf!>W^4BWt6M=fjY9o9Ify&B`g8g=YtEbzgD@uiE-Pw(i&u+jynq!y%O z%_kk!zc7~2k|{w6HfODU8&Ma>CE>R_dsYKuoE%nQ%GDKNh21^)kLDwTG=RRym4LaVUvAXK`R6umS@ZpADct+;fBBw74Bize=Tm zX01MqN+K4vCn_g^h_JaaW+Q)@R?Dr|9un*U^3jM{bVlVA1|TVwPTF%cL7O&xO`}y< zhfS+;+l+BL*IM=AZs$rjfNv0Ul>d}xJXN%Ka+PeG_1^K7l4WZ=oA}RzdAt)t*1F@I zw;yDY-9J0k4c;5$vIqeWO@r-THZ1V&X4J-AU5^W0&!52=ZdwJL91uab7>3lK2OK)0 z80%OQv}x0^57y_FaJb3fU^Y1I!a>zMX>iykD$~t`*PH9MsU!f@78anGi~;s%SMpa4 z0_)_lY4j28!DoAOlNSBLi4wC4ZLM25GV zNXEJaYl;w*98IvMoNFPreK4=<+zyA2aeYR+9gfA{$wyC~96(|QoHqu1EdEPp29PU~C#I0w z;tAx*SeiS=<#UA13BP_nH}(3DQ-+{?XSi>s${wv*nXLOJ+_U-pf+`+&X7_m^92Udo zPo_f@Kd0?8dlY%J22$^^ZWl9-U!BG97IBHQdQaJvfuZd5<0DVQ@HyEE4nK!;_C7>- zV#uHy5TgkJx_V7Z4(+R4@I)jUPaqkPf@3a%U79w87swE*jJ8?O+s0B z1PrelGiKD7F|ITJAPo0{-{}nnk?UPI5c~ zow5SHJl3l2?@DW2oLdSm(!K7tSbdF%nu>yQeAXEW@c3J**huGct8l7gZIf=bM-AgAZy=ae6lOYqnnP5xHr`VX!UG*#k+2i)0nzd*=mq#YpHCy~!0< z(&o9#^yIKX>;}_J*svD{7JIjxJXEEad^5hV$*+U6u#J->)h54AC%+iw#gc%NIw$*e zF!@93}O&$>q!^v-Nlb^dOlOM5>$wPf$lSf2<%)QHWSA0@($O#WAC3L`F z)F6+V&A$?Qs}^gI=>r4s7uT^A27D6$VJ=)CLLhvT1t*{0282)G=t3a;9$wi9NG6{^ z?H)dh(^EnAGa$fIDg?qX0|Fv`reQ!G5uol{BY?pmZT&qt9R}{}Z3U6h)RXD9IWE!# zvxOxFbHkiJr=qrYZgQ$?VSvu!`8xdqc`plj6o`5wojcfm&BLP~)g2$LpVz~58jZ`%^`$z8{9kw# z8W52qx#iR^M4p3NIdS;~SlxAa&vh8>V5Xancb+`rHGw)FfuQ@Ck|+6G+6)*S!tzWQ zJ&=Ua#k@sr7&{eGjS8uyX^=w1mm#jdVP<0hsigs=8Usl2ImFC!0!S%p8l*l6AobTa zAl0bIss2z107)mZfI4jqQnZk7e}`U+;Eyc*vHLQF4S|~zc%5vwCkrHw z_=JbM5vK%?GhjY8!+y_pBhF%#D9+&MR6sK>%W5jP*+=M?cjF#zh2=LT3-5x*b1!15 z7p>4GD1*Kd^TTkHBA z;|9d~IHaR4MIO)o=Leu4QwKv$=Jey_pPy|w9Lwi|>BPwmfYrE6!4sFSX7fBXCc%?= z!2)~*t}`2}cP^YW;IRo#V#^6`;K7J>KgJD4;=VyhBIDrm2=?-H@)^qPRe*}*MInb| z55QCYbF6cVEPxCUp9PF#lMkm;WnB@d02Xbewu;Pi&6M zrQ%PKaF0p*?OI-fq5ex6;LP;?S97KBBfG{b9?vpmmSGclQnoQaud zD~w}uHUdwX3v5I_h3jitIVO3T*91RWH|R3w4abV{&Y+SFxdf4{Lg@y+DU`PmiIN-a zUkDAt=ZgfMpCiE(am8@#@1_5~;C_UE5ouf(a-KbnvZt|b(DB#}cf#AGkIaQnOB^YN z8t5vlmE}nk?Sg>+edH&&=5zCTSkH1ms+wVp<1$|{&R%>1KPZ-+J-ZA^er&IL@`>Nz z;-4Dhaf-TzqRl;`r?~iuZjb3!K1x^It57iq9B-lsN$wBu0&Zm)qIhRB2#;mfXWAOZ;^o(?O$tC1lJxMoE+!2vOt3%=gIqsf(j?>pa|_E-f6qd za(p=&ttX0pa79rdp$DE7$%|D9BFDGUNvbFt0@@qF4eQM=M)^;^d^3v@d4&aN1011) zFrtJa=+PNMb0x#i)w8f>7$m~r!wu75QJyjA&jVN_5U#4&2K_Y&nk3pEG%nB~Tu1_L zXOI#WJP8tH1!ci=J)XPEH?Tz;1b;ydG&*P)CQ&FK&qBs5JzmW(T&XK5hg7)%(R%}U zRnSp$P#NfNs|y|xn-@HWN;T3*&;1KGH0o{WT zSRs{q8FFe~-|~@*oEjc3ctrXbRsI}1JV&ONBWf=`hrb+8@VH!6%>`2WDla267^DjN zu*i8_6~oc%s$vxXPsnRUSucaZcZWbYcZc;?j*&o_@nny0|(mYCxpmqVwQ~JwNzIC;vOn zhFn!QR~(hzx*V|=k}z8Cj9t4cZGo(;SnjVT)iPK<=t$#j2p1v7j1upVn&=|@IJLr` zE8z3w5r~3H=&sEr8kuY>e0?uwmu-QH_ zP+#UbU8ShAdHvk=>4&+u3!JekzatF zb00#ZgaYOTomb+^uJYWJRN0N4fm5<_3gU6Z2xhqf1%=2f(s)Y1PSS1Nho-VS5ag(G zyz4G~hFE%$dzpZCIERpM{pX@awoI%?4PW}QezqKiQv}>M9Qq3?B1LXjRs|E(7%9e_ zZ=c;^XAeYW(O9T)Ps+$Z8JbQZk&Rg_Y#R;|Z{95w!sDlq-VnsqkhB7q^;#X)%)mWQkGvO)Q<8D|1CS z*E_#)iUagJbNUso%SX=LoSfQ&IH(OSOLOV8oh`2;`G<|#lS`3d$AHsV1#Tf(5#w3~ zX}djNpUYWykrh*eK&D8*%C4T*37s_u2;p;xGB`COF5L&=)-_T9|(-e7yawf4ZHdN7$OFBU#8Np%$lq!mJj@izwN=|II?-jEO+`$ZFm~cO>!r`LG^c*%g zi*H2oD3lID_9*;C=z}VEEJ?BoyBfo)K&Zut@!vxpo&^&r{4N6~K7x(DF-+8zzA#wn zx;9pN|I|wF6s)vq>9~)$RlM(jpu7{T@UvOM0A52pKRbLKKO5EqDYjg!YU*Ldn67xV|6=43Ent z;2}&tQt5B!VXEmtV3=+UY|lU7e8{?2=*btC$B-Hp+Ie}Hx|ZTv^kH=8otl`vxWKDx zQO+cC-61xqZ&ALf^_u_@gxW!%>0YvC#yb4IgpyADZqnZk_8Y@h9u47w8{99rfFHS^ z**~5j0c&AEoIqA(o%3f=)7a)_E-9G*mJYR(8A_Tv=F?fCs&xGlxGjShL znA&$QAo&zwb#o2a4;+aO_(y4b4eW(9| zfcN0xN@)8M@*YjoQoP4K7)-!wP%+$H&iB@7!G}S75!@oddARmf+eoYXt(7s?6mH%57^iB+MD=@E!i_V5*o3ikQh{US%+UoQ^*EG1c-R#coPh0 z@YovV@wxV19l6{vh$%MYfL8dI;hdZeG4WNoj-75 zqXqGa%TjE}-f|sM4H;Rt_~#IaG5|UQohJjJL)RDvK<7A+urwS7=aUuyJvALl?$&^> z3*qx&c5{k>goa|nFbw%ee7?Cb#2I}FQgXL{MkhC0p)3NwNRXeVO6x+HwIp0=YJ*)n zOAqJNtlQsUDPWhO#lEusWZn&0Ed;RLF6*BL8*JIErsOIlHyG)_d*D2}-lr_sKQuOG z$bumV-2txfCf8#WmCl*$f3K+gAp(h3SM+?ySqx@2Clk>r_ZXlNv8tqhcGxv(42< z7)UDtdji}cx#sgh(0E8QT|qW$$Z6OUzI+Lit&C7lxgQ^s<}IG7BD0>DRzll?iYh9P zWz``WgjV2tOnX z;LGa~QrHr?RI$P=2wn$VIoL~a6KnY~!X5(37h~(Rnao6CV_#d-+}7AlAK^p45st@Z;K>l!RN4qr zC8-%eR+{{x{TXC~`~D2_*5dPZmz)dR%{0oSRJfK@xDUKA5G6EC4ilW-QP~Bg=i9In z?TJxR$89J^;VE|N9GL#L;2c5*W}->guE&08{}AKB)d7(eR_DLRO+eCz8z@NwS!Y50nbL-$E*|t_^WxlH)*tSuWI7IONjS3A2jh zyuei{jk#Ys6UkY~EK%ZkDX2F&uMd#2 zfv)p(YF}nf4IOL&I#lkuA`uPwjI$`|BYX96#;(RVqVj1(Qv%}lp~oFfz`)rmTUwueEYE)J;IS$$3(GgT@Gep)0`xO#T}EJYa3NQ7(X# zhf?pqft}rS>0^q{P|{9H^A$|mT2h+VA{kPI`gB6`NI+;F?GT{}FFhMmZG00Doh5A% z9dd)65bFv`ssP*7EaQ7%2cHMh?4!hDF-!n@#tK1mbL2&uDP{A>Nu|HC8zItPp|qR4 z%Bb$(a!C5S%hD{+f{d*h$`Av&VLP>`3F?KEcnI}^Y%UWaxr$vFK^hq6(| z2?JXS<&CnBXxhW6QxQyWcii*lWO|C=* zNcF_D0x!F@p4|1m1L341ZXeA=Gq4AP`KtU1Cdi8XK)Ke<56scE| zy3>t{4@1#c$9ewERdC5m00d8&&tc@W>8DKkvVRDjG?)DjD7!)cm^w2Ka5tr|`bxMi zeuu06=rmjP_0!u}b;=1++O4WK> zlV?$E;V~H&TyUoksnbf+I8E zmAl!@`C6Y6Fnkg=QwRKb5cXE_dbd?VH8{T9?ur831bVLsjo^9=LXPbMAZ^kMGNl`nl6WCd2)|_S*cJK5=>h1@* z^->_KIWXTpom|U<*=25UT_A0|e|o~@&UJZnUAGA?cY7DVg?3oo7TUGr1lR2Vt9YbS zv94F+V!k;Ln_?} zXe-Lx2wcO_*e|gXh7)VRJ}0R<^JHyMDHKxm&BFB5Mmd8{ zKmqcF;8<^817hY+sQ2rR7)imJ+=%=aYHMC1r$c5pFmN=VbmzLiZg&_cO*Iw@>eM%iqs zTs}BM4YQ%`#=5Z`?eQ5` za=0$DzkBdrL2&;}N-K8Z(B#30L7=WZ2&}`0RjSR zt3QJPhAqyWt~Z`O@{O&oR+x)iiR8HE#&FN&I({?FCY@4>ZHJ10Yb~fb<0AMETidWM zmt0(8w`Dt(uG_(`1hFhe*fUu{&cl~arm3eIv-bINF52jQnN``-T7$C!8!)EplW81! zSeHD6uixD|BL+unqzON0#y_Dx3SCE_6^Uqr!!%08EtNoSCN2L+2C+!ky-N zg;Bn&nvVEcB)56;ViZ1?xpE)Ch^L1@zW6{x9>V^MTsVdT_5czLtxS5s3}wY~d$mC{ z3~5jpJroGy+&mRroQgd4K%ur}H1s7~4#S2LDQSXupueHoYE`R-TtP+0c!HkgZUihO z0=6JR3CN$Y_BhXI)`jfJgvA2fBUtjj>RdCQDZqHd2gE6kkH}(JJ+taJvqNDB#V9PH zG{o$`fKvSGD@aapk9PhR@Y|J_;bz!bu9<^GYOo=v8+$2&P{B0~PN;(g{Iz=NF7o9Z6~X%msG;anW&d>MI`~p$ z*%!SDR+&V^M-USI=S2G_`D7i9&8TUugf+vbWHA8|)YG89X|PvL}Nzl`iJta3>N; zmg3F?N&i%H>?xCFQIqS#)`=g%S5+5fEFX^LpQHI_g3<077-=D%efesJo}82U!001N z+9d(Xodn2NGn)8z4F6Q}&%w#ZF$k}Y>o}MN<^mhcS$C7uqR2FWDxMrQ3GC%y+5p8$ zUBOk6-n=}`05iao;)H}gcOz}f!ILXqz;>gm3W>m*kxc+3fnO0iQXG_D(<7yb9%#QN z2LYu?gbN445QG?cgid1Wk>aMg;$i3t4nVlS(xs^if|*U}hJGF!?WUU?3q_IiLfgY! zyc(Yz$+f`9IIb$?-gtxMj6khlhSLOh^tm{QJ7BHQN3RID(_X0(ryjBdUiAThC(w|1 ztGYvj$3u9cD7@r7Ft-M;-{5W%MYfx0%S~+l#u(5_+EQ|;a$qy4!0z<5Lt2Z#pX4)F z2fm+z(Z^>&zoUXt6{^dfV$+1zZ1hupAC0k@40r#9xF(suNxIEvotC{11{zd}P=OsN zH$hjqyjl-TEBq(CV6x>3ifXkIJmf$f>4UOPyZaBgprouokuD%*@!6IYD9h-r@}IN< zHGuHqqBRF6eTvNF;5WHRin`h%N()>H9x9f<8A^dzknKru?? z)|tjl1@b_UoD=nnHE@RK7q}~I>L<mY$))mpNQ^@4 z{>0=A+)kzPK0HQg5m67B#CL(wMu4viF#4kEHhi#X*;aw8opwRu?SCU5V)Qd)&jnox0$k!BQ`6 zwbXs&;R#?9`|V2RJhk=}}EW$F$7D_${XgLpP>_Ea8UI>+Az=^QgI zzM+}T&_is2dgAf$RX)xU64eIF}ToO#9Yu7N5c^(B!ZG<72uc8i5 z-bOqkUY%;8S>0qc20ML=@m6qc%kYmuOT`d7z(dCTU+&YB(37jPLsjvMxc&34gR9dn zJi^V}g*xD`5K@Gfo^x{KXTB60=-IvNx7Im1EXxgp|Ysd{hb z!)XO(0I6mWYLQ-@#d2m9&unXFy@ruwS&9=|59dq^$*5kSNc>r$F@_K(vHP!KWIf*% zz&z_F*Bh~ne+x+pfK9@*4m&Q?Xr2!{a_E>iJs=u_8!#pMFj<`iw!bG!!85`Z13uC4 z(gGGGryYl#IaEokQt?shCm7vRm<*|}w%A!kW%?7#ikPlP@T9TlA<>om5g1V8rCofa zQ$oW6!h4HHzaJZBZSly?uXORbF6Ul;2g-m?qB5LJb6oa?H}SnF$&o!9H?%M*;OW9( zZgZ<(?a%`FL7Yn}ehV2cM4|kgdY~1^0~*RYU*3TiYT*c%zrCW|nGkJZK(SZ&v_D6( zZ#a(AT*3I{>RWuxQ>*9@6kBlQ=+EgnTg^@UUYUeLP=2 z&4S#mu+on(n6yPfekF{8+>`}AEN>*O(Y_6gbeE*U0sl8(r1B@lqi=GFTSP)!+sA>l z%}Cw(PVG#*RRm1~9QO=y5vN9o;Oo&yicBHz-K>O#TSh6uMvq?^v7p^Uy)jezkHBZW z)i{=TL%4}xW{3TFKRVwdZ_LBwpxANUPoUdq8F{RG6yZv}`p1|!>W z!+COWf7vF80MpN4A{zdBQ8==H`LG0naB*u27cYWtR4+*gZTOMR+SDP!IYxI z$~LVLcVKLQQe*!Lf^4!o@JNeRcAAy$Wlt0BX#_7pNVT8~rE5p7z=s=qxOffVIIQfs zEEPtL(E!Ub3iX8OC5V5-e@2-d#6o*Il|}O{PSAXN^M*YoT09L!O;2!h(_S(LglSF< zI)4^7T*IdCb3oq@gt|#?^RXlMxcT|#pC37F)W}m@-i!12pHnS%t?e6sH*m`k0;^`E z!pe}|W4;xXnfNhwirbptj$P*9oN8DWTjPe#x1Z1PSND0Qn>xs<2DNU+X1jI{Be0i9 z7g%Ar?dTWV;0D#ZAFT^;`w`$}zXkTS%s_OZ1FZ>xWrz|01f102fk(wC{10pb<$)q} zJepH%4>Y6T-Z|C^P}+yHFxpO1SSPT}gZm_@fhXgkk5PJt!1xxBsshre?fGtu70l)D zm~ylJK&9KUMOt*JRg0}^s~)C!jK7-)4dn9(wHCNVLJbn@=ei>y!p*MFP)z^&nli?q z_)!Dyf2%R`GUe@6NytR#p`K7Dey~j?nD@bo)A*#v0-ViV%VRgi-iY06|8qTY7tqF(3!HR4#8xdrXTXJqHO`KhW2J&Zo~%X;Q7ylnQ^6?a=m>6@MUf|o zJHjA9SFQlMdnUw_iF63H^?>gYE%zcgg0Z_*v=j=#hTdQn$zt?IKAVf~%NObfqDmz> zBNAA2f0M*r|;kb*8l1F`#@l zEwJn6Yf5KLmpa!sFwBRXP?gQDrT&>Mz8`_!AmkJm%(P{CKN619|EYDni6dvrSh?Y; zIV^@x4bW?4mh$u=%C@;6E!0Myd>w^aS7G}xSeJhD6sCz9h=vp>9T=MlOYZf6R9vDR z85lwRf^@~p{I01x%#zmv=JXbip$;iDSmq-XHkevtr|&~BONT&(W>kO99PR?lS5bp8 zK_YT}6_81vSAqREwGVSQS)HX|(XGs6&M-pgADsg9FOZ2t@CLCzAj%J7f=o@uBMZWh z@mqu|v!{s-RzHXif_n;FwnEjK@1;+G zX3#A3d4w1ZrN>twmj~X(#;1oZFpWlgFh8&d;Poz!iUv$wDPmd^^Na$O3}h7&{hWh|Sbu4R}}uiwAjT zs*p)e04uLKt&Bl0ddLetNC2^mF>cBtouMzNij-I^Gx*n2(E?3}mVwsf9VeCESBYud2bJSzUoQpHAoS5Gs$slDk)rWeO(J-hxOGd*W!yQ<@^5+pL2opYO z(J+z#(%sZj3syMZ^*A~7CfXfy$srVdv5hcLq4h|x z5vp5n1Pr7P*1&Cz3($mTb=B)|4GSqKV;BdnB*}#*j9R*qa0|SRd_FcJPZlB%&wp|e zp_9@OY4r?))}at-kszx7zYqkSi+lX$XoLdYA8Fdx!D3#ki&-cy08(iISVpTF=(}A^ z6G=ZCEaq?8Sj&!JFx5b91Ud@?ngJs+)kEz&m~_U``UHHPOF$^@sX@~*-1)m>Q}>iZ z5sys<60kcW!xZUhS81y8M{*p!Jam$mW;O-3g|9)E-paJl-`ztWx8PpabN5zMIa{`{QQa2kvo1t7KW3m@ zGlMie2VtBW9TfNJmVSmG@W#Ys9!EM2OFkDtVk`eXWVLhBgSv8PE|pKm?v#f;e-!f8 zVGl-RGC7D`S*Ni^$)R(>$-=S|K3fQ}G>$#Fo*D;@eBTJVK5A?>>DI4WctUqP|NrF`CW4d#ZX`WXo|-@_^9a@li3|D-y(A> zil$^_(nViIduu-o_`0FE`Nk24Pd>?l1Ca-Mf8HGUPx?T(j}VTSsnAeR{ybB5dFQ)s zx1#{j+EpX81gA?8BIx7;sO}9u4Ck+#@QXqeA3o{)=U{t64njecSkLW*LXpE^DTyE6 zernAx z_m3RG5lNRut$IB6Pd?&`AL8<1eaaqq^u}ceMslH%uO|ngp}mp4pb>15yAnGqx6as= zIea~pe*z0R^+OrW0yX|qA?tj7uM=N}KxsvHaAqmc-y6&@1pUzg=+S$Beu?WKSpp~j z#Q7Vfl|;^mAXlDt2s=R5{JDLwzdK$BV1)-+;TS92-*IrTt)(3_+VCcz=|ONZL0pr} z)p&5DtD(XOWEEk9KR5UvV2kn#Ta>Y>MZxfoLKe6VP93!iDr6#%;&&Z`)#iNwXyze7 z07a*RzZ>DWx+bHyupG!e(Ji6a@B9#q3aHSeAlRro;2r*%INHiT zw(<%quW%Fj=^<#G!ZuGnhnGFCym0>i4gdK%IiNFZ%=ZKxXm<#7&Mz!rF?`K9Or)LB zS(U4i8hX_ytAeK=p$bk+9bcUdpw3MkgU|Ybi0hx7jR+RFggU}(&h7k|WF>@s>9VRj^`~h-G9i5aQf{;h`83MCjzImeV74~Ed74O_3pVXvrxH*V(x+}0czn!(^Pkb3t^f+dcDRJeYK zI!}-`sQefsu?tfk>@~oIeL1Uq?5GK-tb$;v1zok`hOsELsFyF9g{7r9&}DQs{MxzY))J1N3QG$C>sBChr2ud*QN6zY}A%czF{pMkYJZRYWT z$yCsk`7~9yA@~j}rR5{^yO;RgBu$@zzeu0Kj+|VH^C~WgNN%7-K37kA#+ngXZ<5=a zGl@On#<8*eG_RjO9={{e3jD<`?-m`4fZ>yHD?JXXzP$nzu)M|m2-7vBeGo@R(wC_?Fi&K z8uTuJT(ZMUW;JPtz$;ktATKJiYH*a#VB%^AKax|nt#)D^N4B5-!4gjtgYpRm<;wu& zLngF$C)WUfEO=8|633Nz57*k#Wa3l=l4cR| zry%)FCD7F(I6>8v4B>>pMr>8R8`$uwEPrb#f9T?%<$f7cI{MobBonCuLHDP1Y-&-t zExbj_Vys5A9OVgA=`lum0th;=2CO58!C@s{8SqRj!EPOM7*r%$u^0Jbt9T46*1DXz zE>240A@XFM8#oU+Tk1fNfkVoB3PM4DiC|IueF6vvFkSa8`XY!>`>>W2SXw5bAu(y*d z`g{1R^Wh4(8@YNJ_tGPbdEdajR2f6NGKcp7P=0pG3n^BEcYusGH@+77`EkZ`?vruX zO>cUGtUTZi^>CyaN#vY&F#h@2bw0%Pp08U7tTE}af2Ojl@WrF};A;ev^{iaxlDi!! zJu2K5avUVGQq0i**sqZAQReUm@aXaJhIaDXz?FM{eFpnM1{$Ewvww1S)`j-YbOS%K zopy*)>UR+p%_w!L9L6)k*IbXu(v4?{FIE5+cAd-eV<61^wf-+i$AZ8I7Ng&*5|E zv}raYUm~J#IH1$u@hM;{9;>EAlP97etRv#xvZjsZ%fJUX2<(7rG#J8xdj^DdJTUHt z{3z!zhaMt`!^T6z4wuDq%2<)wDkl#iMss*J-jB@GmC5ezgB8BmPDIec;MfhvcaM%c z4t*XG8pItDV+hrSY0p%}EUgCL7@WeprFtq)JB5L3Q_1~!RA3V^ZItV(jg>kNvti5c z!J z0Es}kcT<4!6rxb>%JqsZ#Dc-)SILY4;0wg*REZ(Pm8b$^F~z&FRyZakB#NbD3~2kB zZ}1T=n{7_8(|w#3j(0!+g_EqoXX^mMeFY=}0UgBkx7K?bq&U3>wNhKxPbc7RWh z*a+7L+~085X9w52()BsU^&Vjsa}Z9}QtMofK@fa>;UG*3JP~(5c2>(jk4q5+Svk-k zA|H#f^LRo&;gGk7S*Hv~L_PjX_zG;^(FvUUyOrOI_sS91xa}{(WdUwM z+y5lFU#>tllNd%h+SwgvSgKI{DPOzj5Qv()|TS;X+GPKc}F1JK23&Q-z z2SUC;2-Ud*N87IjY9=b9(Q%%PMt3|@wwGbN|K!jcq3zRXH-RXIdk^GrH`vdgU_aAJ zf}bQ}^nIX>s&^Q44qKX%hctj{30{-xZ~!YLD3x~dehox>DjvNI&nA=~fnR}8Ud8Y} z#&sOYg%)05yum1Y8cQG^g_b9>Z)9zmWULiLQY@%!@uU!g5QXm!uyXI2|8ASwfF8%`p8 z`UnEG1sg_=qzK0B{2&5LmXfDZ7F7B{pu-+1b$4dVvNZfT0gt z{0i_INCJ8p->b(3!W$~|d5?|g^^~a!Os9t|y$I?a1bUep?4L@w{A=q6)}h7p*zU#A zd9LsfS8}>5-VX!HK@Mf6vo4lBabwF&Jt_${!7~@E`v!v^YhS^QIfH4kpvNVLp|c?Q zXugb~lVu#KaR}V*@+3@o)Aqz|bFgi6sSd+y+vJaBC3pEg91{4GF>Xix;t`9?k}z2g zI7$|Rt4yaJDx}~{r;`70xLIodQz?h(E-xgzH*cG3bd~DoATS5pN>Q4X$wBDnw_qKi zO3?nE(L(`pGvu6sM1@Wk4!Ic#UVU#%T#GwFNsD{N*;;i&>My-!oR@JISYv07UNao& znNkSP(&T{(G#l7{VLROvcA#GAG@<2_E`U-2`4#6y3Qz}aL9i?KSZ6ckUDvpr%ja+v zoeU&tvZ4uT+}S|x1e^R}!L{!+moq9;)WP!-O&}8nPpLw-3TYtJs>k3TZc2plYFXL57XATT_b0fF(p|DZ?cb%cWh+VC~6S zr_mUAYAFP8*J5DxF4JA{Nv>oX;Ilhw%Ed-hq%?)Izn`O>bxa0_c06f-vI=u$dk_Qh zuG*gNtkK?qS>tzSc(F;4o_2}t;9%xKYWI637kVkgvC4Z+4#)+M4&~4Uwbzsl>A}v7 znH!#Dy+r7Qv)NQkiH$0aPhs2)@=*nEl+Y& z;eQ6;=ovx`bn?Y)#(01|AK2L7s&Om<-3B^>#c?^Kjml489W&76-6L$g5tnIR$!g&x zItXsUwr@Jfok4I;4;$TT9sVTAoxPRhzPLHkPPl+OyM80@UCs@dMjVcE;5a;)k3B~I z={@hQg?X{{2syBLB9gdjyztea(h&fZ9#VM@H!~? zPXa*;*}GA07<8^s`@=NgTX1=QhCO6_|A6JIrrO+7#n-_YeGv@h^{$b!D{GI3!+ZwF z$CQS!g$vw(zlR*5o_psRV|X4KHU6uPl8K8I3#P!+=6^Q7r^H8=ASxC)t-I2An7`=0 zhH1Q#us(9DJ{v_|p(}3}_txdc1ED6HgTnk zt-IN|Wv*~B%JVd!XCHOzVAjNtU)HRNQ#EViRE|;O=0Sr`?wx*l-Q*_RsEY(HzAj$n zx=szS0x1;jVy!cao@fu5ta%=i?!iFnyXZ5SvN6Gd`-=SNR3E;vQld^51SX% zxy5looxND0`U~3lIk^p~mq`7dg0OsNIR!$dmKGUg4f3G8fYBPh@Mv>25t*o9vn8iA zvpUR`U@4>KTHI4U>Q>kdtwA{xh?Yuj2Ij(w%Vu?agTV*3Mx&C`{)^(bf#ZoZaQr0= z94NO5a_qk9Irr1yG(uIvtN1Wl_t5TS^akxt4ro0~f+|l|ql38ow%!-|-32=pPPe0e z2E%RqCy!@&=o(kVb^IsRtPdyaLr^9s(|Z-{E{6zj-sZ3wIdoFmr;_|AS+{sys;Wlp zL^rVEvM&7XgO2Ia;N#SNF^j@p%TB|WQFMS=h7A{Eq=7%`cJP!B2AZ*jD zUI<*8)hA8rv(KOnz5Z!oO@B-_UeJ)<%;(2%Y+Q?-*w*Bb(-qBA-iQQ>_VW4 zab3p(_i`J@E^&opU8m7*$EmImqVIV!t-ro|aO`4Ni0Baexvtf&5SCG=S8DmjD7ne>F&I_FH5rVE`E6c8hWE?_tLbe}d}FPX6lN@F4Eut6ejHg^|rS3rUa#Gj5~YFXL&tyni7ocfsJl z6;gX!zzikW#mG?7ha1SNu41QVOg+KKY87^S3=im7F=hg#wfTks`KP4K`HayXkk4w$ z<={(|>qGLcyPCFFfLU-PYFC;w*qT>(>q7)5%qMOrXbE3ls`&Du2Vgcne;9Lm)_pxM z7_))9l5l7Z*ZC-yQXzXD%bm*$5k5O0+@-^(!RhgZLr`~`Yp9BFP3kClI*vV`gU>n?UGmX6}WcH+hXC`zeX*T@@`Yb21E3#>=NtbJSu^)@>wwcmCI*X>ahfp_r{J49}oc-9BG zQU_UhH6d&_Dbd%(krw)cxqbS}>Q#N3M`-iEnUlZBtl(jE0T@>+AV2IuJed0j4pyE2 zadx1>PL@1Sd99msxvO7hbhgvH$h_nXr#aKwG!b155z8A$@y7XQH&2-Kt<4iM_!-S} zpXp*kZ+OZDf7|HfK*vdWB1Y3p@ahgPc4~zLQmP1G^ZQEt8v(JoWUtq@9!GO=vnM5oa7kU&`D$-gIdW0vP zPJXloq>q{1@K&2+ROnGO4BFLW*;4QOqWxb9H8w{3J&nzJ4q4xj*$Oh-Iuj3*pI8?oxx2vOL|w~b?N(pGjGhsnc3je zjpp*FwUZ65lVj{0W*`0m>dL^v0kr;Xtxiay{@di7F-&MZipueRi0pT;uZH&(l#^Vo z3(ZeHARac^1ereipufcY{+hf7Zr* zTr%9^swK+O%#q?cHR1}z!FiSwzJ&Y1Z$t$X0mT-#hRs2d2Ra8L8O9#r`#;Iw#Var7 zobeNhrN|YLA8sR~b(W1kE_Zcb<8S(JoTE?N`|b+7mh}=*0h1cxn^BI#arkew&8X-6 zmBE6oGOOaRpnsJFKl%W@ZpzL63Bfsc-){i-^|Mz77ac(_7h7Ks6CDJnf11R!P1Dpr zC`1D8Nw*|&BqewKvG~ofmw$)z*Y*BgulI+%;p4tGG8k_J&a3)kIvVN9z_gLh_BDHN zgr6ckn-e+r@?r;FA$C3B3i-o5))n$O>k65&D|DV)DgC!|sjiT{x*8MNhdV`sLycW? zDEz{E#+(!X)_83d7oz{mu2l!ZKf%z?bRfVdpbmsT!M|>A{sOoa-q{XE0r%)%bA}Fu zqaC=EU$RkOw&#Bg_X@}HZs7*JY<`iyzKoaKzkvtsx1#Cdm-*S>z|l~AihsV#KXd7V zXv5R?9rQ@t$;<6%Aovi!*v~&-Ln|Gh?{t=ad7M@|9RybD78xSGE;LN|aakY4T z`t|(wQ*wnPVuNqt@(ObfIowJtV!_}f2OC^iZ4WhKQ$hh9_qj)}?GPn`3286txLfrx zQS6vIC!%-+HTV_X3LURw!$@W^J3GlxVhgwZ{z8|xh zZ=e$rs@E}11XrAKU~XWcob$~!*o}tk1@Iip^<^uV0j4}?=rnFEHn(r%#tvnmYt<~< z#uoL?8YDfxQr>$_=dwP&DN4CPf}=sRyO&47 z@0aQ?R(YS^_i%9Go1l5Sf20ZF3%Xv{lf^9I0y#vyoVl@zdhnHPlLT0|-&B_DR-)59 za|@MOZR8@}xyalFQA>pz~9!|#my=e5(ZO#V=jxmsv#UgNQ1vut0 zqM%DJaF}aeiRx(c?u4PklA!hg(pTz;*#JxuzAeqYzJhJ<_QC@*>sCH=0dh8MB&Kz9LJtX7%jR#Vxe%@3N zH8jWJ^G@OgjB^eHWTF7rZxO`2XoEfh$jp8_4uc>fHqu?t5Lvia4`mb)*@m;QhLliQ z%-?V0QsC@KI^NwNiXBg*^LFG~hJ!HsNxd#$2AYh=wR=c$IO>Wy%OGO)h4RqcKUWKSLOH^8?Z zq*_QbxnTad4W42C>eCy&v8z(-D)~$sM$r;&YQBy0vy6%C&O0FO@(;MF{KG%T^!3~H z)}6eCUDy1_-r8Z6(Hi!v^EP*mX;)KZY(wT}p=w_WM*Cy{_h+AIdx#IZ=*35o%2!}w z_4(#K4?Nb(sTR!Fke@i0wGd=OOTir7rk68#O5Ry8IPkk*a3K6b(}P6qbId6N)!KXm zRhVB8+=f#U>TkfSINS%hVdt1P%fo*{{VhKHqx-3#EHS^1EhjI4j{8@5%4PZ+R8>9) z-+a0q%ten&V~Nr~Md@UF%o848AiY0@-60EM)MCGmTz?$Icaedjybv*RWJ#Ehw>>G9 z^zuH&%RC8=FU#438}Q8s+n;Xm4G(3&Q3q%L&8!B+m*GNbG=Faj-&b!pShtJK@7@dg z<&kP z0>_(cK6vjwgA>unW)^6q)yq7ysk^Qbb{~c?%8!$i4rt z|Gulx3UxN`{%qTSw%&ij8z*fP&iE}TR2wYGf0EaC{ugKSB-V?AAU1&g*SHUoc>e|EAfyd=fm19Fqny6FEKTAy9ux$XyQsUr%>RUP zL%M#dNIUZi^J^zM>E?)Z%yWW^JFsc}alG>p95LaGH~WE_Os@QxeIKr3ypK8oku+@e zb?u6p&Bvk6sVcz)tfKiXoKCs;eF@Zg5&Sb+9f4`}Z;!l^>HG%CcC%PB-3 z4p7+N!7%<+0*1eBVfdvK3<(-!bQdKl6(z_15%uf|?Y}LGg1Q}#QA7{NaOq9zlUxD| z(eUdk-kZa(>-I&Kfsvane7u5V=rCWiAAh09kL9L$$9+1SFBDscw&g5ffvpj+td1oB zJ?S@Fx3w~3g*i-xi7K+tOruX`Ni#(Jrvr9i=a6Lk&mMC+7gPANdq`zCilPVuR4g1g z1qyGsKM&FObpCJRkOaw1FrUSih4WDM`rF6=clUk-TNaA2Z{F?Q`z&^0x_f;8lWi;T z{v|lN<}|+N!3ZS$^Y@00d=`zIhocsrY?qL=9KFEnQ|;&55GIdaI1hySK6G7tTYczl&=5^U%@KMD0(9&L45` z;M7j%s6EW03K!B*dst&jIG{#8WZeZi%RWv=Eqpzn0VxFYe;Cub-24a*3t&J0;QA?c zNTxQ5EIB7rc0V(%^@C0{ef1}#BNYBdS$KEv$FYxa$tUy$RGjQw)Oo=-g`<4xK5=4R zX@0dB#dQAUJ_ORZB;-$5>dbz%$RaE1(D{@5Iu}Z$?aB`G0sYQ=@;;`hGt3r#@`2(E zE0^duf9%?11j)AfL5xbrj1afnd%3%Y&&iyRa`e0P2F_ggA^o*GSn|j>18kYU5(S~f zABh76FGm_Ld;cfBvB5^<4v*+JVbL+^3=3&4A?7Yc4St6?G2A>6+UJcs4s@(bCICJLQ;AZm%97ZOSKt1td2gTF{<{*!K!HGXBv@rA&;ekLi!LkSO#vFrS zC66|^jPK7ur4^h4bUK)WLxFkbIk$=w%-3OWh|jTj)k2iD#F=Yy`@sTC>~AXZe>gb* zCo$uH!2S^DxOocIMfsaHKkj1;T;O(`HcQRwMdo4C$fff*rIL7Oab_HR z1rn9!;f|_=oPtAvVhgR34Hlz!7z4P%eDA)~ghxk0`%?eIbEvI}MZL^IFZ6R|C}&v6 zCFF{-RN(125ghR;oHKEPb#MO;)d~2!xEe_eceXFUt!<|<`AZOqKoaJ;;IdCZ4)Mmv zA*J-1iLM3n+imc+EjGt^f|z4J|8^h2uYvv7;~ zd}kp_ONUA^IVHd>L2$B!F1a>8gg5x+$5o+|yXFb2(0#m^Q0T(1ce<}tq5HV#WOB2G zk1PJiFET%3QtrR3b$CIr)qLW_8NhH6zAu^@(_R+?y;|3T=3=711I2+&{V+7;s3|;n z%L5eX1@oRFw>MD%Is*c*cMwG88lJ@2?{h(Lzl+23xR&2J!RsRDV-6|LIf_I$vZ+Jx z&hq1nmA{+;yOOq`9+2Ohar|cAGh;U+9;0z_g1WMv8Ra{u3w-W@*0@d;*)t1mC0lMS zh4USbmHF=v z6jyKo!eu4}{!s^90Rm{UtPdl1C%uRQ;IFowc1!Z3yzx=#{4diBSLAUh2$;@Ae$jhh zfrKRH9{m%Nk#HO%qpdiO5z$n49AoYsF6uW5#a#tKjNdohy+%~|$UT$|a@Y=!tg<+Q zunUn_HdjJ}6ZHj$3ZRjWpOXgO-X1kT+mQ`?x%e^+_2bBt;>j%=G0w~kF(g}QhL~i7 z1>2Z+C4Vr*w!;6w+!mPJ6m#Fg7*Bbz*YhkN)Jczgc#VBI#! zkqdZs^{=>R*T}iqJb?sXivoc9{Nc}FF7y%KL&3EOza!v}^y~G(wJ0Q8yy^nByYi9XMUMqb@#B#E*cYsv zwm+EuS2z*`e}(hi{xk?)t25zf=O44vY;Ck`=a49s1wA%f2L2k?2$KIr2|f@k^v=va z1_S;x(abY|AkA*#U+9O@f*yS>r{gTE+aZ1f!0vk(=eax{^ld~s_oIlB^hp>{BR_9{ zV1p(KVWw>B_Fw^u-t!l9Is9X`t9FVAGp^eA2BHP-53cOID|jI`a=bLS@-~!b+!tKA zLjIA*>w+s^rZ4UZ7Q8%ovF=cj!aJq|Si$B|nTYF)7D?&Vw46gXQU@3<5(%_2Q zux|yCZX7I&e{8!~*mk!DFL}9&E5y6ZpCMk~$5y_9i*q30{TaVSX8bh-K6S>AoaKyP zh#B9AQ|opFH{KS!@X_GL6~PNX!#gsxt-+0UXg@pqDR-`%{hU7^&RKZQ@E#@Pqj`kf z?g-hXc?vNZojDESNJCRDkl0dN58*lBy2w)PABSF zf%#IXV-?Liap(tbE--(uN)tv?G(U46b)}O0{t}cx#&?UN1)bEp59%$kKc@9o&AW9H zD%^$U&1$}i@n`-8i73lCRV~Zlu5fY$+qOnwpHLo-I+r(JrFmx-`dfflnvyl(JMEnwIw^m+u4$i34O>5sydTmXIhoDs!>8+0{KHNc5Oy0-7twT1x2GaNiVh z38LWhh2nMC?1*#UYQZ9=;LbO{d_wGD^BMS_3g3mYztX%Dhq@GDng64i!Sxm{ocYOn z_jS;^7q13<%m!-k%>>s-z$!BkSLdQu*5%#!V6XWJSiCDv1$(yzE0EN=cW;1gZd;f? z%%v;hW5jjXs!bWAqjd#(z)QSK&n0&l7`(Vo^1gvEFW?Ov9fWcy+mt}I23P$wGN?`k zSM3i9e-K=Cd9VnH(ff5cNauMnxV+eNfnI(X{EFK=Z!~YYx408?D!#X{FhJVUiq5^k zjr%do;KqHps0Vu=3?R4nz8Mz_gT0srINp}dhl3S+<1^rxw{XlDtjt9QEAtz%iE!~* ziJ{`A1r3bz;eO&37!n(i37W4NxFodYeE?k8_}5^)5wdT&cO=;IC~iAQ;<#wEgyWtR zg!>eP64ps}M&L;=d+|#;L2F?T|0tv3l>B>kg>(ao0kALc)1mijQ+WO;YEv^UaFKc2 ziDHc*#TDi|Z8KPX;mU86Jtn-hP2dUO3O)ci1SK5sPU)1AD1q@QU;HYZOKkeB4owI& zH^mT5Fg%%JZwympAn%albT|+|7MS^b3`fQi$J!r^C7YUm0GH<)F0FD`p|`D83-Oj^ zuaFMu2GldNvDZ>j>x`YQn-s;yNaU){cLjwtKP%#sz;^+F^bQ<_(HUEhT>_n9o7?WEt*XxKH;VeYQA~ zC?{fmxX9W%^UcqkusPiS08zMxvB%t>>Emz&5_~`D|}6mm2Ln^S%=+ z;@RW-G(4cm_cQ2OdE>|1$!q7CCve)yTta!C`2-V|MUgc3Q1Z`ZL}!tC59Sywk&RM~ z4n4u(5#K`gGPHo}PRTFA0|6a&$V~_{(jjtvk(PNH@h!QHvfv$G36}H)i~424_xP0PEQtW^pBd=ydopEXNH4@`;uOUIG`3k>J2;^RMmPHLL6X zoqNzEPbsF=^7VGcL-fH@H~|m)(##uCrs~p};H8fS8xgXY33fahT=!V;iYI~_;a~qK zUSDD!Ie~2wf6JP)T$}$3{941FDK_VtA3E{0Z4rt698tKrVlI&jM=x_Nb)~1(ID&yi zMeTDZ)baclEmz!O&q=X3rGl8`idF=%%r61O=Cbhd3iD~;%3cuJiYIz~LHwTf89d%= z{^A4_jYws~ZB;br4qjo-;A7F%B9i7CqSTD21P)XF-*^J9;~AOw525hHdYQP+pGY*< z{9kuo46CtoQQhtc)TP2_I`OXx;O7Aq&@gW=Fb@W{UJ8zM2Buv}Sj;tlh1Ps50n*5F z0YOfC$Z^Ud$J6XDf!7u}6sIk6fRvRB*v8wLaxvP^9DIbW|4XN9_Y9&(f|Gy>0gHx z!Aghq`e4!hPa*w{uxi9;>cN$yQQhr?azbvyM8s&6T7}Wr$v+J)^G#dDdr{}5E%Y*D zdl7b)*$Xj|d#bQ%mY3OHurK2QK(Ju`K=J}*?cpXBul$Du7m7R70C+Q>y@`Jg=zVME zgPZf_?Xj0_^8D+BRtlYO)`zcDbJ0G$UCm6UA>jP2cCg}iW4q)_zy#^ao^P;q0e5%7 zgSmpK@7QMaB0dlfZN4B6UyY4Pq<;V2F5IJvulK%)O!9VcsM9Fp$VT7~MuPdaKsHRe zS0HhZz8tUh;5D4k%3ccQi?W4a2)6(<96x&rc4D$GnJv;NH9LO>S#684&yywfaIPc1 ziQa&fLifgRqxANW`Bpip2ex+sxRp=An?4rFto=MHo&v1Z| zD7-ti;ZHl??xY|pfW_z_en$o|XQy6XuONOEUMY6azks&!x4q*l_}eMl=-PR^lf=`` zHti@mm^mF^#NXmd{ORaGz(~gQ@Ar!psqlECxQ{zJSj`FJ{f-B!Hx^_+4-Yo=q0?@m zqp<35SLmXEJR|(m{to=6jvCBY|CX|6ihmdAJJ_v9g^{3VfSr` zYVkRa7cFpOnOy~nVvNm>>OKX~aon6^j^Yq4KJC(Wzk}cCkmB6|jIe5FHws1p9idhb z0WYiCfo(p#W6qJKl|i8qh0s5HqkcFX#ch0uV|OCdeUps1@LmPU-=Vz@U2!%o*yJx3 z7q7@afYqeH3Pn2Q8{|pI%6vWSw_qMANF#8i$fKulY_GsTASwdi> z`m(`@xy*%%UVc;kL#Op>pDyLQ_(JqW=X>=f2sCkr+qZs2-&$zf{ff3*(@XowV{&tj zIp8XQUW<+VNLryNI<$EN4MhKnMX$kLN0Lpyn`v#@GrMtbHZJYzvn5u_=UuF71H&jC?s>7dy@b z*LP@XB5shSxdbKd61ZCJzN*KAxlaV=e>=E*COH2THy$pg9CGKfV#jN?%?~YP6n6t%C{$j<-NgWeZhv_VEO*w+P+}(gTV%*z4mc5Xngrh za4W)=C?0;YulE4{58+A<4TbAa%=N!x4_|Q6zTj#&eO?t@w3{Wyu_+6Si;_0>?s%FW_nA=OGWc zFF5b@!R2=Z=OKWxpXUp$Xg43fP54NM+(6IbQ==ITe+!3f5fxcke&b^sr)Km3OA*%xeme{e0@#^ouFXUj;ia)dA7 z`_lH{+S9?!$fuFfmhcxhB@n!ITX6HKVCnW?!5thR9&N>__XRhj$PBP^e{eI3o}!~u zfxd~Gxa8-j8ArCC15A7E{qS?x!{N;^L(TsN!9WHuTP`W?ZDr3ixv0eR! zPavxUwYV_)MXwHS_=x%w$gY$}8~zO7vt2_p>oDg$sYOL*B!9O3@i_ey%o1>+r6~Bj z14%B$n-+ma&o3TCK@;<3YJ~I6JH^H;n7@J18hlGi9hpahTiG~rg7A+K3k*LH+QJ4u z>e^ojqI>VS2llb4Q3~)ZLoMyIU&j8 zTvh=R-uaVYXUA;~#@FyG4b}V{w0s=3%ze9Of*CNu#paiAAS2z$@)@S1ci)Ag$QYUV z>p(YZOOd>S@S*+Mb&r0rLlsp3t50uO=7Sp`X?ySO?j>-JOTkXwjz|3G5dL`-Y~~e* z_V?X>NNb_uTzMR3&cl~4dv$QJ6c1mxc5k(^w`z`$yo!JJ9@u**m=E^JW7!s%|2PhL zS_E+6>M#In2+=PEnuyuCF#;QZeKWdLh~K9WM}Y!9`9AL2OZmFs*bp9lpgK5Zrb9r6Lq!_o;>@3+AgusOaV@b>%&HpUn&V9!?n#I*$Sv zpM>ebk-zyq)G0$V>&?3*0zKb+=DwLH><#L1+}3|?$8@ls;Vr#*B)H;uFkoIlBWAv- z&*1mD2qBw=5Z_nuoK4A@?>rDJc%&~Fcy+Mxba36a;7-~wuQ5M$->IO7p|b(=4>*W@ zBkx|1yVU$P$}PN$w)P`xYAFn8Wd7xN>+Wq3T^vq7F2Mc{?o`0NGC0V~ z>-Irr9o&A${=H~>2yG+Gjg8CP@ea1R*lgKH(pOObis1kjn@`=lPxgcB-SwDX7eqD0ai((4lQPI~Q7sv7BE@Q>a1 z<@PyXlyz1(broPvJ`r5_pWC*+lL$U_AN*l(@+U$JvL8dnt=bz0`IPRrv!b9&sg zPz%YrXx=Et6Sj-q3okMJDKKFq=oWu2Vua7<@;RKW=A18Mi~|rS7nv1WG3i$7mO6|l z4E=W(nUm;}5PjJ;R+4eg_F+8p%y;iaF6#`E-TplMhh<0b-g)Np#mCI`UZVZI@K15P zk8!>oX6cNA1PQJL(Yb685W*Q!Cm()u5;VYUHmb_i&xlZ(Z~k89=-*pnzK(rO?!AW* z@^jA2Cy}$ASwF|U{3O(^ zq@U5EZkKZnUq(+GsB7X1oUt|=@5e;3=e^aLp_FsTQ zeMVhIvQ#~E?_OkXAr0z+{X3H6xro1WZ(HHK@Bjx}z-g6%g^3kG79nhn`hvKs>6;=96aA8M!A3;%oMn#p-Fmx6O zMOcQr<3*MJQff=gGhev3K~*JGt`$toqn%ix9EY^d?V7Ht^lnUi9{oQ`_zDSZWx7}D zD*ZR*bNEO297A-!h3XaJvcJMST7-=(dB1>pR9DHXuJj&*+h&Lk{TFhR(L!WDk@FWQ z^$~k~!ApC2{=%gQHO&l|V;dm&E`<@xwbJMrJS0$?i)4kzsq#~gZ%tJVMIW?~5Vi=d&0YsCvH==>+h-Tif^_dJQY_0`hHXTyoM;Le7y({kz6rtQg~EOu6v#l=H?bpU1p|2+A&YJ!$lZ4{W87Yv z%9{|Lyn%zTIX)T5K&)>$Hu(Lq@rO}%<7Ue+zRNXbJb=rhxQWm>L(n^M7D%FEQUX#a^A-kN8vTd@OI#biCiNGdtCEX795z zd&BcPv&+o?HpNXkv!C{727aA4HO0p-*{Qv~!PNB4ciO4_#Pd6~i_B9<9kcQr8}N!R zrTKS}c>|;Bg<@&R-{IkW^W+J;SMfuL&XW}__!#ahRX%xw3_`ijhuS;Os0TqLIKHC- zi@XCOoote|eCKiGh>-7m0D#%s3uY2*nL#VcWBGls!QKtQifKgQ*^YV{UWbai zj+K0>UAfHrvFU6}U$ElCc%tg$p9%}xVtz&Re7*N6Yu>%@1U);#tIVIZ+pM}%JP1RA z^frADfz18EJyBEbK8{ut7` zz>vbV{~Q@Dpi(fufR!qk|ATO^to?7pUnci|lWxAwm0Wo9R2kuX^FQyQWxIr>dF`e^^JqZ5U!nZ?J*Ts%jKNaf6a6!A z*pci1Jv^szu0JB@7SWK=H$Nkh{(0u}Nc+j)`p*Sx{4sQXnfdtu`%}sESsjBnelbw# z^_TY`0vFwwix;qqg^-Q1C}?|im##?aKP&L&xix|GV>nWV6yH)TnCsOpH9LZNyMk2)2S(7DWnLQ0`>6iv3C{hP{xV3^J@3Jw@UJgn zq#tqC^GIt*=y>l#!Fkxj8{R<3^4wFw)%(L3(&upG1cIe}-TVTQM6o~e>e~aHQQ(41 zpGBO4gL+TLX`~cv^x8TXn~zQ+XQ}g>LE&b&57~w$JW3AD25dK9)?q$-f*q*C?3dcV zjG9A_1Q$O93*bicC~}*xn+Y~Cw6(x|xgE&{IDZ+<6xX_fHO%-a-l2|a>jb$&8)Mzt zx#sgIJaYK~8Q}ZdM>N=p6UJA_!!svXaDikMnD}1k4@-M=1AZ=8f!ujBI>MjuEU1P%0sjxQ zdIIlnxXtUj$Ae7-?_%={SYrX*kLs%^SH?+nE($Kgl=L@^vKvJ^zV3`d{c1UI88t$% zQRhx<71+8V*u>9P1h?@Y99_}*`e5lDdb3w=im})6Sa3}T5*O>i?R?SnVj%nh6#088 zomN=SEMsO-6?*j@=Cd)S=jslWmq%Yb0K@jO^kLV-d3OdE+@!Z}w(q~NgMokPW+Ys`8dK3FGQ`hsH;aCbcHGXo_w%*gEqMWWw9>E z>H}{g&qcsmR=9V*&H-L<_V;vM_XSs(rC%0o{2t3}3$9*jUWb|@bIofW*O&eoeOd2cRgK4{T;@@YxhD}r7)3{)*LZ$AOk;cYOz zfwu)@vOB8X!t}a8Pg!?l!jRanq67-x{ABwnf#c)4R|OuofW#^ZBvPl|5*%Iv+j&}Q z%Pll-4o>&kv;$=Kfc<>Lai3oY&y2XwCqX~pK8rW%YT~F=Nl3%ZALq9-Bc^ z2hGeJJ)q@3j?w*c(453Iy-S}2b6LV;dU*3k=hU~$`ErF_^6@|0muKckF0XMPByh0#$%2Trbb+A{&WtL2$q;H3C3S* z-abe1_Djb|iSBwXpF32H@3SGco0rYd2ST5NTyCNxcAjxwqxwsDVC%K!&*rF$<%`GY zeY)11wtamOZKu69zKf-$=KtI_|F_5JrMuQVYMcMtV=O%8u5oTddMX^XAKUQ&wW;2_ zvRB9dY1_dUEUeyPJNN>0;Ac2~f~yr*QBy$tprPit`2QY^iWH{iUs1o2)cAYHwh6_F zq%|L!g98Bh{L?CpHLbq*9+lFMiSqK-$OXLoAy7%d{0S0IMU61KA$=smvQKYVEhDUk za-ErbCfJB2F{x#d@K;5r^h6%2E2sn5J!Y{JwUW+;SU~fsq_3*kVO{V9 zyX2p=AL9p2a}n~AvVH*T#Bpvv0LcHF07=!6KI=Tjlq>!FvuRj;jkD990mJ@2Y+J&! z&*Hc#i94@{s1?y_{=K94m&P7gtpe8kEMCcQIDA$%vn)hl@>H)17nH$~>Q738vK zw_rXUU?0uJ(%I4A(&CF2K|;?pkE3$dl1GH^iw|H%b$aZ?pP9u@d`M0P2blZ22sILM zoORUc*zM=EoDX z@Uc77PSUGo0Qb!bcpzxus3!8EIr_@jar?jv)%zve41 zGS6psn5nF-`L#KiWWg+bS|MgeaGD4g{TsM>dFRtl2Uq`d5EPfAy6Ho~Mc)bvo8cF_ zJ1Fch=RA(Y{s0-*3(dKwta}Vd-gy}ycl1d(<6nSeUNeou%aO{;7CYb1zwZbZ|5|YI zc6BKMa=fQl^3%teRlV4J&0@`4j?)R`uCFt15v*B&=cp5M6qV#J4_-VHltzNfMuKGz z1usUD`6cF6xHMPK1SQlw)QfAFF?c7kjxIA-F`W*#St+7a++M&`ZuzL5I?qbF1^6fb z?HF@+o(yhcKtU6)-ZIC!#m%d@7ifcdvkXiX_J8WI3j3Sf#x7wMR$<4^f7AU~u*~c) zcA<9M0{kbvyzw}dZ){M!2Kgu0Xd=B8%sb|slB%#&&;ges%0kvz9uxmFDEiA=*j$W( z1?FdE_XJLVxsN*}aI1jhPKqyPf#1u`|G2+%2HU-CE9NKe@7$o(W-maCUvT9tKgFVy z2Z9Ukz+bLH$3D8(;jjYrV7WQtGd!#upt*A>_7ApO z%rU=!8VSq||I;>Z$CzW@kCQ6wb(kbBMR#{jkB;_^*Xrf*p-S7}ft|a{z15!0t9p*e zMZL79JUTitRIX1<4c4d1)q1TLZ;bTxtQ#vI8?06Dsq_r=?Or+9c3Y*sd1CxX<=E|L zaL-h=Jbq}j(l#+!nc}z0hAX3$x^_@6bq(H88DfhE%e6}Hh&EVS>+V*^CPxQ{%k}c+ zE(`+=_fJ&E>$}kx#FKYI zWxnpl7B<4M$*=|wl)C((0bYYc$I63KmHJ&%6GvsFnCRemwO+9k?XUJ=$fZF$i|TlT zS;&;};`q_Ael|T)9UUDUA1rSe8J#HCufK8G=*0M;Wz*xe>Y?$<@G`*X#%1#87QBqr ztXGEytLVF09QmE0a=o%`^HimbMbWl5ppEQy@!*!W`;W+y*;!W5 z7La1-uD0Ekx?62op~>=8c??TE1^5FPf;$y&+;04}`tVIRg}9~Dx@FldZJQ^?CZ~xh zgVW=j0ERqLo*u0aj^_;!Gu}Puuo2%y5Vyd>3&3ll-4;%N;`m^B`dIJwgQcFH^@9qM ziQ~QfKTPmX9t2;$8;t40P$U;c6b7;s!ve$H43_3i+j~$Jr>!z z*5>vxSkeKO*11>w{;Oj>96fksr1qN9nU>(i@Xd$1&i+0YbuSR;dglyG~!||8Q zaVR$qKigdgF37@^G0&=ldtV+7xVGcGONRiqlDRbYn=NF#4)Id>pU;CFrKRRx?a?NQ zs>!*p1}GsVYTy#}L9Rgm2>PaI8LUr$iPbBIDpN9Ail(TYT+vio>2dY}AOz6Zem~N) zS*CViXJ3pE*7nv0D&^thn6@%SArE{c*co!)41(BQ?^-hj);cjZSRNXx)M|rd_$n{U zhuVfhnJA5Ul;akVBa4m2C(1|=C!tpIn4T5Ej?4M9=8#}LfPAP@AKWt;;V~wAzb|vc z6%R>by!ldR>Ic=Hp7bI>#CpPl;vp1jjYUt^yc~mzBez}KY?^f|kEh{RqJU(!m8Mnq z+A2VPcp~IF&>d6_A*P}2wSuuz<`1UMP;g`AP)teyZSff#n& zWENK10ckC-Xp8tP*(s>i3fVM?=d6e$@HhZ1k|}_b3>BS340^D{j&G#CVw*Z^83-Ai zkbwzEteL8ae(Go+YOUf&lBTO;Rxtx76(QGzrZY%MImN2@RC~ds!SB{r#>)psD}&XM z-hH(xw2jxQwZVhc`q3(wo&^qH>D@DUq-XV!^5`@)QXo~&s`~NCO3!X!octHc*pO_A zzl|A4L=y2ebJ(1rb%)rP0H|AEC-=ZT;(`%NEz0lUXl49R{Vs6zsR?lIID2oScn!4< zOBC5-3-5~7t-~UPxo!@Yhn1T_SXpFuY62K{Bg~=%^;ezBO9{$L%!;{VVsv`U18-@q zCmRMg?cTq0b0ab7DZTm1x|KU^fa!+RTO<+8qmd>GO%Pg6Nfbe+w^&Pwx=UgO!CrO@ z4&7B5dM%fzXRN<(2y5mk%p`mC>f1KFUnupW)oK_MF3u}nPfGHXkn7&&lwF7j9 zT+xKQ2hmzF7FvTCscJv6#vu;@fw>1I(TjHC`q20^tg&NI)1XnrsE@ZmeYK(TXn9IB zR&=f;ccfK0S7@GPP@jW-(1ttmjIH zzdo7hQmD<7V#h$G9ER+`U_nz#u7T@HNM&lOd|W72JI)-tQy5~j7YdcoFo;o)H4Iv? zv7i`O8!JUhb~Gu~F;dy`1b3toCe{g>dz|+`R!cYZVA!wX=mT@+*+m~!al%zek$7me z)B57rG7XNAl)-U-XgY@zitMR`O*Eo55$xNfO%$Whozr8^lCP0T4^(PkfOHkG3sq4| zC2!rp{2a?6W_zl=rIWY>4w;dC+Dz*SkuGK*jwI~E&~j85i&l|le5*tg$fj0@@r@*5 zmJO)mkLiL6g&ZX=PEnyMlJF5(dz(BsWYo6V6lqj@)b(%zy(O}+jCBE>f@IX72ewW-aPRJL79Ne7&oXqniLX*!hyC^uFe&z6+w z$;k=<$AQ_xgMlm7L22nov=sDx`T;d0|D!{MfG%Y&6e)p6_6)2U>3N5^l7ax{2ExlYqt zG^QhMb?XeSwcWP!70Wi?a6_rItkk`td&SCS%g5@|JTo=Q$u%cJhpDl z@-?fLEk9IRwtQFjvgH`}#v%D^`N;SLF7a-CIUJkiS`|*v$ma%`wFNyf&Dsd~MNf6BGv<3sS=j!#V2mQRl#t&R^b zuTyj2JNR_ywaX``ChFn@F8gLnGdGCR=cXkap2UYMWEi<7DUA zn{6^*!Dep}>}UQWh$kaypHSL8C-w=%!j7^;0TgaR-Y_6hG&(U+xA?75>d1#+#_`YTh^ogIk7*JF+3I!Izp)5uxvIZab68{S>t zfheJ&xYZJ8`UxnBdmlmv1b~(EX0iG9&^`_g*EaQ0=yGPFPsnXytUM6T7^s&xHQ=$UyT1t5e-X9QK0I!pSu@DK5{@2Y%g)H^k#o{uQlD zpd>~x2&NrI#ty3$D{v_H?p#+cA3Ru}s#Ho{n>O8g>mK~-1$LBH-a0WoK3wCyo_w!a z-sh`!P7K4FQtqW|#p(ZPa17&(VgI>XVEX{p>Tau>Nq|Z$X{EN!qZ2iRn1oz+jfdnc z#FPMfYlDnFB6UO>vl~)gD=|p98F$mU?v%t#hMCu#LN4Xh?H;6;3Mv3}RS6{`@;EV7 zJ_OW^ynuv}8nlY#&4wOdrBaILQ0H9D{{|5MYJuyd=sRzcF_<}DVf6C4ed>_t26Di% zAJ*AK3ncu=p-9N3WZAkR2Yn7^uZ2=wDc36D@`Mfe&7#{e$JTx67!5K`FL6*tamU#$ zfneQs@#yWodxN9Qn{IOO*f2aX4Lymvb9|KIQ%r_v3|rONYezB3uuI<%Zv3?)O!%ZC ztzSI95rzYfBh_(46A)9TL_{FJhKWB9FwKHoSBsDXUHU=Y;e^o+-7c39%EyPwwR%r~ z-!LeBV3Z(mg2&1<_3x|^?wE>sr{r2%-xyYWj;Kq3 zL~&@90~l9R6U4v4(TRx^VT*SQuRJFto=OJ^i9O|16DLs4dRVVggfr`$hOtIv7%}f~ zf8Sx^n)jCDcFO%liiSOmjZ$HVt>+$S7{uQ`tPUIUeA4hYk55dEk)*-D7%A@UKO7k) zhq0K4d!XKk)UX*BUC@`Z=uwhH@HG-Iu-;gHN=Nx7m%>V`_Dlg02P#9;Q?=@mO3&ee zzSz()6`&$0Po%8nM2AE5?)s3^Nqm#pL>|9(z@)l%(@l}OcH^>0aOAxs%LdMolGwwZ z8K-Knw=!{&S6YqOQXLw{#Q2WNFtX5kKxZolRT6^g1fi1(aJ0;c#(aI#(HPdn>QK$Z zXeRG~qz4}d*u`^_HQSa()3ZK=KGoT6_&A{cd6 zc7yMQ>4w1|mY--%F`X-|f+5H1wUy^fiFA%_@$UKqi;qj#l(!PIg7CVbGE~@VotZ{z z7N)peUVD}(!>~wKceYIE$`=ZP|DG4SneNm~wMet-CWDUo zNs@PjlSjlB0d=-iYO4)k97|7-Bm50sBn(@-*VaI#a1 zENSOcz3#<)u?4b@8`Zh|Hy19R;6A)ul=B){!$->M_?*3bSw+VIm`q&R zYjKX5c$Yg>2irU>PCzUjlm~vgB=nFGq?CH!s2BMWdLCz`Rm9PuwIMggJp5j~~V!_|?I z%2WmWLNqA`WPI!3?j9M$GgPD+!YiA~lkwuiR)3xy@+C47fYJ^1W0*qWmVidipcu5( zb!gYm!_CHKk2SIZJ?KUq`5;cEia>a~Gxow8ex1 zo3Ni&-8x+z9rjGk&;py?vH?vaoj=Ue2fq$S<$F>vB%MV<4l7x6PysGNoaRL zZu%SO|MHa67Dxw9+vaWsyp+g%X#Neo%3CJtp{qkx^^my4DIXzUVsWy(2Jz($eF)9{ z%akLN)3v+AeXYVPYReA}M0b4zqRE@Wgu|Y41~VkR(<%=@44?s=DcKAK&Ceoj_<6Q6 zH#=bF=rNAIwO{<)qwaFYVWsWA?(!?6c`&p}`k>WjT6EiNy z;$d(w1&Um||MD4y{v z6Uv$0^;L8lK>EP&NT$^zcb7XHd$+-rN~NYg*sHjYY~T9yBogpPtFL8>J({g|m2g+0 zLZKV2$^?AyAUrLtb5F}PZm^Wxd_L-N+4)CoYW@K3Mk-MT1f1#^&bu3V0WtBJ5gs6* zG6@%s&@jKFMJHrjnny9fY_$|Hk4$vU?{Wr`jj3I4si`$y81EfoJekz3D@k-*It!_fNjzsQ4Tf@l){jRwm@uS=LO6lP~;YXFhla;muV zr%>09dun=}pQ3pf3!7+qxWa>Nz6!W!!g^w2+tTvpACp2{E!tp+ND&3N!+4xqi$*JB zNHR{+Rm4D=#}~qgRw7{uQyV5L+_>i?5)=Cs*Z1|a_1j%6z=wnpR`-w z1=$7;4@8s~=d5;BoE(whV6=8=-7XKPid12=I+KazeH&Yl3`W!2EIw_ddU+8D?`_N9MUb#K8yld2WZ%dGmB%98!8*1e;7Ec9be7R5#oB* z@T>!ac4@7J335*+5F9?7r7%4U`w(LaU9Ln}{fcpfV+n|aw;*Y>=juHzEse*eMK=nU zCaw(l$Q&8vjtREZlS6`MlAPw$sv9uNLXFi?~s!*mQ31>@qC~nxw%&_LgbkPe~jCCQRhh))Q`3PTqshJn&@+> zIFni?FKJk!@417jbK&PIJJvmsW06a^HpOm`uw+?Ro=AgX#{7`-aja>Ko6#W4NDR5( zIX124*cw-jD>)+3C`lpHrzWZ}*?mWlzYVc0q zm!BaeD3-6IH$Yr3%p(XK$0nzvhIhr+>II}CM3>6yTAo*}ZO&$}E0OK~tpg@2tjnms7 z^MY`vJ-nBVDh|2rHBmNGzDFcJgBJ6MEzU%)#AdQ4-iu9DmYjFrt%3|Ur+A!=2=h=F zh!S#t-^3W}rIxBTh!Co^N3(My;Y^8Qzt5eLrye2+eK|AQXRm@1-gF#m^IQONhT|k; zhO1TeK6uW<^gAXV!=r+E!m)yi8-A{4d=VT2W`dxf8i2 z7sN?~3{^JH-b5j3QZHrLtCymJCe>^N?YQa?Lu1d;W;2qTbez|!Dq=FKRk{kk=9|Ye z#G{hWYwhC}*(~2yNe8OvjxC}^5MYM}dz5(cEC(M#rB%?acTZ1^z?RqqccOE;G^)ze zu;w9Fj&578=HXfKoz=3_-HoPo|Cv8G)Qt?yM9IDeTglA7b|bXZ6S2(J`?b!UL_SbI z9IPr2XWfzrsE~vq51;p)MG&$13RN7=X& zDV;pc$z+JBFrmy>t2)~H)&jaye*l4V*H&gAGBWC(i;!f9FmZ&0&eNZAEv5HdmAdOw zRWCcBf0${qo4N<{bGyd2>&D!ij)j?~oxT=lWBa2+`V}+Iz7evc$PJo@%y9>cm$MxW zt+X9QM3CmW=~%VKlz?i&-^SdDdaIQ9^R2@?VmK*b4z_^7=vW6t^b*cv9Y zH51n>u_-*WL~-N3RhF+&g(qg$Sx83!i@A^UT8*&|8!rUn!}|~wDDbGIxgSEg?^$sy zK@!Jm8pjqJsJ2lc*X?+NayF}=ywx0%x{BqT$!NT9TiZ2=?N#x#Qq(?re)ZD9^6=s5 zT0N6nM-0ptSWUcAG=RK0G)ITfEj6nZh-0M}pMt)q8@eJsVqxy7r!mBbnj>Kos}($x zVZ@9kK?9(K^G%+mHDtZ6uz_+mC8=tky;HS;HfV8Jw3f(E0?N)w|L_*oNlj{7H{E0{ zBEc3PQ79fq4{1R$h)G-~lOJL_QW*J>je2%5QZSPO5%e_0m(Z=@046Mn4AkI5=I$%D zC{1cT6*jH%=v16y1(?FZ%X;mma{xG|?|JZvFpvqF=aT zaiwOp2-+SJO^$ zs!`4qL52nCwHH7PL@=#8nuW691joCzi6Ke4?GmiFVXH|{MoX_D5P@L%WsYb09TQf8t_Ugt?E z6gsEL`^Va{s))Z#X_nCktxX6(FIKuu^5XQ7leCXSE)=zEU#CvuZK? zNUVPyd5*|x5-H9K?l*S)IV1_k&@g&w#e*w~B_?LXS0doXWQ|MW$Rq-5+Q!sin>?)I z)20wR<=Szg4A-)XC{%0?xr${bE1NPC6wJ`%?ZJuhitrK^M<73m^3WnQ&rlWs#;9Ab zcu$($_K;0%?hFs~11B`NO`hA8sziv|Y}v^Q`AhJW{bV?vPSX zg%M&^as-4RlY3{j_DUQG{#3vpk%5|LxTI**`BvyTiq|D$k%%xJXE}(FOST#|en`Z* zbl~l8X)&IrNycrSk?#q6KvqM183DeKs_b%Q$EWhvAOS|24J_dSoeHchHVeP#UbsWF zrDmNZ6Sco#4h`oLAH55eLsyak{$wL3!JT z8jVec2bCoJFC40rgt9c6ja6f#ImP1~BMce0jMrueP`e2q_&%uy?wb(sZqrFDohzQ4 zIt$j2FMcBaL8`)t5jPFv&@M5mNiGG*+^wAB*aKS2%Ew?DL7w}qBKnG0o_Xu zAsj+k3eU6e{Pbl<%!G`m5?W!MkcCbU_wpSqa>Ozj+?x3YtvGx=7jg2LM~>at;iy}Y z5{^ee6RCchvK&Rex!pXYPSsg#BVRX&o#d9w%}SNEx}&GIXLwRPe$&Y7nN&A&95RX8 zd|o*Xpp?tfXf>ZLB*=BHo?>r|Bw^`cTmGKG(#j(|1V;DWhpwX5Lwzwmn5-oLeE?>Y zl$&gmU?^9@!?f1m7jq7ZC&H@c5NM&JL(A+YD-7t$JCwK58YmxTI>mI&#>0txy-|s( zFj;tTvU&uYcw;2+oOlMonUXp8xkdY8YlF3Q%uZwMUhTe!bF$ie3o2VHeA%VIStD@5 zcZ0^2V2_uJ_Y*!Dy2^S-sn=-40Yev87l<&@FhYKhGj^C=f|p>(>+CRFdm3LXvO|Q3 zNW*2Ip=|_s8Q-0~D2W3(SE96T-^Ck@SxaKWx*}t`sdP+A&++)Wk?K_KdB8x=)`_8e zLtzuaJnqTLoU7z6B*ITh$O*M%R;@n?T1Ti63%O9DD|;d z3!PoEWnvSLr|MmQu&!9z-4jkD&iWRBTzXeRbd;<(*YhXm})f~ zeFl{KbD$}JLb?o%PDPToYHerb5axOWh>A+oC|!v0PL2-rMJr{^^cI$M6&-mPTR$}j ztjDgYDt7z`Bzd_T41>_fYbh~mdsg%m>irz5^z=kI1Bv$%c&`P3RE5c2-Bnw5Opo@C z9qw6mSM|_c@EV0n=+-I&ra7IuH9_r&At>EbihMG|T*}n=pN=r+P1kJYZVIi2uUdp# zYXKZ#f4YAUSDoJ>mrTO73=yqKdwICuWQHLyxE2lI-vp;!1|_gBM6vXMEv&@z2h1<; zJc|xPVs)}NlXo^Au5wQ2O3hh=hyuNIhu8;EM#cBuII^NL(Id*%Pf+M!$(x9HBen08a=Rj;1c>K(Z-d*6}a{ zo*)oWPxIj}Z3N^E zgDWhwlQV&)fN;3T@6%}S0OkVGXucYo+M^u z&nFa|PM|rDY-`|4E?fo@rRP)X;%XwMziIr?XrZcDdy&y9&Q|=9OPM!QRbM5(+$DXKz%(cJc@GqLZD}?SROC3sGQMA z8Wc_|?rKHD$Eh+7s2RgaKq$>(T}@4-ulkB$dQd)suN}gB6>4U65nKq9Re0&2ZxOfCu&rh~`bnF0iA?CTh{@p{Q1T$V#}5kyZc$Z=i1k zi^6pofvIWbIW_F)tu=ZYu#$8pkwot*MR*|Z?-%9sev+v}qV&qgDKo*7a~8fS-o-?X zEUQ3l761>|Inoj!u!xz{b>A$Pp=7ih!6h!1e5A<-N)ly6M1;nnh9kUX>K>k`lUhWg z=HLe4Tph2|)Le6qFiTz3Xq;lGfzNBh-% zngN?UBoAROHl|oau9Y<2h*IEGg1ps@X4irRSi5Tq+E>pAmD#aeBQ`w}pLel5VA3XT zC=l~tVF^K^eRoe+hhDoIC+j!{yJ3=Rxp{0-LWE~aBu?iXJg^2D1xf*jT6}trc4hDx$ER)M6T(V)cM1O0x5!(_KaCKkVsKa#cyTx zsGVDXTCTDyx)3A|l539~gm*#=KX;tDi$&ZZRXws{nK9zE)@JV+@YW~;S@KwuL(*29 zRu1}3LPegnv*|FV{+vw%3Bu0f0FBW+(MS&5Q|{TuIG-eHA;ggJ#~FT(gpVl=*-CU8 zqvNy|F0GRd_h%WI4!JW{z(00A^3J5Yb$AhKs@6?OU}iKM5q+4kV10EtALQY#l>%{2 z6?q~ux=!GN6Txl**qUeU%Uel5-SWcO_FMsZC-7g9vYLzGQWvB$oiPb8989`*DXoJrInFog|j;wD#g(VdK~dt zsn8rL6LL5VCfrXZ)L^)jtiovIMKNaG+ZmlUL!q$JQ4@}6)s_}LMtQwD1(}#opV&QA z9xYF`9c$tS3Kz(seNWaX(xevWlCEoL6m4*O5sLlg>Qn>JHDw}k&(|3IB@sSYyvd<4 z%PUq9y{*Rt!5obV!f(uoTu$#H|L7i@9tEcvVEJPWXLWalZ0aS5@*Sv5O}KV45F@Ak zh(&r%0oP1Z7^~Jttu6l(!CkRy*Q|1Hoz3{f%F1VBj)g0B0pI8bfP>0)nVw6A)@6b0 zzR4J79;S!5n&1oRZsTYge%Gqzp$e%3#w;ak*jw87lEV*NAhmmR;%MY%OwTG!v`ws( zh;({#`k#;@{4me+lHxal5Z$^ep48i=vds(7}5ZbVdAGpCT{ zBtl+fQqPr$r8RPc(D%cGGx8AKq8PjNyb@>A=lal`>0Rd>SV(t3{mjDQ%@5YKHi+=8xtnuU4?GbIL`8-DzyK%+C>a&iE0b^bb_1LT|a%W;9`t zahD^U^NjP!syll>SCW@jMlC@H@bNv70LIj;1VGkWUvH!N2@(PjJ-atJ`N95dKvZ>Xw+S0nTw+C*E$Y-0LWT^z*>sFULOWB~*>J-8B z7jeP~nY>l$hoKHcWK1ClnB+jv>2#W#-)NMf1raeK$zhEJEkXROZ`p!1-;`8DidYS> z$>6PHJQUf?@5yHfO?ytJKuPZzw9>SpNI~?Kr?pnKQe7dFj+(V>NjVkGapEc)L&Fse z5vDOlhwnR+%l4PE8n)F~WIRVoRim(n#?jq!rBPiBlg$$oQ^VEqGBz)$Vd7>GVs^&z zG-YLe3<4DBbrLq49*-lKaWAzdBDUc>sQTp3CIqRQKu-%+U(<<{)<#wVvk0)+A!QvP z?&XxHWzdPm)hC4_a+W9$1ugZ*XaoU{H2fO?f$U)wvoxD9#)!QOqFS%DYdGCyuKg84 zyA`Y1t*|4G*^L4-BC$D=i_uYTfXIi&pdOWMphglg7Apj{ zWF;77!(LHr%_{KgQHE&5LgfYxrv*GCh{shNYMCzXf@nKij<6(#<7xDa2rvr0;rkrG zxmi6sacTi5<$vUAu*N*9dX0=ulvzwJ&n@q0c#Znq8=2)k32LM;0uT1b!um{i<9tnt z%mT@J5^g;U-elQD%%;xh(z&1Hb+>HU*cI0fZTp^Cl}XA8A>(GKqF*UFfme~zh+}axgMRpsW}%ZyH@3GM-cAi zgcO~h!p+1UhQE-W(AfYx1*-(SW$GDg)`S(uk~niGDJud_raJ-ifstWe`RZlH@Zbll zal=Xj;)r9u11MvvjSRh#7k^By1XzyUE}1_S%Rw;nMwtesRdL%$C6=r)n=Y-~x_#HC zJ%c+o?YX_wy;)1~O^?#o=#ad@E9=TQR*AN-oE1MZ#*);PsTvaIwpYh1yDNuym}jkP zcu#dqB5r%?wwlB%ro6jt&{GjbL|YYOGyQdH3|} z8|>Y=rDxxozTrK{gYL#~kOMkG$@Yp`Wy{1g4u4L5xdumf*1Lz%G1jLadDNPc>kqB8 z3S+2Dk5;DkOuT%$T<2^3BOKV!XmxTA+&VJpeFxZfvyygwvCm&Wm081IR#a}ZpAFNR_nVb@2Y4t z6g$b&#r}M#tl9&VjGvKE8r+64Vn*I3_#{%r24ok?saJmU(Ymam>%4qSR?qgVK`91Ov>GwE>EyV(p7TjmhrvDaLmZN4eP{l-OI)>KdoZl+FgeE3 zTCQxRF6a#6Vmc|-7mI6Gqkp2-RvRx*?wPm^w2=Co_$WmhLVU!xS!3z8?b!{{hxs>4P84mrZgDzRJlYGK5#t5voT!80t+?6;e zz!k(=jY4jfsdB9{SgsFNuw$-YUi3#=S{IGf9U>^B%sb4@|2V#;j-5r7k8o~zvW5-n zQbaScbs@+nbPvo>SeR9d%(+ADmdOJ>Si&DbiKu}Dje2!*^mq?SUZfV5xEtdsmh7Cn;hoa_z3K zF5|%CF4Zvosd3=kxu9H%twnHaMp)=%P(&)A3M)YeCfdV@-Sh@tv_?YNltY8yht+zm z7sQ#-a^e(k8)WjN+A46ZZY^jdBD_^Tm(4FXmnUlw3^bZ{R*q&tQmI6l?2bz9t~(}1 zr^lECJj|mI+)DBkX%QlxpiHjG+R2I0!XLztz(M2@191+4ai%DmYii5hi?m4XAzmsEdjq_HPPN2%09F(K8gNPOREI$`YX?#|Kq45&L-cB-Ip3-{gl~=rfW)R5*O<20V^d zvcvX&Z55QD24TxS>sT2hv7}SY5Rs{45&ruQ5KR@K@pmA_LCLpj6Qf5e`-xs@J|!6B zNvq7klbHcS%l%4kUBfB}lTZ*$wPJHer97@ew#JtsW=9SSeJ9kPkeG&hV#L%;Ys^4^ z9IX-N9m!vOYF4SUOiE{gqiwsOMjqrMTODFYxjt1r*0!ZWVP30_AL0h3R#QkV`$$%4eWZyl9T6gD;}xBCg%VS+?;;XWE`gqf z8~2VopjmOCw4G+H33C8@g~+IA8C1u)64pGks2r95jm+7bZlb}W6_0(mm%4Wx_hbsR zI}k^LY)f&}SQS^E@QG6;T31v2v*jYYi55kFRLMDR5Oz^w$4Oi5HHRe0Vf%``awqj7 zK=&jYO)Ez^a+(WGAX~BQ@y}$BCGyS~Tj4sTmopAfD+U#ggCmpJyjdR^9J=eZY~C|= zv-pNJ(R>5g`@)*X(V0)Nm0xLrY3U3aStd|8Cn%ljd5E*V{*eqQhGaz{WXTFdd)Bxi z)&{NWhS(tt+835FiW*4kf274RVg0@tnOadAhX7E0qJ^2u7;54}uzGGvLJLlY`js#N zF=7ZRpcz&`os6-Cijkkg7VD^sy~sJm`j=_<#omcE=DS4JK?_r5ljN*oE2^=ZDYo>i z$`s7RO!YM=QC!>}{RDFnvR0azs)o`;eFFUCFz9DMD$>F=h53z3bu;)Nd65Ch)M#X( zt=Z}PR|ItmB|Pl}0+I`YQl9jOQ5#A&>&vU%R4a8EA+N-;fk?pcXbHm<8HQuPYE!9Z z$kbFD7kx3IbaDPEH$XQ%58=$Wr+kYRm+&qA6NM*?Ixp9tjVq9P1aceIQ8kdG$3Mkz zMLaDu6{vyw+9#(GDdi0q6H3PJhG`tCD6)AdavuT9)^a9?rsNQ_@fz&wdhbCVq#~w? zV4-qtD9p7=md)$jRN_Vxp!?b%XqPLI1>)pd#PoV=y`UiMNcmtbO-CNGluD=rNf}y2 zQKL{6#_O0j0#W|)hz%&$*-k8Y=K59_uQ!6mwe|g_Ry7n%7Rk|C6MHtG$Eh|u;HI`t z3YhmmJW;pMNXm^m;-7jjf-?{31dh8*@mZ$0K#g5baX7pU*AxqKe2zQ*K{Uk7uQS#l zQMXYfOf+^^^E2Z^hjcJNllpltk8w~xuwT5-!!;;i*6S2yhCF8A!^`+QIbNeTS1PI$2-Z#qUpoE0nc0y;{|AEWPmC{ zhL#}jPN5ts-M^crhN3?2k13?dK4Pvph=#1DDYL<`Y8859oqFnHdqU}L-C5i63&`fsC$X0 zAZ^lw?mwvc`m~wp-7*e3gM4IcVmOpH8#k(OQDb#BVkeG9!y?AA)|%2CEO0k`ExsZp zxS8W473&wDdDxd)u(&IWs*&hw>YnmZsewYr)dhlPD0FnPp~-XrB%vF5LI$fHX(afZ zs;-(py)Md_oJ999b~1%XMl?mATPEPxhLDwgCHF@GvE;{M%2dZkNJ7p<%A;x1M6Zri zI=ifj92v&RM)NJQ5sniU83t*s5*Oe|woyu$Ep#^JAqEvU&%(9?OSMS07wS{ZCn15K zn2RZHL5XuI2091(3abkV_)}x(K2nCFxFw35aj?T}wepe5rW#6G@q-j!$RbV`vGA77HLuI=stu70OBn?9g;?RmWHp<@E^cXDx0P%3DZTh817AnAK=?Pn4}GPYvBgz66_RCzwMi;Tr1k1%ZSGm!&*X z@dMeDk9jCNrX6?t5CWJj$N~CNcof5g6xbfjjEiATPGd`)w-Y0wQanVN;XtRjNq89| zc9(X@OM{{C0FtGxxpW}PYBlI7=GNP?&mvYzqaOCP;$f(tJYprOQ9EUJb}18fsiWwr zU=!(fXkvQ2&b5if3V0)Ar32;TcBx`e_D$!wtJk`qz|Nhkw{=C{Z7|ZD1?Mc%@%WLL zB$<>}?KqA>hx(!@gA(Ue#G5?c&G@GJNQFzKTA;QIJovtGLHD<~9TWRpCXe zmt1hwrek)C1UTaAL_nV{@2+$WqOY@LM2hI*xK6=ONKI6sSH?4N{#;*zA&N8 z87-ymm<3~FVuuf=wK}S(>K@$@rfi74LR6WYnt-cAiYj{EW^1K{zG$>i!xo215|*-T z>7+JNjHq%}n~VHQHbo}fXBcIkB*n#zYG|aS46;!$8jx{lCkSzC`hxmu+dL zTf%_RM-)*BN}~f*rp~ObG;IkXCUjG$rr=BeNwVcE8V+@?72_jL8BXaa&GOi zEfq74 z!^W2bFwj$;`Y80Rky(yXBLkA9v5%{u`5NJ`1F|wT=1RF@`6mMQT`G*AyIoZgK5o%(S#RAa_( za=adQb-|?ME1J#;1(OsGXkdbIk;@Nac35b$Gf~_VSSt2ouO^7Ilo=?n!RAQ8)eu5* zpmD6X93k8^X%`Q>*rGrfTZl;C`7cw!AR4gvk1l+0wBh>nsBMP{n5n!y zwV>tnKVdH^I}|#$=R(u;QdzUq)K`0GBSfYQp)sJ-NjO=u*Q$fV$n}foFey?14xuYZ zLTGj|#E{EcrSyZwX#^tslUZt!FF`}^9w9Z70}e_FIS?*HlxotFC6(nOcXT+>q@jV?MppZ?Q#SLrU1*m@<0OE1Uetu5lD~%*v<55ico) zt0!V`Jm!Lg?8LdPsn{=Y>d;>l|W-gho=4*li5t>a{x(%h$DvSb|jvTLUu2HRv?PF;?IScSqp~9vNdEz6BvB zmP{?FyJBicM5)Qi28^$!x|Xzk#~CG3>4hY$ydW1Ik>UcYy@z{f6{p(sC5bg%GCdiQ zFclDE@L{f8ayf+DVnQriE{NliF7|xnP7_4v*TXQZZIfd1S;(FlEXz`|)Xb#%VHLjQ z^i)NWZIsj}fI$)SaO!V%u8276n`Md+Ry{poo`{dgZVjSLg)HkX8Scskev+Qohj|Iv zJ5ZS9KXMqk?+J=g7r!J$sN-A{3zhLfTx6_8PZroczmlpvVkBv9K%_xOkanBtvLG$K zk!EnFXWPLU*z6}$Cg&*rA%7^uJVnj4(qs)2H0iR4{e%yHIc+>B9Zyg7p9owy3Y_HT zMa^k#OC={Y*>OSx-R*MFga_3%DWUPmk9;MElM&THn9k*9L#XnDR*kYOZ`1lP#1b(y z;c3Z{c#Pk=&qSAWTb`6l|I(buF$x-tm~bS3 znb4K7`}r(Vx7H12>}~Qnw$=TWV{=6{=HutU+r`LMgTuRF>Hr!Ep`R7fOCxP$04Bkk z(1)V5W1!l=K@{n>J_z)gy;maHpy*C=PMC{@CEpO>#Nw2Smwd0eW^ebW;rkkWvBSd{ zC82s8I0ApQ4Id;ppzZ;S`SI8%@o1t^Z^^9a3DpMt*0z)mr?yR-g zo`hydUvN1zw%j1vq=Yamw*+`1r^PxtEx6@MDj@8xPmkDYFEOSlc^wgBBL~P%`|+b) zp@#~cM>4|sKQf%jaX*)@n`IgheqUJ}D{sFxx1l3zsVx<~OF}8~A2;2jP%Dqc6X1Ym zUaT+KvPUyHn_(K|<&y^LE+O|6a z*I{yUEhNZC>`4+-lP$!eRjPR;UrEF!;B1T=xLS$I#yMmoK}MV>Bqd4Hp{%uA4qD<% z%%6&Rtq_5TVxuF>sJd~P{G(w}je^D;L~WDmb4f~fTg{_Yd5sbgJK(#vJK5cKu$Fvu ziM$O2uQ(Q&8AOi7VTAn1z6X(Xn_wi3G*-xJaWALT(^4X47s>Q=`*Ak9GRl~A=}&&h zAF=@(Uo%socv5aoKEVIS+;zaoRaO0&-Sf63yXn1B5+H@_Y|AEul58fhsY^CQKyldY z&L)%W?kro9pcrY=r6?$f1rZPt=^rA3A|NUXq5=XI5F7R=h$xote{OsCm8n5uej&`e z@4fr(J@@o;?ztY?@ZnXXn7DK)(${RuXU zmi$5}S;3twGGjz+ku`?9E(Q=-QC=Kv%UUAHoxRu;9x~ayH-)=!QoC{8IIP))>`;EIG&?ulBk13^AfwzAZx>lNzDt} zvXU3G46~8#Of5T)LXi#-Rsz@O^Y$ zn$!Uf4gG4JuRc;hRx^JR=@h*CMS0;eqooRCi$h0EeY7|mX978`4gFy6ywjBl$qK=s z)`CBtf{05<)8J6o(xq;A&HqG5{+Le(2t!xB;Ce5XQZ8M%>3juo`mod=a+|tZr(y7A z&}00|DBNH`s`lY6%2S+52|Ju0bcdq_tsV}RDo{Mj-?eZl1#(XmUsIkoR}6>S(IIMr z-l>$^XbIJ*gnc5nswir;T%7(X=w-6>d!`DruYz4Q{&$3Ss*|%&tRQgTtC9v&w8Jc0 zR;t`SgRH^3;mKIey~$qW+(Z&;CutDl1G$gF==H}s;SIw)l05C&FeTpm^;pcZT;W0R z>jeRTuy?=`>t4xcbAT+tg>J>^L((;aZV`SqFba=5AZ!i1>*m`l5$cd(#mVbuLPX#H zVU;Y{6<6L)cl+H!H`p(jklOE^F>f`Lmf1hx|77+uJBSEyZg^XEcRz1S!Suw=Z0L^I zU+(`(EU+nx;U2(UmTEHFR-&2TLS&`RLvUd<74lHvLot^5rYF@)E(R2085pC?_*DHf z&t54ev#`vf)QQ9rhPUvdc}p$$KQB0tReO`co`Ehb-X}1)8wKt~NgC9{6E&5*u08*k zEX5t_jaBy)nY-?GIR9I@!kr30a8PmCv_QBsh|avQih=l4XvG!$Bgj}vOuIa}l$iKV zA)2nMqO<2+hvk`kO@L{Z%`5TPR`FE08n!d zE#iUMcqKuz-fEDK6!5<46C&G1K@RNI-~kwCDPO2{7rR>m!wGj3iG_6uU-%=!NY4l^ z+rgQKmqPB$cjaDK-^x}qn;ORu zX9p@vD#HMNOyqTn0%qR)hs?V_pdv7wWTem|wXGeuo+6UMk@V0h(NMX)tPPzu_a zibrd;TjfL`$y3E+J!mC~+h3wb_l)fs@R%W*UGsev3x_Q7CF*1QjIy9Ug8e~f(6^RJ; z_AJ(W0Rh|444sCX0d6mK#z>sZx<{|B%TIkq&&35)HjEB>CqQ(5z^R=HVFG21&LRkaP&rN&u0x`=J!@O`A(tLsy1nuMycawmm2^kBOqxI!{`2mgiadlIt>=ff32DtEd@n9O%Lkm~ANH z^ew4pi_}=~f&hQ8;(o&*#jSnGuGz3I7RTU6ZV*d zlqRq}8X;SUIrDEzz)BW4o|e0sR6d;NyB;ry-dz}$H|)!AvvU!l#;VA!7gqwf6c*4? z!1f41T64*FO zXUKCyI*>r5XEN{#A~6p7o9P6m;*ZRSnv1OB8f#3MC*9I-ltcD~>GVSM1?8@ZR*Cf7 zK;T7Whjn_C4Q6G5jTzodio6#@`_%0`0hmz*=LrB(wwWI>>l{G(jJTo0hxzGet}5k~ zPTuYV?q?kL#d||#$idZ~6or8=`vJ=Z^F$56T`z;NeKz~dh(6y30xuELYYd#dtJ_Q)8p-grsZ@~!}RjFQ&QJ(4A)(kstf z8fH1zNr-J}y9hkrkrmIho(?~-%0bJ90kKAT?f`QdxA2f zf%z+y3LQfWi#FM+xwjgRatj9Z>X0{7rnjo!BL#`x&!bFrk7x~=%Di?QE8dh=QAC)Q z82ld3AzW;_Xt%VVco=x{3>F3eKWVw90}rc%AH?2Xm9D188zQ!OgPUMI9J3G(bbp7+ zwDWJ=5f+X(c8JR%U`)ymw=3$HVj~L!G^2O$sklvEkz;&By8W0$T!){3z$Oy`+tDEH zWGOFxD3dk=4sEePQ@1sc%HpLI&+;{jlujIRiU_ehLfy*jG(y_MvD~bEcomVgZ{1VN@yn*25FJunI8Kk2c`2)T9g^@258uqA`?i@4v8OT z_4X+SFA1Luk*xz7U_Ud$DDO!k2MR_-VrbhydRJob^ueJdEUbZn^Z*N^WiiEwaXZ<8 zR0V9sN6|-(dtJ%y4paa_dEXuskXR<-h~6hv0z)RmX+t7O;~-mM$?fSa^6N|5#r5j+ z7E~&;RpJI+H@L1g0b=PtJ;6tri^EcObqsX1uH8}_=jzZX{nCWWM_p-@%jx7=M2Qr# zwd4Uw9b1u??s>a4#~9k1pjWgZqXh!J9hGsL6NV^f<6Oeo?>t3PMw6U)U8+laW5YCv z9P^d0Vs&<=cc79D;IeFII@N_m@WN>6QUjz-YGqI!dtm6$z;k?z6+k(NCEI-5@|KlL zm$vHvBe7**Q^(+rjl+F?$$@!?EnT{-r=xdESI41;9VL(G!VDS`tA_hJdGEU@vTe;R zHY{prBzn@FJ6du6RWFJ>?2b!K-b#>J78sHl`n-9v3~L-h64RGnqf)^HwplA##L>i zSwj_#uKaKVHJuNqdXBWFX|ELazyPdx*E|2pd9BjN|Qwl z`8bfEEOoxNm(e`d;3dFT_T0)@Hc3K_3xZH(9u>`GU65>;CReL4SQP}S^=kyMI;DMp zoMg5>d8(@cb^pvlrZ41h9C6(tI3$L_Qxf1ye6VKgiGVP$Of&@DEqXr zeF?m=oTA>>u~n3q3Vh7M`hZQ4gMILscuI+gHA8dm5_KdJJ3ElYhf-A1>Vl~(7?s-J zFZn1o-vURw(@Y}?KBsGIUn1QHE}M}R%wxWsp555pap&>0b2#rLdB_lbu+2IpHOV<$b3Mw<>QBkf?2lpqNi`1Vv^9BE<&D z*6}tj+Xl%TNwf-4Igo`aB>P^vRDkU*6OdfkPokh!E)=>KGe7+f)2_>F3KEJ2M-gw& zLCV_M#;B99WlC+lFy>ykAgoQWk_MjG3Oy8L|`AP4^N+0`K%6N>>dwbz) zLw~jgDxcM+RUw8Nz|Za)aiEuwu^vx@8UySt7WAx53+Y>s2_cOKxwPT#iY6((wFp$y z%K1+0=h9zHG;2MR8MLw@3tPMTg4a%%sPT9c;5{(hr$^{FIJU5HFVilSxeVedWEF}t zmqmJ`e=OVM4cJ#MY?3-;lcWbuPo!nlW?MPZE=8~JBD3=lIIE6E8A?JEbA_eC5TAz% zgIdvnde6$7Z!K(@iXyQFED>veV!HzNg|ar|(wQ1&Xc|CvQSLjj9d!=|htmC0e{{*i zC-ZW2X{iOpNIEbJ>o7MV$VCZlcR0mTE8{tc9C$9-P( z-Ru;FiUHq|IA+dTZ+To>`%v;U*N+6Ra9r3f#@yO325aUBP7bVr7_pA<6D}X<{4%X0 zFIA#0wyPsGv?@KoK9^1D7E~SLkb&W9Atp*o%-n^tw?EVa%Sy$%W2=Q$TzMCL4v6V= ziu^P!P-Q;mf_A2`3^vNNh~L;gqW>Y`1rfg*S_XWrdTV|_=Vv9L77m^4@C?!U(GwID zD^&6}Ir0$(5_XIFa(ZZ6a=`1X_ppE01@<FC_SA-qIKY*}|l&tNi}{{&H^9Jh~(2o()(;{M^m zZBjI8v%^^OQ1JQ~O17ec$RSY5Zb68#E7hHnVS&KJLanAIBwumxvTY|kt?cYzYLz~L z{l@eQGTAH@q1M&5EZ2l~#2K$!2LgnM#LDDU0JRv+B6B4fL2e$~Ku1kf-3?)KM_Bb_ zP`iT4W!E9OTsaty6?v8?Em`Q=@zx-z&$3Y})c%omK3`2pnS)&LviV2Aa8#x~=nGuQ zb@;e+U#t)a@nmH75)@*BsnrP4OBz^{VN*I5Zc?f@gMq@}SY5^l0V!hC((Wv>3pZIp z9S+7y=v(Akwe4_Dn9tu1H#UAtYaM`QcO=|BV3BZ_s6byhcD&{E#xpQLF3#%=YotA~ z1r-S)VF!^%qm@j~N%!LoQL(5&u-QZx%$9lo2Ru!DX07%h0L2@N&ZvQO zgP9BYlO-ohGV6S3*0Pj}bMI;=4%A7G80`rrh)K4hm~z3gxo}oei(Ex1o*CYQx1oMGk6mj)-6s!3kF%9|;zLT?E-Sucm*RA(<37XpiX?Mt=QGZ3lRe3FMJ zR%Pp9`azp4yVud{_Lfmy9i3RXkV!M>67rj$dhk##&7=xQ<3O&v3@!=Y+5>+KC$fWm z@tS1uC*|WM>rM`JZsSceipjWnI z?*%&tcM)<>E=d-Yl!43tMk1z&Yx$f`TjI#rPY)#HTQCccm)Bs@kqTjoa93)oM&Cue zZQfz-Ce)2R+6doA97fM>qza2G&VFD3B-ECST>~BcVB|UY-ll}01Vs!%CKqN^VjlU# zuz0Ns(lFIlW0kLn40uGrvDJkE#XPmxtidkhv0>xeo5Kx!-Y%4F^$~3o>ojz&Jr*>N zCC1+%Uk^al1uD~}8$Ae*$*x34bDF4suuAjPXW5Rs%IB zqYz=)+5~+6CKEIC`$0X)Mc{aRv2x|f?Okl``r9RJ{4# zpgrwYXgkX)yBiEyOY$X3WyeWR!VB*{si>nbb@x(e9&ZP>pGnR$}G{`>TgH~};rRtz<74N3mO{m`r#qhmF&idB?ml{4v3 zVdUD zmc3C_GB#sw7vOgj+gzk%3C2&RC) zgm?iXdj#>ANfztAvA6~M3*4AiVoO%#Dr-y$x-cs~5yg|Ub)@#CYb$%7_gWMgc}8?) zH6ncQXIY^pzZIximr?J42>&yCMI?{0*6*##rv&y%Uc0Ood+$vkT+%<|rkxa@WvTyb z3X%58&)$+w_n5tPZ%KYkF_f;bMDk&#c)2lHY;6mX*w_gRb!u~}D{oNf*`N$pDrY%O9M+y?FegzZ?+0d?Q?*k=@<+VaY z3&*rvw*AiZIHurddIc$EFs+T|Ei!RA+z>Edp**@9wwu|#tQAu~FbmwjW(hBZZVVp~ zxp5x9uJ?FzB+P+n4JEh)46g($?|$_j_~8qr#Nh}L5fhJ_EDOWSCk!XWf1wTv$zm+8 ze)dra85_{$+@dJTh6y}jx!5H0UV=cI{H3ju!mvf8c<6#HoZ_Lo@k`pbpMmYeURj@$ z1ChGIIM_ZWQ7Sa$;$vzd4eDARlR{C5M0V8oh4K#7q(i($Qu}sS%mo#SD4$eC@CX~_m|qG(qL-~e^u+- zv$vk*3O3ifdCgj>4uOBZ#mut9u))Ak?!ZxcQ4s5MMMK<#5wDLi-(iX!?H2Lae!8M_ z=Np7n^=*dt=FW$Q!7&h7rZuop(+v`+=!u$mPzbou8aLdyM1)~^Kd0v4USTN!;1=n@ zZY(M2_Pd@c6c*cH$(W@oN35pxKV|5tn)twpfJ3K$ttqP!5^Pv$@2V|4EsJ$M6M4Mdxw;+$zt#_qn=Ck1nG&@|EYVp!pD>{t8MQ1d-*j#XCjrN#&GOn|k5#Bs; zU*SQ&tpwiWH>iY9L(dxb!yV;D{1y+CdgR(LfxA|EmTCRhoKlc?0X7!8WjMAtpfZc2 zybM5wpomhGy1tCF*&P~*%x zxi45~9dE1Z%YNiA)z|eC|Ic8fs{%?Y5K5b|@Fk}#%)V!?kIJMnydk!P#Cvsuv6T1! zQ;L+J0*simhm9MHe+Gr){#Z2j9jh=sFGhasWU)GXT$Zf;RP#((%RW?iC0L8GXb?M%cS$O3d3jwc(Ai+x7_9HbzBwTei2=k^oQKW7l)wi{^6M9Chb6dv%9}1Bg8pJ!|MOpq4 zM>}R=e(WE|s5H-qtEgARdP!i(1mdy5>A9F7B{JOeZiok%i&L3WMoq zV&p(3y~C;yBQx+dge*lh!_^xBaTC8}?;BD8)~9!&e(iE*$cb&KF4!P@bc7Wwg|Vz) z$cdwB^(jE$B%RoMgv^L$L_%=@OnN}na?y2>6GJ$3VSQ8LST1lX^pn+y3-iGPN)Zn@ zXaF0foOsqf78Aww*QU3kOuJNc!ZyVFF-WJ&<~F*5%fDE`P@I8>!zj0JT(cgkkgoP$ z+txAA(aUz-peppAz24$V&K?P?w|FeheQHVf?M&`wTvP7ReMbq6fL<{A&nWN$JJ#Gb zknT>QLS=j>O6RvD(xIxLgAk$kAdy?GA$TF8xIP9Ly3sO>7?gxiBMMm` za709v%`hbR%`%E2tAJafWZo!vs8%B09`ecPm*65|$}i!|HW0^=>BCK2&b@T0TCGF7 z(!)49K-#V-9W{)wUwvHy=by?q^>xTCeNu}0zf0|NLk~+S6u?N*ve>d9R;eN9Eea+1 z*>(k|Joq^vALil%nwrsm0heURAqoK}nM2_f<%t zn}_8m9|7a8i+8D&)~KByVY$D7ajW7?9&b_@e(FP$w6h^oM|jRZ zXi{u|HM+W!)LD`66Kg~vIusj9;z5zM5Jv_D|IK>ZRR-dytLq;~^U+vsNt{U2w{;zU z;_w?0$uUOU{^SwbQY)CH0wB_<#40}y3h4w)E|-(V@0`Y{5xP9RoN+U3kEHl6^RUf?+K&Q|pHwg_TiGiKG@NRwx?Ty$*#y zm^BM0JilJJkoL+DRs=dX$kZBheiA&}A(&Wr_7fkopr+rG-UOKlPg1V5 z{=+`l=(Eh2hFK03_eG9euIV%`q~HWC9Rnw%J;uB;RyLWI3nJi9{!$ETZPxYbM7W=G>Az*;heie<~DOHw(!&4Pi)9 z49H3fu5gET5XiQGqm_$Hgl?U|^LaW7jStx8A^^hxx6nsGW-odIE_+!Yl@I= z9M&1eu?k#(Mh4cv3egr4CfT3M3Y=gw3$GqiUkCdDMF}9c)_@sn^pYN>zQwo zmGR;mAtZIz`H_&7Y@!p(3s#WofzMw2W)aIX=hJ6o_(1+lv#AE(y>Ro#YF!P=Bsvx0 zZxhjR90@CiAgRk8jf=k?_p88LKV^DwCSMgy^M(*DXmRV{Mx5g%Ql`R0ybj0&h5uru z8eleiEa$x(V1U;lXF8+{rSf?^uJxfhkvwZxR<8?(S5phy*{Ya95lh5a#u=QvI*c0y zs=2JY$bD4#)ocq9+xwHbN*U@AG19kho7@sG`H5LC(F%8SIBJNZ22u=qJ|FXHSQ-(9 zO|KC;h*C@7FdZ1;2vj`4r4*j(*hNvx>{iRjBC&b5A@7LEpw7Jo%u8WUxHDNgVPyza zC>du3fMt*TTTS6$I zeYDG1VNtOpk`zwnS5KQ48Zc#DqFxT+N6`Sp9hWt|&L197&F2~!o zhyv>B7*20VCa|N7sZgOq4&_3jHlT1{C>vVo@~r4@9PaDWyKf3ziN;R2w+Ey=Q_u{F z9D%{*%E2jItSYVj*KYtjz^IU(}+Z@l~STQHpNxdW_?@aIs5t|4%AvI?U^Zx^J zLF78QwNQ!^#h1hmP2+?V*=Tl(vKgEML3U5%gjo%76DI+sh<}HI%B_{(4 z0>nW!?53pBBFIK;#=E zfs$hri;`!PxsBB|aj!&cBvP;v4a-k# zZRgB3Jq@ehSY6vd3P-Er$V|4T&49p4mF(7j8ukMuyWIF@UG0{kMltMBdqyep_9vpXSx4j8Ok6CshUC$XtxSreHEJGEq7-)!hZB1fZ z1{1StTd_hc%jtcckzJ`SkukPuXKkXdt|4Kkel3w%5^1@9_&3?nn|U9LdzxF54$*Pc zzO!R+XlG>iAhv*rY?TayU$SKg%ZC_tz1eipp6(fp%bXhi#qq!OqD|H{<~G4YM4QBk z!Zlk?Pv~JPiX(|bgC{vS)Y6mc??*;rTTcfn{WHaEP7VyEkgMntqeN0WAyhB!=5)_+ zZ?euuX{~*O*!R*tJkV_`@e;}F@Cg^gg^MthM#M4-RjE+cXkwaclz3R9#fOhf8hm#k(lq)zh;s-Gx?O zuGq$o)0ymr++$dE?&$7F^;n(dl42Tn6oqX{^(F_v0hQ-G<^muKJ~a%2zuD7PgeIj) z8}@z;4Kk;-0v|kRAFT7-#D_>qe-F?sz8)s+*1ln}1rXTKmmcUvo{G++7U6n!kfFXs zTPNz>SukmB;oJuuEi9aqY5E49_VV@xR}!pD@9G=ufR+w)aX%L1^hqz^FXYYejv$*bU4zB|0?<#wj!4Gm5%6w(1oJZ!jWQG&;jVQCljF zU6pL~dXqWik;X=as$1M|sZA>JT*p~CIS@}~5K`GCtQTpu;6@o7jMN)57Aw6NfD{z6 zGa|E1g#8Yx$JzG1+$c@b1|8vH`=5_PaZey=SV z+?L)2_uJ`;7~9h_{QyTb4zaeju|2J-yrebqOO$Go6tQGE!kLm<+dPa)h^J4Frm zFoiqA!zryYdbIOg4>xP>%rmI0OZw6*4&_Zk; z$vCe#@_$p}pnKUzP*yi{Ih9RFy_8JZYb+E;b;6UXIbAPHL~9M?IE;ocl!GW6*cO1P z8j5@ zyGkyrx0@2EbQ|YmWZ>h0)`K0WJ*{9gan=Eaq2>c!fs&rfUU{;QIk9bz#B^)Q#wKJ_ zMPR?M2DOF5$aW$>E`ct1wW+9O(^s!eU#*u?A=KewWEIMn1eF<%zvqC=Rq7`+WSvPVtPY^<$&tb2P^<4dVo0by z)yaiG5Z`OYCK_0#$-a1JWMi^B)t6kpJ2|MVa*t^?sEszcmnpJGC@JpRWx`m+8r}l4 zR~53e*1cIvEV&LwB1M=ZWADU}zs41^XG!}NlKkwGu`k4Y%PhVzEu zA?+7pN5$?zfX14%Tt`#%frB*C_LfyZ!X+C7k79^sv1C75^RyL9IE;UNt@e%*K4u*r~j0wV0&k$KKGVJZk{oNvy5;JiOs(6kB5JmhqlsFZRT; ziW!SR$JELIl2E*OT@?64I8i!Nq4f!8cs33yv^otHJ$L1wXLq6TNZS0sCXrBjZ(goS zqUnsbW|+;`Uo|MJs|39PF5JBRi5P+9G_ZV@dhx)Dkc)~A04~sg(4|rK$kME_p`2}F z%!XBXBg;)ygoqo>;Bg>IDz!BM7BGwfcpWf?&W@cZp5%^H4RZ>b8}=#+ahW5iT;j>%ifSBoU2tk4P3LNaIR~hd8x+xHkcq+> z?a2|5&(}3%@@<2eFt&Tm!!D`fX3*h%Y7Le+#RZ=>s<&iSftX*@_6{81)F4o#<+rB? zPa7Co)|2krI(ry?a}LMMhRg0K#uc`Y#r{{zS=}h~RK?z)M4+HRPv}Z#Gx{Qoq7u<^ z;fqYh2{3elq=uvztA@74MVGes*~OY%>b%H0G+t(#(k1u?Ib0$+F(7jQJyJKU@I zn4F2>$N7I1bDV%VrCKswA%Eg0tmpE+p_OFM?=n4gJN zL~4Ft0LC&$BjbRf=j5Rm`$UErV~oR5;9x6O2%Sy^e$|RdnJRq<36A)Iok2peldj-p zW%VqhTwM$aCL@wsq$};i32g3Pm|iSw)148E2}b;G8hAaB`ha3oEEoMO zQF12fKo96>iV3^atKvPzT!o*rn;8c$lgJ=UbsHb6%*TP1YCQ1c%U!N(FbzaQl|)pki)9M=%ovqcrZPoz_%cXD zDk|qUjnh6&N;9+ANooMu$ci0YUN*ol`4a+!WRgY3aso z{Y|=mHBez{3v(il{Gq{Q7dHUCO*q0`#hh}=Z}ywHN@`2RT@RoRCYhWazwW`G4W~(0 zp*8w){!$1p151K|j(kx^f85qwWA#eul!jL*M>8sho71XT8IZ`MtgiX5h?NHR3d)gL zw4XoRcyQI^NQlw5nyNU}M6@ z2YqQko@5F!2B*zS^h{lRyh77JGZ?cSu{(~ zk#_D5$}Aao3F~7yl0W>^BD}*2J3N#F<6jyB`H_soO)|3FsP1z7O?H;BgTL-1)raU?9!G&=x{kU$&a*4DyX9=|^$we>1n<-f5y4I*|S3iF(E`(+d`Lp;yIo;=l(#x2RMDb6VqQ;Eph0M=dsUIve@ zg2D2x6tQYKq>m69;IVe7hF_HP)4LbiRSqXM`}87$7-UrLD=MC-@>oF4msw~Zj7y9y2i0-^@)9mE32pi zc(vI5=0ZRYTdS>#91YKvF~n4#H-=ag{&^fK3PGgVh0>|+zEWOUA4QFEn#ih?U05Vt~*a?YBP z^H6&kBCa&*P4OP1w&KK3eC4LzRA*1uP-NOcbs-@EMq5V;Y9YnYGwsh&}0wH3?bI z2YoC__I2QBqC~2@^~6D_bYM>%eO(D0AId+uv@+s!TwViJVMQShFpV8aEIKx_4b{ca z+6(8pThBd(&RTKW1-s3f^<9|=&fgKY;_=3vVwd~QcmrpBBNmO#`^P0bq#=y245=hW zk<%;WV+;NyQEm5OdGnP!O;SA+VQu_mgH;k>WRKJYc;O{56?Z_|t~}4g;$YHkAIN~s z;mr>sQmhV`WZZrdo9vH!i^|$}j7?@A_@T}#u%BB3gjRd|#8Iia+=@jatL0ZzVADl} zT(&B`J<`}kww=!lq6u2@du$`(5-)12F8@%}Esxw1^$}gVlPt0*GUH7+VPq?J4F!+J zfyi%3w67AI9K)TT0(rH1lyQ%PK{KtN=zU}?p+ZoOy$yCu*Tqu}IZZTj0d%)PEl#VL z-sePyk2ht|n;)xa(dPuLhaj|;*vdwTH7OXl_|2K4wpAjStSdG+2}Z@?qM0bqEE zt;`FHF_SMgL(IzKyz=aiRWB$U*Q#g;1{RpRt-L0opwSU>kIJXG>yQO zlV1W^qewZE92_s1a42F-w(rDd@zRLYK_kh>aX*-@ZTT$p@H0}Yhy?7BAwGA7H6dx+ zTLNC%!&9$oL|bXjKWia{VL% zhYI>GQLlh^1Bs}L@scp0ih{YVx1kR|ngnC7ew)r(A}=#ixfHBQBR##f*HiBSTjP9+ zGy;ySGb4v8iL-)PVTSD_)!r_YH8tnw1-};%O&*u8UPb>?8YRMY4NbiHvMKSrq$JOp z$OTm-4D8Rr?oY9y(4BG2mXEQ;0k2>jWky8FU-uO`0F6t^U72k=ltF(z zJ^;$&S=RtHJiz#pf`p{dNzEUl5GK7SbzwMBTpQ?V<$~9n@kLn3^`E$N(j)LMsm+75 zo`@Q2Od9EW6jj0wS>NosU=6B$Hy-1}f>-WCz0{j|I7>8EX|O1Q;dx|yJA89hz-{Ah z^Y>zzs#3;v6HJz!OXoHu3mt*VuP_i*>94W+!Ole2aBuJFtthCoTLh657048YSi?$j zy04~_F!_=dB7wAcP0H#s`9L+$hsE7xHyOdG78jb;C(&At{URP0V3&Y{<0=8PCK`tKD5a;a-qQU>P6jO_VD`MLL zcC_0%i?vfmj^4#Njo3Lr=bMR82EEAO66ijlrbqcKY)D~Cstf()tUFd1)++g^xQC*i zCJ;}uf6$_uU_}oX0h?Ch7(eeOXFb>$kU=AcUhWgDA}IR+UYTXqXrXaNTJbHomrygl z!B)y70d6dtW z^s8nsvHv4%h%t+D*+oWVvZoIG(~%`F*~RMCb?o3sMUpomNMA%Ii`g0+2p+mSIK2H4 zLAy%AXCvM)doSZG+>ho-L-vTg+<6};Kd9h^P?auT#o(YBV7@L!g^BizQ?tS3)nyd#SgdF+yU6iD zNcoH)cidsQZT2@sDK?%tucZQ}UjE)5KL%%YkQL`oyA-((9gAV>QUvd| zx|Fu!VgaTTP@8nS5D0o#rjqnoD5GF0;F-8jO;m;}0%tNv%+t?{>RmJt`_?Tk8I%*A z0Z2yX%1s}+^lNfy;Tp$K6$fQibgPt##x(c*%r*p=bp#YSi6oh7 z$XWT;*`ccu*TxmwZuAm*#^JgX=fS!rmKI_~Sv~NZgn)DF@iA&DlOP(lPa1=I?g|LCZSlm%dT$E+SGHE3q z(17ASgV9}ib+zCSeVv4S*CQo7-*Bf_;8$`zt_Sa*lmfdq-A3yADYG{q6S&;b=2n{A z0eFY%_Th!L8H1|BlW7cYzhT@!-}Uw#Tr+nRem-x;;Yo$c-a~N7^*Hh;H+w|62+oIb zLhbYM@9YE4?+xE)gfi#}^;qIG3n3Quv9QGr&dE2zTyWE?;$L$Y_QCZ+XkQgHN3v=X zTbn{R7HBq%r#Tlh)rkr|GEN=-I8NxpdE#MP;Js4Kgau6oji#nNzqA<160qWUkB~Oz z<{pW4L$S2=A~8-BPhHhJ)ZNpubx>AI%d=}Q4}p6VHLh_?mODU`JHDhJg=bTPT>e^a zu#l6K2`n6f(nf@?pqUTIeJLqcmtq8a^mrIa8PalDvMr*g5Q{Y`*@Kx4S5xkG>lYZ< z=`#`Y!hi;`D4CETzwzn?$nR-ijWxB^&|8DStUt1BTzK*Ez@}h8UVt>kM{F%mYfc8I zO-6|J(BwCfB1#x}N&}h&vqF;s71_xr9kC)iGr!SP++N9^%3&eil$&-sw_~ zw9(s#lBb1n4Vldj<&MMKBp zPbOP#WvXKN|9DYwli7_}fDNlwsqf|kIQd~D%cVQ(>J#0AogIC0TW(&w z5Zq#`u0NPJ1I2mD@*Hk0YWDDWeU6PF5^CRq-f68QXZ|qG!c6r@4mdLFj_HiM!txov zW?yoN4LJ)PREb~A1q^p%sKnJnJcH%34mf9|*r0|YKO5k#E{!XTgUKZig!NF6M@o1Z zXMVkT_9VK%6#RCH;VL5Hl!>W@96%*mp$MVp$#o2B`o^%B#{j}Ab_7PVG3wQk#ba82 zAobOZ&+jGa2yh_hA{NH<%5ic)f&>n6kCK<&g|NB zN0;QB?L&Py3I#*#w@*rke0YLpK>4A9wvB^;5Ot$) zcB_!MbqQ;ailEoU3Y10Cir4^`-w?x38q@<{6RX>}{G^uU?VA$o;_b)9>eroa)jU|N zwhmzuUA)h&3){NQUyfGU4;%lUw-v?ZJ5U&%c}2nBY7;Swtgqq3>+U#bVQ)&{pY4%7 z*6>kt)*c{_A!}dS2#}QQN&VKASl0J37U<#bSn$2Du2lFadshwKY2_Tc#}(!odWNiZ zk!9^tgy$6vh+IST&o=AxUhU2MJvI8Q@7FDX7S+O9(V#Rf-jN#Gmg>_7X)um(k_r1tOTV>v~3zhEoU=vS}kZADOjL$=CE=bS3IW-wK@$|U{BJ34xHaOIF)9RppF zb|k)b4#{&&RM}uTVcn6CZ+vbZOAO6;#c^24E*Sz4Y101rc(y3WNkFZQ#Wrs^F}^mj zdHLEC>@+3=20@TI>WgYZovHqg9xhyFp>&-+fp||(+ctTAf*f}S;A>3rt)_1F58te^ zccy}`>r!Cgn|7L}fiVss$%;7f-RuTR4+ukAm2yypKV+7>tcL?|S##07t3ZiR5ae9E z`(3pJ@GYW;j~Hmz>HTo1o8^MqM&;(A0bm7~ugt5lx6KIYQJ>hj-2(-Moeir7lB$fO zMq5{3w~0avX5YH#f?nG8lehKuAJ1$oAlV(~VFlQad*Nmvq?`TtVVxEr)=YK9!A-ML z77}C_qc-adoyKCU(xm(3`Dz=N*C+6IO_%!7<=OJ!TXz;eda+z$$^GHL%v!N=Y+w*f z!Gr8PGpjQ*NmbztHKQzDNUalySpX+fbzilp`5=aOvVu$Ov5URAGmgcWJ*-L(FnhDO zgMXgEbAv@>y3X#ED-UUw=VkZq_nI|tL~VoZUD!X4z0JFmJ*aBnK4SsS#yL+iww>Yb zqMvfNWQCNrK;%PSNNJIuwF5iUYXMKs+{>?cR(@E<8hv6!atkjkBci0XD{-xk6u#su zGyI=-y^=O9y5RSMM1ad8Gxxa+%OtRE(YADNa#4Tp@Ie2z+WJNP1L^G`k-m` zH$iuaEb8qWTGYMh*pA-h%H+;P$hX=$(9zqAl34CT)-SQOc+v8<*26pd`-^(HsFxNU zzG!fuGqPxSa9|O5e$Q}Ma#82}`HN!pi?*b2NMy*3$ly>wU9a;Sh_PXgZ+ z11Wkr1@~RJy#1v0EfUK_L|U#K4-<8GU5b)EL3L9zI!%KDbP1k16C8lyzPE3q6XTL6 z56W|Exc{BoIy#lvWv-4xH4A+qbt!!%N#tdJp*^rj3$; zjMVCw9ytUp4#btcxm9;6Q%Z4_O80j3;n;XxCQU!UR*w6j!x^rkvvqj7JKfWz?)!0F z!NBqdp%ZuHVtM3XMVD$GsyGuKRS@m$L2a-k&(zFJJkP~4&{*`+i7BYLvtSRxhgW0Y zy7xe+SnWZ`I0+uK(N*~_=@Gmw4=F^6l)BM~Q35#c}1 zl_y2T4~mr2gvm5*79BdD4zG(i*IGBmjHk)d>EJ_X{-TKUfA7+7G|-wiO3>9Qx`C+b z3uu^V8V;wnP4uo)^KClCyjV$74x~q;PemincGFlv6XsEU6MZClO_V+!{dAOWiGJZN zdg4m+6^BO7r}O~rI)l!Meh}U7`>nClX}0e3Leb|v=5r-fHX`C&XRga=*ZU&QSIxCU zhb=Dr`cdosx@8e(_}zMq)S^9P8q}gaYZ}a=<<~SLp}V#gZJue&v}kX|6PwIe0*kiq zw}Oi{WIiVs?NoCu7w!G#S}t0l*XLUA^P=6^p&?6DF@olRDt~2~DrxLu`bYHQ5_+cO zXCS&6TlB*tY4kXHI{H$SejEJ*@VA&dwWha&>51qMqVz&^FS;tWxeV{Wg!h|Fx1(s> zO!`Lj3GVr~xYKIxNZ4*N*Om00=nHtFxRz6_e!ds|Df;=71;S59znf>vA*X8a&ZmwG z@zQCXy1^{YO{U4eI6pCW{EJh}oxP?v&*Bu@tQY5X)2)ATD!cSt<>HJt*Lrb^Ynfs7 zBNyj(Gloi%i<3{wH>|}u>GX&*{zN_AQ8fA^QTk}~<5)4fb<#|l(@aOV(5m%x(rH=l zbx<#E*O?B>srHD7bCJ0oK@(<2%KMeDd}>trF$%q^%G=bhDdqj<*X8Qh7fZ|A)Ku|v zwCe77$YLhp*wy-~h%w|Tvis#nv-%@OB)-MYuuqI6El`6cvV$=6Eg+a>3eMx2kDo6G1J zCM3I|#oOS}_+`}6Mw`x{3!<0c*^ir-N70%MwCkKGogaN1Hx_KupDZORr zifZQ2JI}^oeQl?Pv~_mV=c0E+>7FRx{FmvZl*(G6m_(e9qUt zj@_`!+i)=L`eqbt_Bjms(e3&@koQKgNpn4)UMl?~Kox8`W`};KjHa^|<7t>p2XouI z%tK%@i)iUl5$9slG=fI0jyUUjbU%xz{vRcDap`Bu=&xmum($NG?yihDo6Y?SntC8D z`b`Pl>iol@-IQL{>pD83GkATKxj&KaEBiVI@_Exjbav@^rSvqtNc3y^ zJ!QBTaZ1wqqx0#N=xZ2O+_a3K|11467U{dDWg5L0{S}`1QonwyoJNh0I4{`Ov!Rxm zfmG0xM@s05(mPA(+w=pXv&t?kqwC9V!Du}XokGd4M(K+sUjY&Mo_o%8wvsm7gs<=E z(_hExchL8vFQf51(|8g+E(GHrOiL-1WBpDvgT!k`Mx1Hy(cO%u4ej*F==D*0Ao@@g za`h?3U>2_VJTu?)GnS?s0(z|`FQzK5j_0-IRBF$%w1w6k@4by1N4G?rPV0@TQFPED5Ix?e83#n1ZC2CxnRLhkYCPK8I%Q_WIm3GD zfVmOpGV6NWpx21)ETja4-JP1g%ejTb2K_oX)M_`+Lp%4o$p2O5Z4d8e=KjqelnvaYK~uEdPqXwT0G$OShT_ z!IxG=oLkKGk+kal5$9fW9iyd#!RwB*^oQos+0l!Ef|r?=8akyr;(Xd%SJD{B?&V6oEl-`W~8&7@3G#x^9OX#>ihqSGuHh=SRXuBXvS7AJ$ zH#3218q*eWsy?8<+(6e?eiqSALt_u2PgUN8 zA?!=L?BOWWvqPiC(CmZh*)BiiT(-tjyOLxxBpN=zb-u!s@Sv(^c!cD&@H8pK%Tfwr8H_B&B2Y^ zpfd$-EQr&(P1Jc7q>b+tb*GzB;Wwf5r$EFvMn8u|zsHOKius0!^A80qW{Q)hF^auf z^&fD(6ED&ub*N%C9eJE!wx1*V5`B&633>*z@jdg0=s)m*tIUfdXxtP!U_Ldpz;L3j z6rH{&=k15g+m$pK-ql*5Ww2?l)l@T+4q8jcpAd1DLEy;cfbd%ay}(>gCl>Gi>zmAd zVCHo2PIE1llQI}(y1&W^G-XD_8TCQ8Y3yV=@XsZ5p>t;ieY*Pd)%1bUAA=w`^HTlD zeNlSIdAx!?RCz}g-CuQOHGQu7!D>wC3jIU{jhsL;K}R7?-!xAVjh#rd4vIJ-O}9W| z$uSa5oe4KJy)o(o7)A6Wy8URnE&5=TUV=}FUXOmzp+7m7VGy^Q2S(AlcR)77FuW~F z-;6#Pr5_4wAomkLGkrjeOrwToI(7|h+CqK1=!{FEbU6>gu^*C8oa}p|?4x?DWi$@_ zY7rgTN?Ui(E(n(Eqqm@s1?GuLnsg9tKAFyd0B}BInjNa1NC(x^kt<-KnAU2VaR@EI zV!{#drfIFGV~(fI@1!kTBhKMh>rYqH%!BCAMbxl_j%tA^1SuooI*z6uBHaHWO(&YC z%4s|^Cu(YCk$7?<;+$pfRkNU+hhe}?a_z@-znH5KOYFl8Z@tAl3w;IDI+5nX2ncU| z!g_Y+f0D)8IY#U$4K`9oJN71pe#0t^4EVz%k;xKJ4TA{VJLVaQm;1Huo`? z(KPq)h!fJZ!FpV;-rb6HcOz6!q?; zJy1EEo6Vgn8V#{|m$@#Z>Tz_?q0|I^_)XJFG#U2n3+B3v4p~e~fnal>H%i>7Ku0v= zfY7E@<|#qHAkKqiv}l950iV(snlP1S9vE?+zfpHuMKfw?*?Rh7^lQ+(O71M7@0Gk< zLVqfGvn1ji0xMX4yP5`}4qY6*B1%7x{w7LSmV6rbKW5z@JAwLl!e@9JCf{J%A!Xnv zTV}3d#xI~tpufQ<2r2P&^pAM_zj}L>`L;s?XGiHG5&zlu)@@e5z2L%5*Jm}bs%X|R zv~~+UUGmcsdbsolrSuaxq3F%h^C8AOk1dQikD1rtw4Ol!j9ywoSC?E{LVv~*&?TkU zKsY~qv+jB?+3SIb0Smwcdb6B&8H8PTvkFKDfv_h-3-4+#QBlAJ&I;+qPt4I zz3+_`W{5Dg4vaXtu0II%K%y2%bOa4U(RXe(ZP05WLfvb)&Tu^z+*!WUOqY~gjX|$A zEs$&nt{Zx2Mu!=O6{xW+M!9%1IBzx z-=9Rkj$TwkmzI3Egg#pGaZK-UbFY-9&7gw-gP3VTcUAOi^l#BfdF^$wkBiI8_pXvZ zA?A-$t*mT8(*i$vif(r3p|Y3Df;Yh;ufAT7gap-o++2?!cyFe`hml{?-R4D%aU4A( z0!;hZ_?YRl5*ouVfGy{k=|LZ!Pk*529Qv{I3#h`LJHW-g=3U_0C>lG7W);}_n$^Py z*rl^Sqd_IHJ=gVZW~@Xbr_vXkJ01Fx^RPpYJ1=0subA6q1UJ(lv}EUxrVYl;6q+%Y z79InKE|%XxQ`&|F-r7Rmba;2CzRizeoZNVO8q*wwER@XB`>D( ze#MyXFO4ia+=>r9eXCxG+4SS+C7{^XOv?niGWq~O{?ZrpGiSn*sd%cAuCBVgny#<@ z2K3^CAuT0Gn+;a~09gG>(=w4BDEU3QJl(Y1icm`V7c1yL71veLwIgqX$KwHWa~3^N z@e?@Zeq>rE(v9WMlt-M#yYxpOeW6Xwf#xDVT22$E(joASPPtn*{jh|7BV>P({AD!zl6++%LerW@!{$j+xt%S8Hd>A7f`bEp1@oZIaG)7K?b zeGIkjh&Z>JruDQNHsL+y8gVGto?o=C%SXVFwyyd9l6!TRYiI)uzvbpyzQ5AC=J$74 z*Zlsctm~6{pyQeA8lf3p@kQPLDEMw8&d<#?xF;;qTKM?oa#hiYnb7W`Oq!;jmeQqw z89hn=0W{op#5kU$UlIM$*FJ*AOp7=Vn)l1tk3a38i1U4`3Cw;9wEACKO(Q1?{{N5qU0)nC>x!Da`qI?X)sK$N~)@*?oU zZIS~%!#rXJe1+Ad27H6nG;)j>@c)@6hsGX6bK%gr-)gF;rubUga8ks1&a`4xAw0|O z*Mlsj`=j3gvQIZHW9ZK4J!n~ITBgxwqVNmOtKj zC(A%enaAp&q5Z3bo+!NrYnbpo_R`ng*CyW*rO$J``0gm&?O%x^0Qz|Jdr|s9^iTM1 z&13pW2)H>kzaHdankuMfJnN1J;s#{>N^=9#9m%esi8JVcLupZrmK;MZtLWrYXnSwO zd585Jyp0egGa-m-so@A(1~2~c^sX&%vzv#6>u&Oa5vLarlSo_2Rtt2GBUjU=1Z~}p zn1;Cr4;nm(Uo_WMG+_qK1~$H6n&u(f?6D~QD*7MH@IR*6p$~EX+4n^7tn6|9vGLFX zF>zXa1Rb}A)^Da$FudjFb{PaeEk$5{3w5VpzL*EC-g>~a3Tt~TbWPCp zLw4&3gqvykO4XF>UEH#bY^kS5sQ<{D(YfR1RVYgsw@p7XLpzjOWu4doj1LD=l^@o=BHSwVM=e0(ImF!C=z zgLnT>f8g~>y1we=@pQ$6uS}yyru_hN?(!e&n+PC)4rbBeN76AM+S|3P6{^UVFR^hV7`Mu8Q*q~8GRJ`?=(^FP%MKndvjPnzpWWOJMibo`-d0pl4R zaW43ge&c9bvz|6`FSDN4Z4T97U=8LP#tz=;e^K9e-x>W2-G-qcW9(}4PPvT!(2Rqq zzLA!#h&VTzdk#G7>^5Bn8vT`Ng~c%eelY-zmVjeIo}Wsch(e&-#uxNw;6VU}&M?;{ zOxQ@cK5PLks-qR#Xa`io`^;@5x`$FYT|SKIe#NwagSFE4p|H^ROMeCl zKlCg8Y&nesAiiL(9cqEM?;&#y1AB6$eDAO2^Y=_G-+Zk62~;@s72Qh}RgVRaHP;nX zJ)0IZ+fBrnx1bhQf_VxKz)7O%Y^wyVy`lfz$FPxy~es` zd+;y6*Tb7ehc?*PSnV-%D7eGx<`KdSMAJX4CX9ujs{VuSsG6og=DXL9h<=PU_vJt) zTGu>~Ml-Mqnmm(UtN3RHT~hfWSRQffPR%rV=j_UhapyjB2V{s$4S?hk(^N_0rX%x( zmLEr(PeGolxluvWfjg1k=<&jxQcIWDe53}-xoJd5aw_euzNm(-t+}a&?x?w|h90T; zA)Zgm zmam%@$YV_UeXr_wK378DD0vQ#+;5tu)3Z?w!FkrSRMGYiq9JZ!9asYF<(sN*siHfo z?yI5)svfFB8jE?Nj3&Z0{yJ1uJajAHf31psQuFH?x@6SVqv-lk_kao8kMmQIY^DRm z67dI1|h@(P-po9RUA?)wJPM9-DnTuS%R z1rGG@o6G2xikmCxo0S(}+1;m_=xD5_-DyH6X#2rgx48!LN0z^O{4qmVKv;E~&T`@2z=L z&tV}A1L>bB`77wP#!S72UXDHqxl7kO$Y*=Fj9xCktAhSf@u5okP}N;kbk~Tzn8aT5 zd=Kp{c?c4mULrcP?5Q%ky8MoEx})MLm@}1^Vl~`HYw7Az{iu3iFQ7Wo%p^uv(mjL( zyQiTb%{DWsrH4vhDS;d8IxKV9ndS6Q=g2oA01s zj=6p;-92vaIC^Bt3sdZS6F6Xf?$`^*(v#xO2j}W8L2Gppr|l4Z4Q&EeTXRHTA3+^R z{d%hOG31FXd3lNV*5%PVqqz4xu&0KC$>Q zDBfGdkwm{# zLig9a06zP?Y2QR!zKt~C>fgh@aa+Y#?`40|55=ALPSyPYAcp}NChF@WsPl_aq*Gs8 zN{>PVr8`IbW(0k851rm1#dDhId)z)u5O+#^`tP;)BtzgJhW2e?8&v(2qLD>YHn6ak7x83k@fTw^T?63 zVkPHFZRv_QUv~7E5V<*yHob%1h1SwC-3m7!T!3}7cqtMtp+B1&u;EA0 zt<^LKYCz=(eSInoAj<=pjcgFoXQOw6o2Ja&vBKd0j=AoF_IJxD`t$e?O`z9iJ~WFi zoc+Dobnm>U=h5Z!Z<|k7)!$Z6kJi5c)6adWR=(su%1^;PF-?!5mX;n(H&i`bMdytB zII!lQ=6kjDwefI+JUa98S@hDpf6t>U>uc3og%|z zh@Ket!SVFk^vh?`RfpYk7+pH=)_L@L-DUN3QPXwsWO$xFhW^M;KQu!>%@0DD8jb~@ z=olD;<{GYxBRJjnyCeQMg6^2`E#QB@`Ft&XaNKR<=$`3k&Y&j``~6|`c;ibzk2}oU zV&s=r-&#!7R#w>@a#Z_WKy! zU;hIP&GSkNeVJboPm2fTGbb2k2G;@r^lo#F;U7iF4Su-tB{0VE*Wl)_%@}Iwg)wi8 zp&MuXaR&YCkZTX6563=-w4$pG;A6a9;yYEpt)iF5y@55GR;>XK#rJ6X>!_Q?(ANYa zcBYT7SHP7Y_(+;h+qo^~0xIm$c-s}a7*wMc3-@Nffk@9}yp7e=AHAtUJ0xu1Z2b-rl_WtmgU^<;}d&L^DcVN`{~PbA4emZe&u(o%{9J@bvephPo_W2`6s^nlKE~qeS|(i z@XBH%z?lbJf)(f;t!LdxA6@vBg>?Jj_b;M1YA=aJ%Ev1WrNa5R`b80B8l8td3G}U! zpFnYS+vR%PWuBAk(E?l4G|A!q)m%@8`Fk!px!nw50mT;6b9=d4$HS^)0GL0K)b0?iO|8h?wvcup|_<(r`W&)I(OUyOE2GqLUT#uv2 z<~;+B{+byV6v=nehie{yk~7ulesk%@F^`U=pNxG4|4jWo^un*s(Xbo|srCrC=eJD@ zKz1o})mugC{L`i#@ccmZq9}c)@-CW@yQ$19$Pjq&g( z-BM2Jo$!8I*L|na@agos>HnBc=gc^723<7cQfLa79-!YqeqA$NHR{IEbn}?Iz~O2S z(|6v(`G0g?$>mtFX45)AJKszHj$VMq<4vO+*vaNv0{DIAdNO@UK<5YMvj|cgNFRoK zgYFyiFh1+v83}y21(1B75sD|%yLZr~qp!vqo@W}5rxUgyb@(z*qg?opnup}Vf6`oI zTnEBQWUj~2w?{n#^|IWwVBr&VSM^V!KmPqt4T(7joIa>sEq9w)jer^SQH=B}rlp?- zcY~9B7>$paMtEfV!A34dS!vZ{W*wrUkod zzFI0X7JiF=-Ro*A~9^Bst>vym@ z2xxi}3=eQ-(|RJk>vZgCx!KbQhc%48Bh5qj9AwFI>$)7;AV|B{G>xJYI}y?_BOFV6 zE-9hC<$o=wE2?hB(B{t81A+5r!kv8mf((BR9HxJTN9>oFd7c2f;Nf%aM4K!S2&~P** zz&Ea^J24iw6*}+8EI0b(3+2}-)Vgqb`9;b_e<~~#gK)?yK*X$xj$VOq{Bu}h|E*bc z$f7K_kmEOBPoa{70#2KCJr(?RYOTJ%knSyemKXblD!OsRSAedk*6Vu^|KBdXlCTZ; z4NNm-+94tzF1?uOS^5*&-)Gv}vF+n;0N_)mr3%Y+0iIcBzIQnNyy6Fybn(a^j)b4N zNq-7K`-3CSR`VD1Wt=I|zs_`B%cr_x^Mx^jBB z>bWZVX!Vb=Y-J7lp^sscEc`okMftNpts2w*A?P^tIMIdWPkP$Nh-iiToV=c8-a|rL z2X;$7h?mYZja7ioa{$Az85`|db8bHaBg(2bKGnM6Os z%UC$`UM+p7HSs}YfZ=7t*9>0O(v2n9!hLt? zx3D0un)i^lemMPg%txT29mXXV0t8}O@mZum-Dj>#r~cx@#g~>voIj{m&iJkArGfK- zd7ideG_VV<%q4pKljuaSvxBVb*5hGtnQNrVB*W zXr5u3k$gh`&)$2-M^$a@Ai{|Akw=cRiq=G?{nsyy=D@)_r9O^e%{|-zxiv{v)8U?*R|JP>pNgV?02`o z{T19XgxmKz-v10gIIcJ_J7x*{QGW@H#yrPy2gb%sBRp7O&>+EK2S&zBx&@{<-Gr|R zw~s<-L+yCnovL1bzSX8CTmwZ@%+_7&PnIA=eb4_fQh0|-i25{@DD>^i?-~l4NRhC? zn87MF#K&K9!%z}6pxL2NecZ>Hrvy|!0Qp0zlUR+{cUL$+hk_)R6%*D_$PWhhZ5{4pdYKWjjVj4WJDzt%KfOYR^6t(AZR##bBqRtIJf8+ z=-TJPHCG>21}@UZGs$j^R5f{xaXc(A$^XttzK`I;!gw!Z97_YI z-XPBQ=Db;&9wYJC{w;a+Ie>||?opX|i0}?x>mm)rtp;~JbEJsWTFgK_K3w|Fv6zd- zO$m63%Hw$d3mms!WRj44xC7>p)?X8+(~+C*qS#&FHE z>8&Q4cPd+^+2s;)Q<&bYv00~iVYZ6dHg1J1i*0__}K`4tul;xHaGrPQWmww{UC8dD~>Tj&UCfl;U{G~Wz!LCt$m@oRjAXb2Nt ze_SEJPWv??t)C1c#uG9gl0uN#_9a9Tp9=Eq4x>JkmLUk0j(BFt!`=7d@u%_Pt2V8; zh2nOD>NLhv$sR8Up~z?ODs?rqse&1sESq-dZNu>q7o3)>ck-NsYQlVNAVP-y4Nu|9k>hGP2|!#>zXI}X@?r+k1H z=eS|-?!b`nk>MB<{%ts>hR+Vivhelc*c84s90$UG4@bL*&Jh?9F+PHlrG?R08@(YK zTcdYH<6891X!CluM~txvzw;~A7!Tvo7jW8l+0fAZCLCvDuX9T2D#3O{y@tuYIX*1+ z4dXI@DkOvt+{^MVq5jt@tt@0W=8zFOn}Qc(p4b;MVvHH`{)WL3RC&3JwzD>cqcDQn zH4`EiQK;1=Bs}n#<1WR&+<{$=-Wrx`gJ~E_AI}bQK?2R`_r3cSvq|rGAV^oHdcj)P zbfdLSN1!t$e>|Eu4Tc+;z5k%1=R2qp*p?4jw60F{);lq?juL1l?(3@R&Q~}eQ8Xd6 z7s=GktwobXxbtq@!;@w(45(b)44_b1Vo=uo3D-rTDL&x~N}gk@A}4V1b#K$#1sm#q zO}nXLuv7Ig`h4%Oacua<1P@H5C4p3KvfkzmsqmOn;Il<4ElwglZ+2xm)1Odn!WWmB zC`aw|9Nv8NRh)O?o@Hb!*Ixl{Bv(ScJNddopcupSQEgt>KP8ay7MgO7fhrbNykhux zfFfFfA_69VCJs?7eACV-CxuQ!dM@u8Mg_y8Gbzmz+67vs8K@U?)0Q0q#-^Z+-i2UY z0UfAP6Ad3gk=~d1gCV$&IX)t^8)eeaQ8x@Wy(FN60td>&#;RgaHI(O6PP5#AE*T$0 zsrDbsnPYDVJ^8^1|q)Vdqby@rp!qHLOTsTtriR3S5{ zRrT;s3gRmG0$!!|jP|}@>HK(ngS%JpjOP=hu#SfL)8m|#n;ua(&DgmHyRgW;oV`9m zU?-Y9%-143z4T_Xe!*GaB;yA8!N2j=6<@SBCVswfS!%p%*%r1xjE`cC)uluPzY*d2 z*Ld|xPJ@ZZ4M|YLYpwWFop%UZ!l-qvy(A%Cqy0baNbEVmfUH_*(j4!6!IFIwhJj(l z#^e58PIT}t@?op*IybH**ee=y^_;MkyxTD}@%9HEqy8=lQq}h<(CEbL@9=JeThrBs zk+-d3JHxP#)?jfVteqXB?6Zt#b~My-gN7Vwbc!k|GaO4C7#J}!0%IelMPO0HvIwk= zI1qs|5f>vcK5}v-7DTR%#D$p4G3XH6H5Pqh2gYJ^?6z3^9{VS!-b2UKC&)#e0(6P! z9f9JANsJu#9q(Ba_?gTP&ZlutVSo7Xa9j$X$8F)e62hGqZwI-+e1(OrfdkYI{Q#MN6p&)j2=*r~Q3CN%OFXvgd&n2}l*)`}79QOK%er9(mDe=$d5h&6 z!Ld73m?(paLR=DZkGfFk?aj3$G;D~o*oleG$xdu^{^G<==N_K;7B&y&;NxY?QlV+R zDF!)pkk8B2iIRl_kH3nyc{Tf!V;gtrmH(!q>m0!x)Y@Z$fc7h|FpKd|D<0}4I{siR zJgbn;F*$4?tqQRxbMPfAnEL2-d8MBor2jDFE`>03-c~&qgk2xv3yOG$Qx}!Pm=D=j zhwo-r%zIw3xQwS#wes-v01xKyy$8F+%PQe zb!>56^R_NDHj112zk<2+ZNy1lzQ4IEHtZj|hF+LGgbY{H)|1GARB_@avE? z1B1f-fodoX0}N}KTlJkf)jqFtYAYjQwAr@J*^zzEb(q~bh?|2VOZYe_PCBr`dD4kR zuDxWfCqaB$z1@L1&h1W&cdcPi?uRO8Ub%13&ato87bpj#T?>i4El3$`)(`1awjN2i zy}+?F88C-Kkfo+IX@LWSVyD)d;Fq7Md}L6RfXqShy+n;>_v_$97w16s!M!1otw9kA zS%c!3MCQUd9-TCfRO8w%h?|4rg7CRft{R#?5!g>$5fsL0qv<1@qix!+AE{#d5L25% z(}OHd^2Z9~IH^S;=S~iZ|KBtckALPq@qcSD=@!#Sk)NxE=E*b*XB;=Um#8IR627N# zLhN=NBK)Ah(Y)mE?V#bp*@TCFQ6h=+jyBv-ED?+$^Z(b?ErPjSp8 zT=DA?No?}%<;?n^fJsk~V7_T$UNl6wo*Ab6T_rJ&I?X)j(0<`eFSm2u z!!arRw`gpM-;#iViQSSgF6rka+(??nU7mfr2NYi)Ww*OXD|i zRh%Jk48AechiAnvA^h_{Ro*jrHLh>haZ&H@!EZ5VV=&b?1^nQ@!sBM8Ld6!iu8z;a z_LyUo+dLz9B4#^RI596~BjFDPj=?17TqjmWZ=ocvoxoG5e(k#M!LsP<(O4Su2jl!C zfjqobM(^li#<(P464poUio(UH&V-}esN$qyS=2^KO;$+t_^#umbG92jJWT(vw}TIh zqNc=QO8i;A;Yuh>#N@;siRh9vo1(**f2eco~@S5-^*PkAok7yT(i;=yenDH?d?Ygi9DmE!{9yc@DZB@=mG|F7$ zz&DXY2^R<)gTL&Z9XK3*fqVML1+Ku8>7U$gyMOZHO5`{uAJ$E!6sW_-z)7K;fJwaW z9Lb)hujgcUr@-vPeC5NVbaxe-8cDxu>)cq!7trV({#`iQMx2X4@5t|>@pEk3c#JA@ zwhWdhpH0TbvU5_fI%R7LZl>(w4Z$mt2)kl5{8e$$+nN2nr{GE0<2~!et};j1bH@uD zgNbF9aVu43C*juLsG_iU9MZeEaMWjr;Mje!C;mh{mL!~}tYV9>@uIgPHpCs^r?@I$ z5)K-hy18-d2>XjvlF`_0tawYQU_b*F0`m^_s&j}NbNyHS{La1k`9G0Bey%F$?jOSb zKSIDHERNj}i_@`f2+tKboeB(Vd0d_+b^DZ$l_;8B<2~X2ls0*Ls8SnWlnOb$6wMp5kyr{Ga2C38z!*Ef z$DEHrXa9FB%rs#O6uehp0PPYm3F95#J8&VY9S6`kfnzYtF@=J~=!1l{-YTajaK`XL z)6Fx)hkb$PmA^=-#_)BCe@{FI(jSRfoP0EyT4Fs_#6Ty!lF*6g@&^P=!kmPa2{;r` z>Uc%qcuY!I%um{3sEQqdt2MjTvS~{N;TCq8@mgbj*apg--86D^De`m-PRA7{VN+6Z z85E^lO2hQ@)9IL=aXJG#$`w__fr`^B;Zmgol`*0Eff^{vS(<~>ImI<`t>(yD*jTGg zE(YeV%f-Rm&b4u=_S!l)U1w4ris}x@$NBsf^>C!#U-dAm{*3xqQ~ywX6gSw-eY!qO z6*ExDWl2AA3aKn$5)LQw`NyQB?+M={a5Q!$b|7+Mq>4Q|3?1}UE_8?=8;{wk9n!EN zZFV|VRk%=r*MFlFn<0}xLQR}Y?MQ*ZXM!hUYib)JJq3xuvDDr~iUpZYnG^bIIPEWr z!=|{RcpOchUKZ2K9pK^OoUpMT6)-IBTaJmO(JJ30_Rm9pw2vD=xR$`F_|d0vF<9gO!;fom{o*mK%;7RPS@v34^iG+Uf~zUxn8ZejpelGW zwver8o2(K_#KzczvFKE`Kh@b11&+bG>RR>58S%Vu&G>ed7Lgkw*rc@k5)!;)lF4VSZ#VW z45+@aIu2IE12LC0t#t}g%dL882 z?8Xmej+Mc&jCq;ZS?-7O7*zQO_N<1*Dxzx4w&nVGbRA7@RSha9@GT;!yf|4qI-*tuq0?`sA2H6h0@+ zO+d$_VVr_eic?XXzCIlTGL|#;bHb~twJ?4oSL^PACt`T~^mvR<`=0a17=dF@nD$K? zhNn#?{H%N|7jLqt@jqXcKv`EEW1nj0hOCc+%i>fTj&rQx9=`+|KU>}c70r0c!=-7T z@m}%-y;9g0*s*i3BuyF>$)IeXmlP*xfs4jC+c3GN z5=@=sduR@z`+pC%k~G<-T(AEVJw~d8fbELxMXHKr(eOSz*o@mBf&Q&LR9~!O-d^C10)xl0&x`2B2PzmEu;ScY!fD@tL#?;ahH5MqjAbf5 z-&NKOyb^81Wo@}aHKg-}?8311^ivhKg`GV#s5nf8&9)n1)mNx!G%Cx5=YY`X!qCCa z7Y)7agZQYMF0ZIpw^}jh7(M}B?-0h7ewD(gL%>-ylujevSi<=zh3B+qLg)l4<3?FJ zL&pv}bX%U8R+Jtk|I)30v3i00YhnGXtQX0@rPe=}zRCLM(+^nxVs%?xGv#DG-TIfM zH?#iL&t8j!$oe-eOfP6)COIffKVbcx7^WB9VWMpk z^z{49zkkN-Mb@qRS-T!(-2jaV*OxwLG8~A|(_c0J($4Ahmge7SQTl;*1#R`d`L};m zs7=(O&rGzNrl(s%7XF?7m5FXh(F?npf0t$In}(QwuimL|vL^2z9eS}fGqthn)2$hZ zWhhu;@@|G+Z1uMNURyhlEvPH!TxYTbbep45As5F3G9bU;4kU5~o**gbgZN$~qJI{CqWwcb9;C z%)nLy{_>xK>AI;Ak%Fz-9!fob6r7`m z@_=I@v@r*q5}M`9!2`4-JxyD}i1`~;g7g4)7#BG^1)&HeK20%~u#`jVn{1(|O)Ac> zhH0Npgfa!3q_FUH*B0U8?F=O>XN9KdBmvd9Z@(;1z`9}0j>bz#p1DVa@svN`xI#7j zv{LKq54-j^s{pXYqt>e><0e zYr=n`eLt%zQfY~I_Op#0N|VC2^Wz2_3O_G6m%t3X_B9<<3gj@n zyxbnWg^>e$heV|3an5%Yg$Au0&^E&s)f6#?*2K%qtA#*(8W@M>AD4s-y05UL;jV|M zRrQNX#fxS3p$?1-cq3}LRl#QB|Ddc~AT#k#OG3>_KOu?LHXhX9OS8pVX}th)0%qJj0I=3K{gPl31Vx?xX59t8|$T>_Wq8;$H-W z#q|uk zDi2Z`)7{aF#R-O0`&R*OJb4>-qR&zWEcturxMLVwthpd&i~T41`CY174#bw6ZvJ;c zA4q~!>Kw&)seHvpi*4Fu!DWgFLZK5!Db$!aN0Lx5Cz}#1|O{tiRlLpSDJLALvn5FPRU$nN^)0cH4lmA=}UcdsMZZ0gtqw>{YZs z@W$o8r7uc)WzfjbDDS&ZWx$sJ z*H%O3agA35)R&boZv7IG+=N>0?AJdTOUs3TjfpeD;l{m>;yr%&>IYSZbcdBgQ?_Rn z?Lrzi97h9N=XcI2v>9G}jnW>WV{m6i|4sZO1z+= z&<9U{I+(#}M;GQ$Rq!iRM|F_W?h}YApz9s%EFsVj3VK(#68PSFHklR+@;KG*_VAl* z7bJlWA65{_`%4vr)~Bfx!k4WSPk$;X#Z5!(v&lF_AX}!ex^G( zY18kRdn)gn|M!6hC0TBWezs$TgXa21IQO~no7*rw5OY~&>&AR-1V|XFkS$@ zB1jyKmS5wyTy;(Ha68WHFxz!S(`;oTP21DhwLlp(p~2hE8;Zq)PTnFaXSNlDme7Bv z-(2+ct|LBM@Semy0|*@yXn_N(Jtx>$Q9J{gYyMF?MtT+!Y9vr+J1)Ds68cae%5{Hn z@8$s5$8CZc(Mco8RQMH|>r6*>=mNW9pd^P*0Lsx?k3clH#c8{f&~EtYW|3AQczhUd z*lt`!X%PklEQs?y(aK3fPGdYoi&&gVMh8@#AA$-?;|n30xf9wr%Hqj4d5#u{FNP}L zO1E*UBo66lx%ztNHcPuVV&V;8=;bWSC7uRt9BKhx4B4W4-vR|ah?4P{9;2og(| zll;gjih~9islt2HwUS>j6n#``**#V9hSstK%3AIgE~#Z$nu;j3%oa#$`KUlr%g+Up zT8HvzF7*H=E6*BRbnaqvS5#E94l8M(1ZpEKiv|&gAWXiaCKY%LHiqfXP86B;29tK3=Vr=Ot@F+RB9 ztoH%uWhaK~=Zvo|I4ghA`6Hp!@~TX%{3+*9LeFL^CM6bL!gqCe_HU8mH(&UQu(O@N#&1MRT-(u|E;=0 z0xOW=|A2O8zr}pu*MdI^aIv7x-I#gzqPy`^qkWCA<(@P5Q08=Nq_Pwy3%Y@hR0>uV z;4dR`??$5JT}f;|N|dH40Zakfzr1NTbcGFlS4d;8s3-{^QFUdPo? zC8VR#(T$c7`Z7mBeC)~|`VXP)h~Z<W;Hn60H+(Y zxf8wa?AQ=v8vfpd5+3251Wdsqm=P8?Sla--?wEE5ez@b8J230cm3Q*uPk47Z(4)bC z1~|~*Mgt7IWAq(pcV{1Rd?g%IYMa+!djp(laIFFM-0{a9l>bTKDmo!=Ftq_@-m!xC z2EqAuV97rtLq-Ci_3Kvh@{VzC|1JD-RBwAP4K`DZDyx=%Q$-M2FDr##H7yQMBh&sWrG3X4P6z3kPcz=3+|jsQRHPaRvE7|5-Pao1L>H z2Rmy1T9dqIYGHHk@m!p!-KP$I&l^w|gYvhs;0bkADK9$FKBs36#@C!t6Vq!gt%c&; zdAXQh``6l-mB+#!tviMh8cKwzP88PcS`%Gr4XTC9wR-2GPwfRX7o4{w56kLaXA`_3 zyazQLsCl|3Hq|;-3&(SN*T$hb{qr!e?k{9361J{f9F3fpWn8ep}vC5UN}W) zJG{m{&Akm2!)#iB%bu2vdD$znu_1eFHuh)#nT_rh$5q6PimNJOqhYA=&x)5SqHU#K zl`yi>{7U$x(r=Z}t8#H=Y^Z#uG7Z>utBRRbJ5ho&gLXG7-3I(%JA1jbg_liS} zgcnp~N|P#KT9pk|u(|5xs*HM!retc&;2!UDDIGu7?(7r1Fsscz$dLb1zQ+v{_T-s_w8|e2mQ;(--`oMIIN}rLAAJccG<9vF%49uW5 z0ZwN0%EY&s8#8e-b5a(zWSz;vpmJl&qf3RI6|gz`*NQZeBKb^r;ydSjK4vm93n&N$ zQo3d&(l%>|5p^ZKgEKylND^zmhQ(`)u$Ymmpjk>?hWA` zpkZkGq;!nQn3utFt;oc(tUt0aq}&FwWf!Oxp(&}W=|vf6lQ}pO1G83S;cV6zax@c; za}MmFVg^oS4$i_4Sy!{TF_A#dyVkw%;Z+_lm|T z^!C4ZPI}IG=DM0eupOC1Q2rrbKF{9ZN*Jq~5fNn{+7&W6R7cgI~*Z zO2*ja`N`Ol{97`*m7Q1?*UC;x!Lrm|hMC#H8KJ2MT70=PB=ta=XN^PxQJ1A2OJ%GD z)b|)6WnznWOz4+@iHUO)v8>FtGT4=TDH%nnb5pS+tt)v~3ol!IfBc{E7+hvr8O%)H zoQz{By$oZlzwyTFl(4bI&m?s$gRaRFl4&~jXbL)~%_m1$r;d0~ zaZ#0boO?Re#0(>E%e_B)G1IrghdtpZ!?7Xy3Xh~g-hEu#wBy=;ia>h`R;hYw^rku9 zgI?Z|UbOY~_E7_BVK|0H|G;!x3fm9BAD(OU#o;|k_hI1!!tqzs7;^L!j;jvL^sew? zkME=p8=|g6Q9(um>A_)!Z#m681@zYs3;v^>HhV{rHQ^yupaZnA9{Fn|oBLq}nY+;g(=pD0#e9o_jvm7W!Y>j`H`MmN4yu;yXd5--c&1Yj&+jIV@%0cTaeq3?fq-_Vvclee{Lg+>-87CZP9Js__ zPWTd{^IKyl=KE9p3Rqp^-sx+#LtD_Sz0(eROd2*@EAK<<|^`)GpAFzooMAdgYt$1%Cr zPJdq;w7rydeI;S$*KQ{?zjjU9(7#S=`i7s$DcT~k_mkjwK9uME=6XACtPa{w*y;Rl z5*bDa11)&a@(Q-w4^lfv8_BRq66U6@ryp7RS%SR6&NJCT9~sv`8)thotal#bhFBk? zV`<>ULy*<{yg}OhylW+?z~?<`G<_i-LGW@)LIR(6ul+YWO*|Q#F4_N=Ehv_!!Z#lhapV<@*^-b2l4R$@YJ?5T4>idSvN|5h3T8D%cT9(yR)uQu23b-y64#G(W5NE{fcn?0L-<#t3z*P zNv^kg^kTn>c692~6XfsGGV*tm*Zg}>)0alb-|5lvm&*QuLCwT+vdq6-z53FM^0%2K z{hIrro@1??tWcpfI=ee_z$L&@D;dFvW55atGH8G58 zep3yt?j`oscAI|SZzj)tyME0YH#KSg$3p2)9(1~!(ucQX_9;P@j9XZ$RySM;cMYSG z;E!Q_I8aL^VTPXyJI|Jiep;2wh3x8ho60I#0!4uG#8+>DsMaSewCH)iwf28NX-g3e2x~BW}9d&o>M_*foh=U{dlGYTE4?w`dWKv zYE7S0sYM`@l81`VD>?_cO?jh>w_4FW)Kb>}lmyW)DOKzTzo1xoOY{L9R?*saPtGs+ zF6=Pb|Nf#14y3F{y=2nUJbJBQZI)UnY}C(q5HGaEn;!)73uWrmBAAqsbBmW1O^?s2 zkUay1%zi}$(W(pu52uALk^V-zq|br0;8hjLtXVZFl#X)r6WYM07qm24c1P*?R_Tr7 z&MpgvErQMv_fjfb?lqMbtvOMCR4jA?j)b2Jm+0JhpL<*J#-ZlD+&*=7=$~1s^O~>A z>nfPvl>5o4p)^J8)!$IzPE@Ij`yS-WFM%}Fyc10xp!(ulii>`WQ*8RH@0zqvZ)9av z+ol&=;ND1mt%cGdu6EvnVXK5|`cPiCy!)O?&BYB4uT?{6FMDJ8+d{iTxD4O$rRn$I zSEa<>CHmAoM_lW@r0)}vpts^jBnDkSW8_er;=s>;kb z#*GS36qb)@fnW`HP_8X5?SPdAY>9!3;UHIXfPrtcB_7T1G{z0ba7|aS_~o1*srt~H zE(HO4s~{c8QYQ=jDX+AL>G(_U;R-2UB8JdeUMOEwfQ6PJBqp7-H^zOBW4m_KDIbw} z-~F-5i}vO!b6iRcS|E$H>9mR(Sd4r+nwpbL1XuhIRU}%VZ4*YbT18Ysv=;&coeP}W zgBGXJAeHV9WvA$Y3|xUE0ha52>i$vdUd3Xe21|pOh5B6BIcmzFTqQmbu*zlRQMNH& z{s13M;I!iSRF#Z|m@@caU1(Qe0;?vppC;RFdd_Di|9+P~#wztIydAgDfOp6!Jl%p9 z0h(p-enKUXF4AeCNZo8wj5GvhKlAuuCdkzjs%R=v5UhD8qa{;$F^zf_q<2Yk!_CptHTKaY1) z!==!MkwZk#CLg=`C_$;Gz!5w>l%}j?m(X?5VmLo;=Py(-xVF(&dM7KJ&~JAeF#dL& z{|YR!(!83WJ0CPVd@-|jeWkK?V>9hvQg<~3iOS?a5}F=&Nxv%e7QQHp+d;kbxNC9x zn^vNK59O$a8qc7`o1B9rk{wlZ(SX~6n6FjNKBUu$wXyXofi>Z@8^S|vmGnBLA)8*K zqnV6PO2E6v53Ir%{oJ2q)1MHg6;=o(A;n}z`ajWTSFiRqEl|6^>$X(UAHO0)BjLarMbFDWM-pAK97MRXl6F8 z)P~UIdAss_2<=HREY{^dkG|e9+y&ewhza(U2<5QH^9M(S@1H8;e9ZD3@?d88k6iha z1@>dC=SL6Cw=OIJyDg-gFnTeRPT{y|5F{T1JZn7Y=o`+pZ=k?_boNa3;4kktC1AIO zj1)#M4cCrkiq67d8P~b5xN*kQg;k6b*pCJ7gKmm2uaG+kDGpIz*EYE~7&fgwWCw!;2a<24Ahd+d z-P;zS9cXwD9)HTF#kW;uBMSxd)r8K+3d3c?w*fDScL?mKS3$) z1hWKu+5)3(dIJG}mv+)$60nQ~RJDsi65sRVvE8W(b%DGjrc zdI+cSDikR+^-&VCX;)6G#O|P<*6mCu?2JNqyk?(lCiIAKOeDH|KF*JoT*guZZb_!1!9tCfPW1`_A{zAmf2powTZx~tH6@&KvBC;+$ zsY;cIM%+sFq+%YQ_VPi*PGf`*b57(Q`+vlAPBQi4mk7hu=1EDL`SkHX;SAu)8D@>fhKgCEJ<@CGB80@|1(s<3nnOLw>tKSp3NP1aJHAR+Xqo`TsC zymwv6U>Z@D5FJywxuBH#PnF8u6vI-teN)(cx(&DYr+E38gw@4H!~euOd-s104?Lw( zqT)Jt*y%z$=yb-of6Eetr;`*q7&O$Uf}q1fo|i%z*Ts8Sq1s1P(x$_-M}$s7tyi$l zy^G6R39upc+l8qdy1R#xV~-$_NM(zj2%(MP<36Ucp@%72FXnc}FOWIwR}s1%uIsa1 z=o&ek1#BR&A18I%DZ3KUvjpt6kjBF3#UZ^bkM{2fLaEyu+;^Az1 zd~qy%X}a@qRro-g6lz6W*GhnFlhwjR1&+X?6MsTQGgrRyLd(kcra)#J{8Q+BUeEOc z)_L}^%smD6(;E9_nuzdhECIVMWPmVwc~8kJA#JfBoX1|Lhxy(dXQ5z~hIr^%o?_7~2;W0Z96gGxd+ebZ<&F`s_XDhp5Y zwlNA+1!Q7_tZ?gqqgCvpijjxu74}!afZS=hoNfe;#mEY4E1+|3aS7OMAqB$7M_m<` zuz6n*BoXDlz_l8^c!OShaP80#7Yd%lOYhOprZ7jLEGt%mF2}AU;pP^oG<{XVbyTR1 zX3Ya8+wEOdrCg|m&%@G?YP?sNM!!KUJ(48mAeSfXN9nIE5CBR4@;f zJV7oIHW#`j^dZL@LHGo-62aajK;1(2N~V$YiP4_xVR#RfG{v?rmZEu)GETzt@NM|6 zaLfppMw%^fEPZk;567s=^DFc1h`??OSuBW$R{rL3Yj{8qN?qT^4#!ak?MTrm{L(7x z`Q<``YutloF9tZcqV-e-)sW1#E=_NeaM}jS!~=%f^ zhpKmQW+@8N{zTONgy(gEfb;piS>91OEk!jhpSs z0=^+}^_2p)v%pB3?z5%{m&|-NzI2vc47)^Ztq|IG7yWF&P%=PwYA(?C^A2DSc=2~y z<4dPYTA>p~&gnXeT*YqvoHd=Y=oO^%7#?jl2tylt7w+Q?BP;_0Mp)dvRU3FH!5hKT*TaHjq6)`dY5J&yyHNHKY8-{q)&B#{ z5t!FU

    4nyO%|<7VQ9cG!QV%zbj{hxCA16wG&I|z1PHtj|EKZFD~Ra`08dF7 zBC#QME9GK61Tm+k5;Ui#cnXk>A8h8XGDB!{N0l#hT`Zt!RJvx|!DR0jI2MzlHb>Dl zqhU!i2)ixhF=6yjYi=#4=Z^*9k(Q7DzQn@lAEU7|_ID-_40NG!bF_G$+DyXFQ!4N5 zIKPyD*b7Doi?M;AZOXx_QdKe8zJ+EutZ`Z*jK)~)H|^-;=*!K}CW*pa%HK*?7^0H& z;f~LEo*kxWFYcvt%oa4&K3qX&Q2NK5Y&FbR@~G+vgr#yO2vx&P2FQ4|ymbm6Wi z@Z!6);3WK%K~zDFx}~5rw!W~Kt-nQRa}1WIKQ304G^eW)G^eY7OLMwfCh^VbYO~OR z=}NCCWBe!UvvVOR5OA*r`fPf00gqWA7njZgHnc#OO+TtYt(gVFmX5dnVbjyC6=rKS zz3e0AHkoQF+6)Ue?5E5Fw)xN59s)`X+$5+)sPFn=aw+Y<3f{XcuUBqV-k%LpF2y@6uym>iif0= zI7u}aU1oBQDNWxS5^mhum8M&TgpWx6C0YB-DkE%V1I~VIx?=I^k@L*V>fFkuRh+B} zLW4jtfi3cFpjU*;jY`~vm!>yLcnapbw^9`EKF%eoB)rL3w1m-I>`TyG?DZ+CI9%*o zmZCW~kCgD&FoNE6F|^EtGJ3YvRk*4L8AOPGOyZa`Mk!ifBp)aw9p_Y4kT0<=cAX!K zbN4WIWHb5%Yu^n*$x zCTj!E2OC?T%y6e=94E4c&4uyyX~r#B@KFJ^E;5bhh_ z!tW6mXyP^YVywQ!>X=L{l!s5szH`hNLq_BJm_ZLER0Z6Hy%4 z0|t=@Ob8&)e5W$v_9Gp|t_dzobdWvjq2=`rRjs;2UCgUt#2bf zhQ1XloTIp0sF=&$NGZBtwhC`&K0%RXnW|1IupLjG)^tqtVJpgKA)wYu(s98XY1kGs z-1K}SsMEOC&>xcyaN0_bJBUF7?hk@91awJW~C`)v9PDnwsug>c+P zPf&F;=jIYL=jJ2{=iJ-^owNbm@s$8MK|du-+^l}BfC7L~9#^rdn=wq~5nz&DwFON6;Y;qns2hi;DVxCeSjkT|4& zMBB+T($Ft!WS0KBv|b303wJa_^Vg{{Dbd1_$UatrPPgd?rCCGL`CTHpP^}K`e#fSj zh93MMLHF`p@U3bXb9354XmfK~PYO^k*f-u1Fi~o!e=1;ai<2G3w!lpOlt;f}vGU~6 z-$IRznBkiFw9vG2_XLk+3ss>g@Q5?qsnwxdzDFsIT%-ajAeYDM3QHAjCX~Eb(G=?r z3Zu)q@$}RYct%q?$V77`0(Hc;226FmArRG_%JQyEXx=vUHeB3(O9SxsU${8l8Z=r3 z{V1`%LR;R>@D;}*#<(dk3ux@ZmIMo2sU8JNLPygs?biwVue41Hs=q=3RUcSI2k!CicV#A zGFq&_e(a5GAB7o#_j^IuZ6Rfak(Zc}SGn&m5QNT_-$hZ>gea_y*%Tw+&KAn!LhgNd zoI~e+i58AHPN5~}GMw-w+}r__qV>*JYq&!5d3L3$UBK|UAy2psv@(P%i+!~OU6Dqq zBs>{4UoAyb#qQ}derkz$V{O>hioQ4DnQbE#m!!XNm{Ie3NM& z%eSWlL&WVF?pvp_sET>^wRUQJUgD?=!fp#`CX88_9`G#wv>;{qu4OJ`eIdo6P3#m@ z{U`(AGkgLV}b>MHRxvruTW`<0^gC>e~&kC{^xH=KoQRGV;{z>Zad zW~QAboE_^W>~J2X@TdgH-nBxQ%2B&u3OORzs$|*p572s!(6UW|mutdI{Ugp&C1_4J*CgC*-O}`3Kd2gus!?f(;_&|gL^YEkl9s7zG*)O+)u^m| z$#<=3wU$kPNWgsS^KuW+Qb4sVaijn4231tLe;;FKo37Eeu)Vqn?lYbdW>v?tFR@z* z>Vjd(_h<+m#h%FtYt>IG!2@>uYNuCYEqSAY_~5(Rt@Qg6o{B7Xq3%L!Na5~bNMIB! z6O`7JGf>dPrloHRNtp9e3!%Ax;U&sIdnXOW+C;0@C^TE>#Li52BEh={93l=Jr3mT1-bTlZSx`se*c)d0~ z>Pa)9%h$u?jnR*^CBMalb-mzab98$2bPMz8Ir6VS{x!4y#p$iAe@*qJ);~UAv;NUO zyY;U?PyfZtV_ukk!20{UU2nF{L|@UDTK`(;>Dx{0W4+k=H!NOX`l|_!O0^Y7aEY|J z0s?65`WX|4EfzTx`bM2qv=`;LP9}%Y>De~DnMKdE>-Sq-HQlDYDO~jZa5M~^>_bX~ zcb4!vczrGH8iidA3ynWO!UInewA6E|6owj%OzrCdDv1fF>l?|;mFyRkLh5yB9cjLJ38 z=pnrIF}~)Bp<`G#tp&+ShN&37&leB(`{&W2q}72QpN1TyNO;cL`n5R{H-IGH!OrnsEO8B9NI z(cL}zQLD>vZhii`;^HaqoMUha$YZEP&{cXn^Z`a<8f@C@!XJgSYB*+SR&}A>NGg|0 z2t9Z6A=*Pst{50cbz+*1(O<|H{!3Sv%@o2+El zYI^m@&1HxOn?zk@HrKG+dljL9RV2`hH1?l9Lf59Di9&vZGHw;!gZMOLbF#6y6cmA& z(_x|My)8lENJ^pOYRbz!ikoIpyr|glworj^IZ^2ay!;kthl3p1tp%sc1@4>kyec&v zDVeyNo4?yYZwi-6ttjj#rdBMtnc7O9N-Z2!N0Z^S0F;+!}U=-H?)XRDRTzpKJiPTvoR`sGcT7i1>pgNz5GW>gW?gU zS(+XksZxwad;|K2KkU*jSdDAEGkp9x-fzb$9v<76rbFV~k16^pmmdCvIjVT{3WkNF z0@eKvN_h--UC63R)#VV#Q0Z?GrIP1!p}Tn>aM6u{G4o>d1y(t@yZ=;p%#M;8r{b7& zR|)4wPQm(sk>Fhd(x#4+#_Hy}m+^$1?pJZ6xyn(}Dl|J;V=Q(pb?cRbHJU5^iT+1; zgRoWDb7{L|CVTzXkirjMc7jFcZ7RNb2BoJ}*c&Wlb>WonGP zQ(;4yb$j7AM@{o`!Ssw#GgrdhT=ckH2~9h{@3qK~qoa9jk#B9KjyM6|?}agh`P8RMc^no2$}E(SUdQzhBg zBU~@AlI6H~O?b?1aU)&DG3hTQJTNX6atm+M)(OZr2&tG5F!E!QnC7UcDs*5}YReu zt~m6S7Pn^8AFD0WnX+M;rTUS)!W>4uQbR^2xz&-bK zf)t2W!Re@uJ^6@0+=y`wEv!^Bm0?0hB8?N=WTB&w!lUALp{p93!F##Ga#U7Ha6eiG zRVjaf@mlK=-zi17RE8R+Sq$IinU-QK>|spcRTJprnlVc3?9B{WNLKgXUcO zaq_klI0_GajagxTuv_1(s#4{}AEy>qbwx+v{?;hA8>%n21rvqS&ut2A@vOC9A#YWI zqwpkeE>?zJrTsG}OD9!nGhs2soA+viro$h!i z(EYY=s}FOj>WzW^g??P}w~y2QlrfZte!P$3A}f*o*zTqgz$4xfJ}mLA=N4gQUNB>0 zqiCz8Xe!(mMQ)0uSMa`mY@!~;ZBq6K_uFVqK~%@c)sZ+H+trWN{-19bp9^)Z_28nBhav+vR-h0#Yc zBoRN>I0xuBSs825OMh2}jMg^cU(QsF$+_;KET)sE|dr)h^IgpNGu;Mm^A zfewzYT#v#FR2(1S_C(4F!b5#sleep5TImqFK{rzA>X^)sw40%5! zgom#{NN@+x2NcbNDv#4We^Ubh{*wHbR$G*^@#0rK3r9fam#~8yb z#rKTdO(L^Jjko*sMn!Y(1IR(SvjlRJRGSM9qmOoQ38p-30UtB6e+UN;8+0R4!4TYB zGK6Mr9Yqcdatx=BQ^$C6;%?P!1o1G=fsT+&ET)8hSJM; ztT{1k85e^tja8{l;~z&Qf@%EYf`n_VC+&?cbDT0vC=TLt?SNS1HVhja81-L;Ex%V4 z$~5-zpXee!{9;EsD>Q&j^O%5z?rE^Hi4oEJ6;T^|jfrEF)egVVQh zZ{~bJ#pDN#LOBZh?Ds1Oh^lDQ>Ij_#Ly4%T(2daB{<8z$YRfgQfs&G0hH($7WOOur z5wEiPi3*GPSm><|(H5ZP`fdPCUcU^pxyTrbdCi?B$*umQh z2L*dc$lw%^L#BRq3^vo6ZKP!!|3(NW9~;p{YZ|_>Q@mw(PQyr=Vx!zHgu@p_8;=7G zPsPOy@k@xyd^=-=ajW%+sz3y)*2bQ&-#CUJ641vj-a;Z-k1959+$sBiLTHa+>f?o1 zxNr+p1-0%%x0nSnxRQM{n>z{#@pGXb6@wGmr%S+Y3)vuyUYv^_#LdS6L3H>t_(;B) zDn~v}MUNmylpuZ_i0()vNf4@5MqlS4;srtSup;^#k#_}&rKy%ZL_QV7gGtd_h|Ccr z4}$_)6{`h_Mc3%>$g@!p586aeByvTNd~}Tdo{0M~Rc1eJCl4bMBZwD&M)f6fuORt+ zHolI?Q-b)>EqVr#7X_ic=I9ATdJB?|;ZZ+DQB_zFKfZ~YP2^iaXudgWGLe0PNJtckE>eb<8WkuBJ~8}CX#OssevPi7e7UwC-RvfflhHH z>RV>lK|nuFMD-%lQxGqU)S1XgL3HHN(!v8gn+X(wq_UJEl%g*QP3uYANR@peSmM5s zzpyk_1o5Lo<{ljWR)Oh*|rF6 zmgTU}X83ub&G5u0gJta;v6d-j3*yJ+@bN@)1o7f@_;4aE1u;|oNN6+F4nmvZeS|i{ z=LnsTiQ&g64O}gVAA`f!64@w-7rnxl5b-}1ENl1h^+d7+@#DI05|OHccyZo0ipYn8 z_XP1`i}woUULys`$28x5 z@=O{dAYOEKPa{$+2t{Q{w6439%gmnw#vwh6;*6J6narM8S!n7cQ2JF* zXfwQp(2+>yDdGd6>B%vHmwKX7Aie zHuL@o*vi^1MyL~aUVxEh#Cq-Jwf;yg_A z?B_j3fgrKG*;qs50YN-8!L^vkSAsObD$fNXT?L87_ny5(`Uv8|6yx?pU;3(rG!d_VhHkv?!FZ_pM!bM-|D_z^k^@Bh>6L5T& ze_bU+?TX^C2^6jAGdL@iz)dJuSi8{3Pr%~)n*vc*&e1Qw03N0(q-8P2$p6 zC~xGx7n&02ax^0;bUwXFY;#dI*2%*TJ%U~5goMS?{oq0u_8Av$9RhwYf?@7Z_`F2% zpu1}(KXA;usye>F1fD9idF_}lG@l_P^ZH6?bJ}}N==%6kze@E}*C7|$xI408K9R6E zE^xo;G$^~(g|n`|m~Sx59SZ+lqWCaV-^HFaU6451?Wd^okZ}jQz_pZ#{Np`U`1 zpHovKfR_#UyD1#Bgu12ipO~PlMCK0|nSUZF@LJ~yo3>48PK8BuzaX{Zxnr*c zI*|V=y72rT8eCxIZ9JYdr1DLL=Jf!d=`^#%u0Opea=J;5pgN`P2WsrdHIToxsw^$p zadv1ebY0{Z*z|O(PNVJmlU7Z6-fLu`#^tkqj2AM^J~fk}24l-0+ea1}V7p8L{!#Za$|_qEy@mDfIosk;8TB7hcnXjs@MLqFRuZDr z;7e6s(4IFw=lb^H*9xLTk8C_zXc&kq4#P&rWGAK@_uwm>zdCW+*+ma5DwwD*;-sUW z*71#NkP9PRV_f*tmp?|>snEM>YDBd`P zYi)k$KI6KNA$rsO)|~G^PJQwUn`D1^sm|PKe!sz;TV}sO$n5`4OEuSh+xswl1Hg3v z?)E57aT`0`^5iUk`m?XJ>E%?Ud&+WWuJbO1ZeKL_;z~x>5Otj^_HzF+`-VCGA3x~* zxZQmooBaOY?(=?IgtsMQOZ?q7$Ax&|=1Z=7-Rzf9kxQ?(@B;T3smP~2V6l5_9ZYL4 z9lWMXFx75>D2#v~1z6`%&UQ<11bzbUG56zhH*UYdZAP%?#qQO}oE)glXs5^Slb4B*!P7bSKd3*Iiqo!_7JM*gS6@x6nejMx!pbc@RzqoQ!qF z`@XMx%qiyf`-ePe4&JDR7p*@!%l*E*@aW%W{S0mN3-4!GEx0he**&!fF{f^M2SRth zFw4D1-YfC2Ij#05k8~l9a(`x)dzX0^`w`dMV$F7sMeCgUzRLHS6ZGvA517}`zjTBB zS(2yT-616~vao$)ry8ZxyO!r%Ko&pqT= z_k>eW8V_>aBTjHn-Gov137cv^x9$vQ_Cqh)?~B~cICk)a@B8u&9HsMpKNNfZV@!YB zL377A!`7iod|VHBef0YipR~E`FVVNx$j$UOBJRk&6@kR=C*(ZTx8^@O-~GyhuPt!D zx!~Ii-0$J&Jgk^c*>rfP@BF*q$Jpv2vw?{3<-!Ou>O=NDz>vB3%%249d%Ry30v2D+ z$0Eqx5#-wu1@ejo-?`1^|;7k`!=e_8>`@)lv{gP#ci`n|! zJT{14*zao0t^Mz@KkV%uay)kMJVMt!tPv9HEB1HXF_m(+Nz~ucdo8AECOjHV^C$23 zHh0xlw{R=ceB!=fntJcMrg_RMMzdr-2izOxJ+P1a z;Y04mD)s!gY~sc4>zi;foJWk!H{3LeiFn92EeTeeGu^Y!b?;gFGG@nRk62ujuJPzJ zDYAMa_PRV2SFfJ$er5^Y%=(`Pz2jiKare8y^q=!6UH714VZwOdaTI-%d-ML^Il%q+ zLH8o(qe3a?|D)wJ&&0r;!D!E36D%GIq7}hh^rDO z=eiFq`B!YR?iEV*YmZEmE%L?*%IV%e+kN-`AA@Ol=C^~nz1bu4%9-|{E;z@(vA6r! z;-AZ=+k?TB;~trp^8Uvx$vpQ!IFe4EvfmfswBkqc8nM0Z5u5LO3jc1Ox!!$vnZSsTNoZQc~>iZU`!^8r(-Vbx#(=T?% z(5=NmvWz>BcSp9nQ^)@Tf@9%xmXmUR#G?l1H{bOOW5|4e>2L;%F*!#g_KZF2Z_rpiuag2NS@!!Rg^_%|;vVYb~ z;HqTcVEPlEvEQ+lVXXFe-?5gR<^FNe)r;Lf9{E0`eCFS69$MyUkIYN?vIlj!A6$6*BKQ6ye~5P2 z5K7tbZKikiJ0`3-}T76l>g~LXSf?SxzEl1^Bnh!hd+)j zorR$SzwTLE43iiN!`wT5tprCdkY!E_DvVM7e9!l_Vs_T@ZoX^z-6<(iluRKyMmv3c`whocjI2K;tYP_ zMP+n`@8ta?v8_g9ulA3$vgF;(KlpSdJem0Q#8(ry<@_q} zxORJ-^o2W=&zUUen^7>ycj|7%?<4;r@wT5S{xOtW9=VW6=r=LnMunYS#IGXWOZ*TF z0HOaS@zaQR0FNu@`J_L74f7?vSh{iaVu}d=%B!6{eI@>06a1WbC9p;Dzk#5XcD8%6?K7AA?rf|=e=g~F zlAh$wTZm7G@I3L_b2wm`;0oYU-?m9zr|hrS8~i}$NxUG-|9qKzdP963F!W2DheGr} z0UlT1Usu8B9H8x1{~0Zb?e}AV@8ulgoWOE&`EnXbKSugPSlErEuMN?kPx=YcLp++N zNPK4qzmj|o4)OU2@tP2RH}JUne$(=Q8x~?bk6>QOv-c9M?--6W$+H6ENuFx`z6$hv zIr}?n-WBW@NfVcE7w8>lu~V)I(q|1Hv=+-RRPq#w&kNP-J-}xS)L> zdG=eX`F4t;i02sduj9Ot_|$pYZYL674qVDV{e5LTi}`LP{myd~s3ZM*h}R?)KZE#P z#O*f!67u;a@v(veEyPbfNXyy#HZ9L>?9R6n_k4NK^W?c5xRkT)8(PjY$>)cppFU54 zOO@RDZ{lt6smb#e(kBkqe0Oy#a5(V{@yFWte=M!-W(}?fiPyYE>&xNnJVktx23CQ&;MAw#Q+vPixt(|`@kzK`Z5&UvjvX zzaAT2@+?wz&KlyiZ&bO5dpXQ=KJl7+mHv&yuOL3LkJ4X8{8r27YNdZt**MPvkL#}k zP;sf(#1frwb6B1R;L>096Uz8%;%AZG3!ftYi%GvLrSx{3TuFTV8(PkBcJMbWJ+Hc& zPjUYX;U{hKt_YRK%B2)=X`k^gX@x#%es!EHh}S%-^*x&S z=ZJTHPw}Bmc&@282+!Gw}}buLprc3hnd+>D=h>WICZ z6RD5*E^##B`ESmnkC4wUaS-7-K(leaOZu>1tLZr9zl(aZ&G#+D>nR7w-g&>l4eP)^ zUj#1oZJVd{we$T4z@=V0zozB6ob6M8Jjd;)mA;33$>Uo>FGfyd3S zFB%;4@mVd8osUlwZ(FQ5+T*pPe}ecF?hlaXAhz>9P;{j{Q*2MTU(C}4{9|ahZJ~C%3Aps{&d|KR z3%KYrAK-YPTI776{Kp>F_ObH3_6^FXcd6q4$#H%*@vuH~4e?!gC5=4$n13DT)4*li z)>03tCH^Ssrzf->wrO)aFBzOfzK^9aOr-n=Qx2oK%##K#?NiTnvxWF)flE8I(a-!A z;xCfV*v>fynIL|^Nm|Zv>b-V;H31hsli$^Pb+BKu24`a5#}3lha2?UwInF1Dw^8r? zINS3P;8L$Cu4}em4nA4x6@40k$Bo<7z)kzGUTsV|P=)?dgF_yUXbBrxubsrFw`c*+ zCZ8Ww;j5sSgpA=tA{(ZnrfAMDF`J}&&^fn~lt`{FC?(9@PH|ub8 z{t39$cOJ)cEBX8f>8I{j`VSJH55a5t@kPbIK)jiF;Q_@l9n8~ByzO0z-$VRuz$IUY z?QHw&qXx%%v_Qwz2T1=n5WJG__(tWkk$4|);nT+QTYcu8q_5ei{cBlA&u;cbH7zn0cC;Z9o2fJC+@_7?*;Xk=B*q(18{WRq>%nkEgMtm3Tvh#@FL41k^P#f{@8=PN! zAAbfe{a#By&jf!vpeeq6wiz6H8XW+)ksj}XHvQ|}x~kQ4?jt_=Lj@9Shh4zUc%%HZ z<96?6ZHGc=y*q+*YZC`+y5%^u&lTkVX5tgH_pfC+*Ak!ZQ~CkYUj$sn+thx+@pcpGCpo{ClKykVcm7@( zcXK}eyTM81|9}7#{uB3WeHnq^v;dd#?|i?O=Pyi?CBCa!>21H<3S8Ffnte1wn1|+h zkn{!mu@@148o1=U^C{)SZgiHw0ErtXIN=hDuQWKu&j+=f+sS_bxbPptd2x9TB0l$2 ztyewQSqu;J)DdsHPx%~8{A}RT?}d=OeK&C7KN-@0{*`kfqY6eH0ruj~OOAEL^ z%XtBC>BlMB3$`DxC4C$1;@PCX$KcU={h0LQTu1CW_4_LHN4!bP(}q`3%X1Z3t}!^j z_&&Cf-l08W$Kj>KYxmRoZeyBH5HH}EzdUEK{Eq-Pa+Q86OaE(wV_jxDKs_+eQE%3A zj{Q{m97#TB5wGQZIhXWf#O<50@*L=Q0XO|}sLpHaclsXixN$NI3#gGNA^+ad#2vQt zQOx&5gY%2;qYJp17eCeZv3~Ue`ApEikztx!NIyQR1z$k?r=*`gN%3~#FIoIC#cPQ# zZqt0HI3Ml2dl0zP%eh9|)B34iBt90B|1~R=zFqPXlmix-AKT9760iBQ7UT-( zt0dp(({6CAFCjfMN4(}*<F$O+t+LPtv#}qc+I8S&Qv>`cLJAoo8)@O;pyB#`ufoP{R;6h?l)b-@=OzN z`;zAS2=Te6Yroh3PV>Ev_-VxLx!_|s4)egJ-F9+aJDBwECB1!R%(pcg=RV+O+}^vl zq5LQEIb{v&b%JYftB-92F5|HNNTokVKb$Lx*HHhr^W_2HQl9P9qc*cVPm_L{?fehK zXLl(7Uh20{kImBpT*_JVq-J<3%RfNed-DwG|IYB?7vIOY<fSd8d@jQ=wK1JM%03`l6 z`LxmBZp-s4;+;=w`8Sb%2|B{a;m~?|3UTjt0Ji5Ca4ApNk9`a2cRr?s+sOYB;x)Zm zuLD{pTzk&dA?8j_ZpmEd>?-xeK_v%v`*#kon~hTZ#TG) z0sp+7^t*np?b)G{r+d__?e=eTTA}O0~h+XztML7 zAn}dBr5(neQU0gthx1YL*~R!AD_6fp`d+T%SCh|w8TuNho_5pW#20pHfAzAzET4mc zoAJ!`ViD<20xo*{WF8pgA(RO=;Bl$G~ZtOtFK|A zGl&tdY2tn_Zi$@68-y8(ocOz>x*VF&s;E!YyU%lOMQ2; zJ+0i>Z0I33LUQa1;^RM2{=3Nk+r&F>)AqFb!!rgak?-T+v$cJu_tJKr&-PzKe0+b! z`-xvfyq@c+^|yQmxEY_6^H{dbGe!DcxJXT&2J^4uyg+=C^Wq@lbD&7beDPk1LpgaA z@vwcd9Ju5=^;<1xO0#jck-q*`EjXsFdER6BGnD`DHJS5S;IbaQNPWhJZ9Pc(onO`V z=P$&)H}(-f z3d!Ba|Uo}&k6d?E@MBwn|N=?KD`~d$faGhqmLy2?-F+?uhPU5=PCc1 zw=3fxQ2ytL_fj8TO8RSvcV@J{D~aE0aDMT9JWl#?j(?~b=6Rm@uA7z5>xnOgVj%6X z^HFUFZU;JN5}$Ud_p%+X2QK|yAJRiUPrNhaM|vE%wCDKW>UbFD`1w6?=R7UX<1Ein zFno;s582%<#Owb_=?g4RFY&f}6u6XpZnS*PQTh{!e}Q=K2E~shpC17i{h@~YhF1Ul ziJ@m=-^brV0GfKeN$Yhgf9o>1kC1=9o%Hse{G*BA3S8R1jdINDf&WJO=?ArdBU)T% zbxQNCy;b|=aSoKX12^^s`{iKre_gNAJMUHopCo=daLKoqe(aNpUqJeaHz@r+ToS$)Z6#gYCE?PZ=?TV z4e6h-^sUPOeUy8%wn)CwX9;j=pVp9nds!9wH3o;AeBUg?%*y%g#2w1{tC-a-#CK62 zUQYbm#5=#Ne2yUg9P#==#qB=P(S5pqJs8@*9s_RX=|S4>)}J;3+~`H~Re&8w{tp9} z@mWv*#!|NDe;b@czK``8r7xVcw?Qw~59bQvwLE8ZGt>N-czB(fomD>05@qlo%(sR3 z6#btW;#uG_@5VxU;2?0RZ!M0m$+MpHA2EDb2;avy$-m|)ZBLjv=6RlccD|^5&Y7+F zaKDyw;*-JlzZtmXTk{vC|3|J1KO%kDPWd%)=Xc8Ik1XfXt-?S0oCrK_T|3{1(N1SUvVvq;F#&-s@S;MKF}&%9$}ZQ%aD}EC5y$-mHTZi#i2ePR?1zhUe7ApUf zEzgn6xM4u^b^cxXzessCZ18CNUq<@r6SRNdsUObgft&Hxs=zE|=lq!X)H90Pad_wj zn(x?v5@LBZPtxMtzra`XY{MVIzq3ypfXkKhUeb4-uYCSO`mYn;#dyD?h(B#`e(`mMd(lexT^~sQ* ztqfe`&lvXst-g1u!F>hipX0zy`}|1xTf6MUVU-WPA$hnQxbP3_?VSdX^3RcejQeyu zNOB?ZU59DDTn?R&6L-$get9#;!=uD&xFLTE$KgqNEl=AW+VHl&))1d$hku0mUQB!^ z{b|j_Zv!sv5VmW6O8Q>PH)|i&6qJ5CWQT499yhNqF*w$>?*zx~t;ENLNb%X~i+E%yWss`Nj9~Y0}rwUZ~-3_Yh9AZ+oi} zS~>PF#K*p^^T35;p6A5 ze=ojB%QJR`*6RnXZ!2)=mzt2@sYv?XE0unlEN=lW>XC&|az zp!HhBd|w0}*ADgDSl)smD(-`>-6Q5!nh~;xL@rj>l|DH+ueT%Rzh5E$w-FD=#l6?yB=UWHh4kaNa!;N+ zDGpw!LO=Hs9S=J>kJ?Ns$2pPsX3it1spdHixU^?2*X1R|&oMZ^_&&;{-^qO(D<3W+ z-gb%BH?fZ<`a1DlYzMrx%sfw2;r~2vY5(zVt!R<-ufJ5wIl+A#I}T4bxUYcxb1Uh` zsJB49H_wNG%e)@r__X8vXQZ#^{>8PVpZ|B7@2+h+PFOAHG~&GvYXR?Bpb2guUYOMW zs-3U+_lbwk5B{EbC-=+FX1)jiz2@t^ZIT2Q5F3S2<4;Z{zxiZZ*&B%cPvq=XD0hcw@Yf+|wb?#wzqT zRly$xF6DRrS<7$r^TONX^L?MeLH`Hs$6so;&I7=OPc8kl#}NM+@y=CBZ{^9}?@;>j zQO);i{n=>+9#`KDz-7I7DCCDM7<%|azOD>bkk2g^KSAg95yT%MKEe3*16a;qS^R5z z8RSqd04p!oaWY0n-M^Fma^jQI%J?DTcM>1xen&I$#|_RezK@>*mv)=}7uTR!&EEMF z@VNCiafRd5`qJA!*8?~5a7xRwhClp0aFH9kC^zgn`#wW&Q&&DeO+Ibwl+U;M)Axvv zy;t!SEa&WZYWaIVsla~vvvW0YqaTL+avue5=G_BISV#U35}(+6wn0wZTYotAN~Q0m zT@UriJm&xx{?pvAwDl?ypP;|AK>CjwKK$bQ_zn5n_agW3x4G|Pzl6?NHv^aQk2_jG z+YUL>PduUhcm(yLJAh03J2i6*XDc@zG<iXG+`I?k{gAeR!U}pY*jH&sp;M2Jym=wLCVS@W07tB4pPba+Q{UVo=-X z>AA`%N4%cKPe0F_$v*{L z^utWZ|8ODjIQjDd;6`5!jpwf!dhGL#YCBke?DNDYhZSGTe2=(B$3q*(!$HJbflIw= zINqj+zs=Aub;d$|m|K9$IP4u#{&v6YpNZGx72nEs_#^pG59+wJex9?TFi5`Pc;nv! z7ydPP`L{e)->!X+;?vY)FJw8}iHGmMdOz`c+Huw%xs~|%7qx&llh1c8AFeMC5dSUl z3CaztZ!En|%Rj;R%wtLa1>jQtT?cA?tsHm~cr9uCA3rDk#AdCp^?NKCCm+5y-O7g( ziPvmW`oqa=9r40_iu1!M0XO58^I~7pe}MG$r|W!7=x}$QB7K1yYu4V}_j=(IeNHeq z*1_krzBW!^Bk%6#YE zpyR>$j`r8zYxSHr0bk6t{*QM7H}m~pm3}q(e3H2L-uwBQ>YKzJ>f88gp8p^|_L63} zk@%t;wO=M^x7vBM47iNj@Oi^`0N-q`>Cv|RjInkI(r85H~rE+RpHeKlG92gwfusoI8Iy{o<+rGUymC6@NiRM&CIa^Hi z<%bHX9RBLq&}um)8d9gP+qiPYx>RaIDv_vPo$kwa=Zhdp;u-Bp_0*@jm#2CXsh)>L=V>&p*j zB=K5bF9uRK^%T*KUD>{oVktM8O%8Oe-O!zC-Mk6?l`l4SWRl75rc_s^yE*0k*ELW# znD0vurU!HB(oEc&>QB#>+XwTdY^mIt&kdK)&Xu?2hF9fE<@7KH&xJGQYcuT{=<04r zg|hVOvu;@_mB|*$+mbUiQq#(j++fC+oit9c5+27ygTIBEP0Hk`*OXQ0+8dIck)|8y zTz7@Mw1Xnvuc5c1*+(OfgR54Gr~M$}0wQTZglm zjavrLNh^xQ^o|aLS5kEhBgt@jaG7cK#)f1vvFyT;T;ByUU+s?_tp(|m_Mrl1Z`azM zR4!MSD;3jq^`=iHrzpo3q)s%nwwCkVG6ITdm`a-XB#CB`Ca8qXIGM{~n5GiKD_b1O zW^!l)scbUTCGq^0ksVrXbPLLC$9Oz-OKDp%H+(^|zrPBhZQ&4GOW&QtB+H(W-_pIK zRL&0Ben>Qz%nt+kEhY0qqB%rq`*xsnL-VFJE817JbeNAsbH`Z_Qa$O?1zjVUgpb{eR#s*e*#rAB8N^a*eYmF4Na`bO30Q#4RjhpEEaP^5R0Q(sYS+QRJ9uB*Cxn3Bk(erOs)&k zu8XPIZ}kwg>7gu!Y$=&+Se8n)7e+dZ`TpD>`fxNmT<$Jp`#Lg>jjb|&w3z=V3LEMq zEmLAS%HEePl>+V5Y38LLTA40EJ_#Ib(GXJ%Qk~=#k)a*Yv_4zfmV^KgjU%tM+0C6; zw=Bm`&oxH-x+96*8|>7{gcvdfcQruWvThjabt3z2-GK159bV(lvIh@^^8 zn~4!GSTo_?Hnhvu<jy{-ziN0;=VyawB z=gOsyzGQbA|0I)@u3D=@NSSu0N3*Y_o!%Q(~E_+CC@=mD{M@UJ)+(YJ> zLCCf)Tjmq89;?;4!NCx#(HXcbPc*$2T$&P<1#rfq*rp3N!7?d#$FRQzW-Ruy<)+x| z0A>lz2-aM&EVen@+z8c88#gD{r8cctcNXSeLu)mCGEtBB0*MJvv97(dCroXvW=iUf zQLD;Z7f)$qOR6KaqO-#c>b_#RR2~tV$jM}j*{xVS;3X;#rTPXT!r=r+W%8-5gZVA# z!Bhs0gHkFzvfb%}N;n9GIU^=z97PJoLN1jS6E=l~sJO%F7Zp2|85tVdfh4|faP8#E z0n*Ii;7~d@Y|7^u#VZEIsz(Vp7uPi;g1@cG_NPY%%R@P|>AFNcj36XfJu=)U-^GNa zRkRxZ71DN&%5Td{0$IvLpgKtw2_Z77Kf9R}bdX<@M>Zw`jn} zwr=B!p49pkJ!=w8>v!}IrFx*TX9r;~FE87_pa=9rzs7$ek%mjXJS!0u+DTR;YnPva$VARy#h68K?7hZ z2XAqFzzJrmo=8DY!GCD%;q*}3>FYXHwr}2?YOE6(vO!k6k>T8hBUw1<`Y!0`E;kRs z7qLCrGBPYcQaX1-Ae%;$jZAvxk?}Iv(4B9DDKMU^5^zkY|f3S{0juFG!)2N{<^MZCc*E4hoa#Q^v#9TaMb5@`Iz< zbEWnA`yJTF6^-0bM|>Nq$X2`{;&@z@h-tbeFwa0Z^yJk|AVX8#m%$cpS9KDKD5mPF z2IUl4xAtV&3|FsV8H*quMx!XNgF~V_yHzyZ%6RGQ7ipYm_WjtiqmWJZ3qQ|M7jy-} zR!k3y@?17b>&pDda0XiVz<{s{yG(^oKw>9a;}*ei@e_@Krb#5Av_bDSnI^jij59aT zlntovaK1Q%xppyhd{h2-Pgo)`oV`f&=+1PmXsm%2YWy)>)R2&7%!&ku`xZ{(;Zi?x zkp}Ffv4WfjfFu1kGwWlxY`M6+aWAMxx(O%WM?Mhq~Q;5P?3^qsB5Te zmL!qsAQejHwq}Pnr3Xi{NmQq4#paG~f7M@s$`L75aOAb3CT9912SC{aJFwhR-Udfx zrX|%~7N@me1lBdkdIPH6`$bK01MMb4gX3leGLS~2z=dc+zso4i!By3nEs9wz1+Owg zt2^N%wUv1T1IQcJBI7$F)5{y7A}rw-jIfce#6mHQ9T;?_%s!0FNF!kC4`<**E=lFR zp#3S17QaNuqg<%v~QB|5yimB-uEA?f;yl^C4 z?#i2iftKkN7)tOss$l9E&S0T7){cx3KPc_Esk^*f zP1{@^W^B~14Gd99v2D0DZmC^WucfIns_Yh!>7|CQ73Z|C=!R03>|T>-T))F-QJTit zfFTN#!R&BJitsev6U|-H1zWC)bHxeBI5iDSG5wmOj8Ulb80~$s0(zZ;=4fb&Yme45 z;4zU6ilk9}ZBb|!#u-JCJf**~Bk+v(p}3l2ExwRiwv1l zs4o}^M6?o3RF_!Y2#!LbAbJWLdyGXDQXd;OCA)f(o3R75D!I93Ee2>dYf7Mlq=ot* zZTCQ_MSR12Sd|xnIb2?`Jtv!0oKtozR0ej?%GTf|3=|l861RfdWz=VvN*Az1XOcsa zp=TC4v_WTnaL3mCa6?9Y`$D%iP_m$KPb@~KLTkIoedR3aCl^BOSZVNUyKkV z#%EwH<`dP|wV5uld?BFF2{57Z7eS+aEy{O~Tx?V=LNq_U?SFoECaUogK zVVaBG^Gnos!0(%bgoJk^;;Qp6{d74PBgUv%o{BZo5NOSnd>wCd_v=P5TGYpfJ^R)6;8=YHLNl>UW0PQTO3}pwZ;Ysl7cVNqW>8rB zMNX&(NBXBqf3_#kIHF7VLkCS;Nuof-#7|U$y;#gKm8T0jXIj)f$az*Q3550!8Mp2 z4nStS>}G5ZWrzBP3ZZ0Hiuz5BDqGNID>>2o{G>V^soNz=9x?C1?L&#fG2GikC+ z-nvX81(u%+%7!I|j-xI>Nb=nYv{!79L5N0F=1RlxP;^+M6a-!3}`CJIfrF`{a7 ztUHP`xSFS!ej2W@{=m4IHPwW45Na%|U^})8u->Xfjq!d(JQ5;#rTEb0>#Z)2rD|(= zTPj;D=8M=R*7e8R_J+g^jWIK?ea^-`3<_Hg^DA8{;lKf=A`}jPJ~19gzvJ_4^Z^3S zHWm>vg39{VuV80EP0>JP4bzh`9%eh;-~Y>gP`#%8^X7AI&%1-OSj=-7SIv7%gTc&7~q zRY~bhLJ4o-5NKFd@P)#6!h%MxU?+QUOB&%1>f!h2Orxr><17<4VeMmMQg<5jr!(MB zwR@^zW%3oP+W+5HIMGo(-3S}94*WhwXmrEs439_C?O`uvWpG!G=deOvyRRbq>EVj^ zrYD+2O6K!L^cmJb#{AlSdn{Td^zCrDKh?MG0&lD`^2Oh$2;r^75hNPRl`oC-m2?ek ziAYGZqi-DxW?S3F0C;OmAd{d6FciBGaa^Hcp#;sqASp{Vq&U3Hb`W92fcULB&s4=L@(A6V2);G(y@0x=JUTwKS@{ zR@@PKv`T`CUWMx-A-m78^f68cyYg_-iqQb|ME_Is0m^2xDx0cmR@*Z4uWsRa?+ZH7wLHnH+VHSk+{m}6bh6me!!e@q`Xn@ACy z-%PZw%JvoI;GaRdC5%&-qZvfb#$paiij85BND~u)FIu0tA-r>i2s@S@4o~C27MfX! z1EWxuHV#Ave9*Q8FTakXC+k$l$@%+6G*!rkiW;vfl{9a6UQ`zP!G~n`CEpX&S|mm{ zD0a+tqfQKARLtj$P_(kCxSi{z15 zGl}HUW0J@h!U4zN)k8j*--?qGRg`Mm-#&HHAEX)Ra%mc?B+&E4F^-0mnQbJ-+8Nyh z%McbVM;oNTGV?O9>b?Jz0d+y$i?L&mph}%42`|?9ZgwCffY}_7*Rb89+1!0)OH$$$ z`xa73^xXQ?PEo(-jT{6@C|0gO6*4`T2nYTBR@+ZM$w3#@I;-#-Omui z%B-v$5VFk@c2+Hi1Th1h$H|&aT&?kxdunurU!oDCd}V(7#?fqXFwOHA9-Y@M_ycKr zdQhP~ac}sAnu9~^m`oXL) zv%Hf~F;S+Gkm!oS8<=HAIYyw7hSgPZ`LYiFO_t!6k$)!XvWs>p2ZF*>@uF}^aH2?n zt?568L7Kl4ejhBYV7mutqc38NLSMc<)p@3cb9rf7uD|RZwQDke^~)m5EoJ|KeF?h-{Fx9o^v#OTP!1gN70?qT4hWO-|Qg?&hh@Z$o)xHu}egnqq-= zG9?lnHR3CcQ#Vb^&$K;g>@P7svfIt+3=<4%eVQD7&5$D-Dyj%08)p)>Oz2xjVgs-e zm2hSM(Q^}lHM2lnRZXClE-cRM(~vD^X#VOl6W!LVQ&oI;iDvKak@a$m1AIh*>qmUj z(^uC-12T|3^mYW$OZbVd#Co$qq>IJu%E|euQWoI|C=o`Fx;FzOLG~2E)<_)a<8V_j zP`t{`7>W9Feq~v2IZ@Y%r_=gM?H3(Ywr`9>1WCenQOKvHBTYM=-EWVztM0n7e?4>= zaXyF=>qX22#(Pr_3M{8J(FP2SfM5&+eqc3~-*zATA(OBXls5b$MtCe^Fs=$ce{-8z zP7R(jwOyy7PSq|6_wQ609Y`m`i(LIu)eG_UgW9Okxe_lfMGp;xfAmLeGQV?D4Pf?v zrx;Kuu{8aj365aX_`B6Oq-9UAu)|xsHLxPk5HhUUfFVC=#S{wkz1k>+9gQ*vq7%R@ zS90=3FV-=e>t4{L)Jx0AsGUSh`?hS~1rS;#O|nD#Sc!Cxjy}sOVV<(8iW-^51w^gp z>J)T(J zT1p62?AwOpL%1l+1kFI8^midlT98DOQpyIJ-Xeza4>-=Thl#N#arjLpupWCJ!C4Q z6#8OCCIoJrGsRND)CyBtqrZ%Xjak8rBhA=i;nHW3YI)yQ!uWG#cZ^jR`L9%$Lk*1;&^fm9(PVGaTN; zI7b!vR}sSfS@P$VW@yd}_O3LL1NF`GYP19-kNkIv~g=hnQD8n+i|lnj+@mD2%6 zLS+t&plW! z0wd2TfWkqu^Y+Va?BJg19^(5AXc?hlPml62E=0)5K&1s~VjbW@h^iB6hM7H7|5lNP zMMTJF2&I4xC;Nsblp?a+Lc`D%kKVs%v}ACr-MF}69aXF%Bibv?7rE@J@{`X|9<+q~ z^Vqv4FUDEnEo0sRC42Ry@xvfmzLJ^Ojn?^r3j(Ba-qP&P)GUNyO~kVfliMXG5W!Dk zGFcbCY6z-->Gc?*NmUP0L)M7~G@M}~(H_y3mX*YJde&3vdwO15G3J-XRri{$w<#dI z1Hm0uw+**ji+b5~_44P%jpF_pHi~r$MsA)n4ZNEuKD-{V5SZ(10>Zb| z7`d{(Mu)BL*i^30=!X?&3cE*+@fI8##_2+e+ybg*==CR32EX@X_fk?u8<%tw%VNWq)PKgd+ZJvTQfn%(~$D`F-DTmoc_Pjj`tIJ>Q_`(0>JY zpz3(8Sp0ipAH6am_Zo)y8`optL~c~M|(^o!fgTRhZ?fC#40qn#fNoJ`{S zy==v66JvSIpf5`VMr2D@Z*ovwHlQ!lcjK?UC|cHBr8N{CH=yx_TuU!TIn59;-Y=3C z|Ct3~H!GUawW_aZ$p*;Y_$$Dh#objs5SZaiPGYjT@C(}5!c=uCP=;~$gBz8Bj;p7)n-|cbf(!@9q$5tuP+XxLu_@(V zNob*oKo%v)AKV#flQc3h$f6UTSNQdNJp2(Q^{G#)xxK;_yrTpK8b z@%h{;vCudPQY97|ULV{EEw?X}b`19=JNt$Lek7j#(R}sa|Ie>v2sil5*K$x^+NB_% zd7C(-f>DQxIkR6PM`3xLA5siJIfCCE`;#WvFOb%#F8&qD24}%R&tCQkiFgl3sE2ij zS8ibT_w{T2CXYz-i2+%q5yUe(fQ8p<1l&G}eV>rB5oi~`N26QrHBwKFhU&l#7IB@) z)oMN!aCr$ghOa0ggbr(dpSsPe1`O{;_67=OV#v0J4(7?|h{jmR(u*5ShRjWr6$QEC z&u2KH@#otuyQ+7Vh=X{@oONICT?JVsx=EKIjs2j$v)ilhnv7M}i8Y}z+i{@cqCP(MCkbNj8P(q zW^{@U0N>_Gc$+KEMNsLF41^diV5+jef?swYCM82X=>*fY&J|ouk)-NA*LQZ z)ABr%CfE}?+Wl(6ud6TJTM<#i=m?1vm(J6n;JIowembQGA|nUQXn=$}z50B&%2U|g zAzc{^0_itlWHduLKK}<^FodG}E(w=LFft!|qbp~8ICfcy<1_uRaK;T#Pp3Dhe)(k-3Sd;qdeDltw$4HStAV6RO1dpdk zoKHaETZJQx#k}vXRL!e0qxa}QOw4@uoL1h7;Sa{(2 z*V^F4>$H2rHKT1Q7bx3pftfDS4a;9`qeNm=E{)6f%tcDcE<`ZmKLl>-9~tZ{pq$3G z_PxX+s4$s5A?4-|$?{pMuf%;-%%VT>o~xlDSO{^(YG53MJOtvsq^7Y4p*T1iqD=wHiUPK<8FX4 zioV(RH32VsHhyB}G!7#r>@}FF!Q7T&y12vaBLx=wx<=kA&qfIv9pPzUR%CQcH$pMh zy3`o4`1xX&ZvVcZ=u5OjXS{Pp9Jqbnn+2pCY`eJY_k&W4qg@>}Ex1J*8yBYE{bR?} z#O@GBbP*Kt)t(+Tm5Z~Xe3o{!G$xcOd`P{bTXscl_o_lRdL$&S*MxV3sZJ<_$psGr zs@M*yN_JDku07i5I%BM-u@WjHn+U$skkFedZ@UX}ppa@g9d~=;JvB!6!Vwbl(olTC z+wyRgvkV8#(Cc~Y%L|-Mhxu9MY`Uh_e{FF^?#nt=eLup1fi{#M4R7CsC2-I+E2Z^f zZ!|}N8XCMqizdXt>Ynn}B%@v65}D}jiauSFTTF6c;sW9;FVk0{kex?Pnyly{Fu-XiKq_kg;o?jy>cQrpKSMI;|H7T zaudYre9@{;a?BMf4D1lQ%`O7U%niLW0xy#^W@u$%u!o@xRwqlFv|E zJ5&86MsR5iriykWR5W)+#PRKo5$07QN5uPvVUCkY-X*}w&Z^Y57=cXw2j0Oc1+HcU zFkrpnTCgvziQ6d&#|iQMK9$Z2DjFL9=XXSNirP>BfBT09WGjM-OuUoC&{Q(;S^u<0 z0*#kwQvaVd17ilX1ikvN<}vGsEDYkb!a=-<(<(HSW;}Q-Ib!R({hZ@;MGa_|MIX*2rn zItXv+CQ_-gif+oti?bXbdOTv4|IDlbo3b=s$tiaMWEVp5gY4YWy=J; zx+j|^GQgXH+a`9Tn>*QzjvQ!FasEr}PvyOV(wNflHldz9c`e@>9q!f`ffNiMK(&nW z<~zCNg}hUd-cz@xSx>w(Fnj3bkWCzYi*d4RhTV8-Ho9#Sw-Cg6TCIuV@0A(HSjOB6 z?+fjx?W@|pc^9F%D?c|d1l7>I`*1yue8ykN;!T#&Wi4`KtFn)#XSU+kPm%ckl{Ri4 z!n%CmxluoiBiCV6_;a6*(!`8ZyUcJDs>L$Hn~`Nq#mnNj1~z-op^L#JPCH_k?B*m~ zMewMqQcW=os8?F0#BzK4s((LMwD&}de~Wiz8i8R3-i;H+nEt`zuvn$c6PeDD69v|o zrgO<2Q=XadijZH=5}b9tpiigk?)599cIL@FR`tx&Qb`w zI)yg!P1cb;mN{y0vhW0N%kdxKiQX$97Rn3< z_Hax?G75}`;5N4Z%}f3A<|X5>EQ}PhsnJ}qJdz#^IqRlldmH;yY!-E8JU_(~BT_tMIF-YM;INdHRmYIhIdaxa-tzcs_I(lLh1jI<1C)~3lmCUQk<**ry0;nhKYn8^HeiS!#DP5IbRN6Rk3Vd zQEXQ5z#08Y1mW)Dg#%%Qff!-zChFdezTdw4oDJM}cq!Aw8PMg3(IN~o;ehKyfQ_zjo3r6mFXDh!sLI*CAa>hNdX);#|}AhlD6JCyJh zTM0Q(=j9Gqr!KoK)nCNz>ZxrRaI@c#O&_*MxA$cW9KkVn4C7Qsd3+KNUKktYCZTi-Ol-_wg)3mfuz?nN$0+{$W2~JY2sEjid>u zzZ#enaSr}BPb~|~PZ?kS$Ckrl@5CSe^tDW9KYI_bl=@l2ulDo$Q2Mb(&EI}nI?Kg= zx1TqM(vLH}{p{RL{434H3oIc&gWMeFPNuj0 zU+D?$kzy#r+7Q2cLg~vjn$CWnwtp}|IR9^j((gQ4)7#GvXM^eO__63?q4ewCrRnYG zPi1oZo^bxp;3KWfBsWn&~#I` z=x6(b4`u%K zA>5{aV+gnG?B_8|A0l(kWBxls + + + + 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 0000000000000000000000000000000000000000..11daebe610769b13b686297b3a44e21c2ae778f9 GIT binary patch literal 841168 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 z

    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

    _XI60~3 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

    Hcb96ll@i z%3$pfMp}XF2~$S?Z^EB(Z28kjle`z6^Uzq{U4z_QoXfs0_g78cCi`F25yM=U`Ecn{ z>i=5soyxPuQRI(?P=+m+{inT9QLCf#pmnFnu zcQe=h+jn}O9x6s%_Mde2q!B&oqpSBJv*{O`86z50Uwrx~#+a{z3$hzM0YHcKQTSlA z>;5Xm@f-NxqegG@x+KocyAAoXcWFvBiyd|$hf_!JNs{qvZxeMa^)SkQ{}uiIs8fDl z;_0Z%r08bS_hoM_v7ywJ%@5Hor|-^Dd-I1G&&nPuvajr)E)Ajg9njmA?3a zew~2*KK?qL0Nv}|bbsAYI>7?kV3Od1z!G1K$_fppVRQ>fQ>I8SGp4I47C+Y<3 zb*FWLJ6^YRg7DY2Fix+kyLadDKi=`9VXEf@o!~C9LE(u=OPHp#b%NpO1jB8efN%AU zxeqMui9F!%(K^8+u4Y!eXq_O3f2^HQ)~^%rPA&8e)Ct)CN$Ui6*gApGX6poLw596= z*Qe1oKqp9}&wC2=b-+lu!N>^#beJ(0YOW z9`eu83&z=cfqcW!3*_3-3*_3;3y4Q>);HiAf0H;R(F<1FdO;m!hTP&e2L}SWPNoxd zGImNI@KUZ^8|VP?P6qE_D{I?M?2r?5fV)(;Gyd0*SM&_g3p()^icVzd0N+Lj@M;}k z%BSc6_f}2XHcNDhQ_=wWPZ@jlQuqDSaP(*|LO73@e#Bs_E~@T?8LJ#^*)$x>zDN0@w&%QPmAsl zc=wLizfRwmb#Cc)_pf-zDaRu=F8)r>c*{m?b2s8Qw)_&i)(rGXkZ*?Pv9KOD2Im{9 zon${fYtB{HpsALOSS%mm;s9wtZiMk8~#eqwqtC(JXBF_TcZ~XB+&zZ;|IC`t^YC*?Wj}*n5Z_ z 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 z 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 z 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 V18EMVIgsW+ngeML{Eu 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: