summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Plugins/.gitignore1
-rw-r--r--Plugins/MonoGame.Extended/.editorconfig463
-rw-r--r--Plugins/MonoGame.Extended/.gitattributes48
-rw-r--r--Plugins/MonoGame.Extended/.github/FUNDING.yml3
-rw-r--r--Plugins/MonoGame.Extended/.github/workflows/create-release.yml80
-rw-r--r--Plugins/MonoGame.Extended/.github/workflows/pull-request-test.yml32
-rw-r--r--Plugins/MonoGame.Extended/.gitignore29
-rw-r--r--Plugins/MonoGame.Extended/CONTRIBUTING.md70
-rw-r--r--Plugins/MonoGame.Extended/Directory.Build.props46
-rw-r--r--Plugins/MonoGame.Extended/LICENSE25
-rw-r--r--Plugins/MonoGame.Extended/MonoGame.Extended.sln235
-rw-r--r--Plugins/MonoGame.Extended/README.md68
-rw-r--r--Plugins/MonoGame.Extended/benchmarks/MonoGame.Extended.Benchmarks.Collisions/DifferentPoolSizeCollision.cs115
-rw-r--r--Plugins/MonoGame.Extended/benchmarks/MonoGame.Extended.Benchmarks.Collisions/MonoGame.Extended.Benchmarks.Collisions.csproj19
-rw-r--r--Plugins/MonoGame.Extended/benchmarks/MonoGame.Extended.Benchmarks.Collisions/Program.cs5
-rw-r--r--Plugins/MonoGame.Extended/benchmarks/MonoGame.Extended.Benchmarks.Collisions/SpaceAlgorithms.cs104
-rw-r--r--Plugins/MonoGame.Extended/benchmarks/MonoGame.Extended.Benchmarks.Collisions/Utils/Collider.cs29
-rw-r--r--Plugins/MonoGame.Extended/logos/github-social-media-large.pngbin0 -> 52291 bytes
-rw-r--r--Plugins/MonoGame.Extended/logos/logo-banner-1600.pngbin0 -> 26491 bytes
-rw-r--r--Plugins/MonoGame.Extended/logos/logo-banner-800.pngbin0 -> 10682 bytes
-rw-r--r--Plugins/MonoGame.Extended/logos/logo-drop-shadow-512.pngbin0 -> 38466 bytes
-rw-r--r--Plugins/MonoGame.Extended/logos/logo-nuget-128.pngbin0 -> 5163 bytes
-rw-r--r--Plugins/MonoGame.Extended/logos/logo-nuget-32.pngbin0 -> 1632 bytes
-rw-r--r--Plugins/MonoGame.Extended/logos/logo-nuget-400.pngbin0 -> 17856 bytes
-rw-r--r--Plugins/MonoGame.Extended/logos/logo-square-1024.pngbin0 -> 72843 bytes
-rw-r--r--Plugins/MonoGame.Extended/logos/logo-square-128.pngbin0 -> 6736 bytes
-rw-r--r--Plugins/MonoGame.Extended/logos/logo-square-32.pngbin0 -> 1840 bytes
-rw-r--r--Plugins/MonoGame.Extended/logos/logo-square-512.pngbin0 -> 31096 bytes
-rw-r--r--Plugins/MonoGame.Extended/logos/logo-square-64.pngbin0 -> 3451 bytes
-rw-r--r--Plugins/MonoGame.Extended/logos/logo-square.svg73
-rw-r--r--Plugins/MonoGame.Extended/source/Directory.Build.props20
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Animations/MonoGame.Extended.Animations.csproj12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionComponent.cs341
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionEventArgs.cs26
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ICollisionActor.cs27
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ISpaceAlgorithm.cs40
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/Layer.cs39
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/UndefinedLayerException.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/MonoGame.Extended.Collisions.csproj12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTree.cs310
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeData.cs88
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeSpace.cs76
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/SpatialHash.cs79
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/packages.config4
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorAnimation.cs24
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorFile.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorImporter.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessor.cs24
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessorResult.cs21
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorWriter.cs40
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontChar.cs41
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontCommon.cs41
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontFile.cs31
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontImporter.cs22
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontInfo.cs47
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontKerning.cs20
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontPage.cs17
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessor.cs33
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessorResult.cs16
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontWriter.cs52
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterContextExtensions.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterResult.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentItem.cs37
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentLogger.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentWriterExtensions.cs79
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentImporter.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessor.cs31
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessorResult.cs8
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentTypeWriter.cs27
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/MonoGame.Extended.Content.Pipeline.csproj25
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/PathExtensions.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentImporter.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentProcessor.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerJsonImporter.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessor.cs24
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessorResult.cs9
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerWriter.cs45
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/ContentWriterExtensions.cs26
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledContentItem.cs22
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapContentItem.cs12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapImporter.cs108
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapObjectTemplateImporter.cs54
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapProcessor.cs324
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetContentItem.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetImporter.cs60
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetProcessor.cs44
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetWriter.cs145
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapWriter.cs227
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/readme.txt15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Aspect.cs48
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/AspectBuilder.cs63
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/BitArrayExtensions.cs54
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentManager.cs90
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentMapper.cs69
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentType.cs46
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Entity.cs87
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntityManager.cs120
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntitySubscription.cs61
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/MonoGame.Extended.Entities.csproj12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/DrawSystem.cs16
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityDrawSystem.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityProcessingSystem.cs26
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntitySystem.cs51
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityUpdateSystem.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/ISystem.cs9
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/UpdateSystem.cs16
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/World.cs84
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/WorldBuilder.cs26
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher.cs387
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher2D.cs450
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/DefaultEffect.cs203
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/EffectResource.cs119
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/ITextureEffect.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/MatrixChainEffect.cs155
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.dx11.mgfxobin0 -> 4829 bytes
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.fx72
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.ogl.mgfxobin0 -> 4833 bytes
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Macros.fxh60
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/RebuildEffects.bat15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Structures.fxh51
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/FlipFlags.cs13
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder.cs24
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder2D.cs119
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IBatchDrawCallInfo.cs19
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IMatrixChainEffect.cs30
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/MonoGame.Extended.Graphics.csproj28
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/PrimitiveTypeExtensions.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/RenderTarget2DExtensions.cs41
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ControlStyle.cs114
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Box.cs24
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Button.cs92
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Canvas.cs25
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CheckBox.cs65
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ComboBox.cs102
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CompositeControl.cs70
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ContentControl.cs82
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Control.cs271
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ControlCollection.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Dialog.cs41
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/DockPanel.cs104
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Form.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ItemsControl.cs60
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Label.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/LayoutControl.cs40
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ListBox.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ProgressBar.cs62
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/SelectorControl.cs173
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/StackPanel.cs69
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox.cs331
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox2.cs328
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ToggleButton.cs98
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/UniformGrid.cs86
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Cursor.cs11
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Element.cs125
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ElementCollection.cs98
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSpriteBatchRenderer.cs81
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSystem.cs265
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/LayoutHelper.cs65
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Markup/MarkupParser.cs147
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/MonoGame.Extended.Gui.csproj13
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Orientation.cs4
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/PointerEventArgs.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Screen.cs140
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ScreenCollection.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlJsonConverter.cs67
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlStyleJsonConverter.cs89
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiJsonSerializerOptionsProvider.cs38
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiNinePatchRegion2DJsonConverter.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureAtlasJsonConverter.cs31
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureRegionService.cs16
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/HorizontalAlignmentConverter.cs32
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/SkinJsonConverter.cs57
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/VerticalAlignmentConverter.cs32
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Skin.cs221
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Window.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/WindowCollection.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/ExtendedPlayerIndex.cs32
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadEventArgs.cs62
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListener.cs529
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListenerSettings.cs134
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/IInputService.cs13
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListener.cs13
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerComponent.cs37
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerSettings.cs8
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardEventArgs.cs119
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListener.cs104
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListenerSettings.cs21
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardModifiers.cs13
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseEventArgs.cs35
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListener.cs193
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListenerSettings.cs23
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchEventArgs.cs39
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListener.cs57
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListenerSettings.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardExtended.cs22
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardStateExtended.cs60
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MonoGame.Extended.Input.csproj12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseButton.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseExtended.cs34
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseStateExtended.cs149
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/FastRandomExtensions.cs17
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/LineSegment.cs77
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/AgeModifier.cs26
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/CircleContainerModifier.cs52
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleContainerModifier.cs59
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleLoopContainerModifier.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/DragModifier.cs23
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ColorInterpolator.cs13
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/HueInterpolator.cs12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/Interpolator.cs26
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/OpacityInterpolator.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/RotationInterpolator.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ScaleInterpolator.cs12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/VelocityInterpolator12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/LinearGravityModifier.cs23
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Modifier.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/OpacityFastFadeModifier.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/RotationModifier.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityColorModifier.cs36
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityModifier.cs43
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VortexModifier.cs30
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/MonoGame.Extended.Particles.csproj12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Particle.cs24
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleBuffer.cs152
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEffect.cs83
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEmitter.cs222
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleExtensions.cs49
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleModifierExecutionStrategy.cs54
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleReleaseParameters.cs34
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxFillProfile.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxProfile.cs31
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxUniformProfile.cs32
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/CircleProfile.cs24
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/LineProfile.cs17
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/PointProfile.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/Profile.cs68
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/RingProfile.cs32
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/SprayProfile.cs20
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/InterpolatorJsonConverter.cs25
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierExecutionStrategyJsonConverter.cs34
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierJsonConverter.cs25
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ParticleJsonSerializerOptionsProvider.cs34
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ProfileJsonConverter.cs25
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/TimeSpanJsonConverter.cs33
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/ContentReaderExtensions.cs20
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/MonoGame.Extended.Tiled.csproj13
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModel.cs32
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModelBuilder.cs28
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapEffect.cs37
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModel.cs38
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModelBuilder.cs125
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModel.cs41
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModelBuilder.cs126
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapRenderer.cs191
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModel.cs22
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModelBuilder.cs16
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapContent.cs75
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapEllipseContent.cs6
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapGroupLayerContent.cs20
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageContent.cs44
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageLayerContent.cs30
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerContent.cs61
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerModelContent.cs139
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectContent.cs78
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectDrawOrderContent.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectLayerContent.cs28
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectTemplateContent.cs17
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapOrientationContent.cs12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolygonContent.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolylineContent.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPropertyContent.cs28
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerAxisContent.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerIndexContent.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileContent.cs9
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileDrawOrderContent.cs12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerContent.cs30
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataChunkContent.cs26
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataContent.cs33
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileOffsetContent.cs17
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetContent.cs76
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetGridContent.cs16
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileAnimationFrameContent.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileContent.cs40
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMap.cs139
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapEllipseObject.cs17
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapGroupLayer.cs17
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapHelper.cs51
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapImageLayer.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayer.cs26
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayerType.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObject.cs35
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectDrawOrder.cs8
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectLayer.cs20
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectType.cs11
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapOrientation.cs9
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolygonObject.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolylineObject.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapProperties.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPropertyValue.cs32
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapReader.cs229
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapRectangleObject.cs12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTile.cs28
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileDrawOrder.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileFlipFlags.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileLayer.cs66
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileObject.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileset.cs70
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetAnimatedTile.cs46
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetReader.cs150
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTile.cs36
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTileAnimationFrame.cs144
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/ColorTween.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/EasingFunctions.cs120
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearOperations.cs21
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearTween.cs23
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/MonoGame.Extended.Tweening.csproj12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tween.cs184
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenFieldMember.cs44
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenMember.cs37
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenPropertyMember.cs43
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tweener.cs133
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/AnimationComponent.cs30
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFont.cs354
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontExtensions.cs156
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontReader.cs65
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontRegion.cs33
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Camera.cs32
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Bag.cs217
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Deque.cs837
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/DictionaryExtensions.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IObservableCollection.cs26
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IPoolable.cs12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ItemEventArgs.cs20
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/KeyedCollection.cs68
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ListExtensions.cs22
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObjectPool.cs157
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObservableCollection.cs117
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Pool.cs51
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorExtensions.cs85
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorHelper.cs83
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentManagerExtensions.cs51
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentReaderExtensions.cs44
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounter.cs34
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounterComponent.cs27
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/GameComponentCollectionExtensions.cs24
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/GameTimeExtensions.cs12
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/HslColor.cs270
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/IColorable.cs9
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/IEquatableByRef.cs21
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/IMovable.cs9
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/IRectangular.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/IRotatable.cs7
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/IScalable.cs9
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/ISizable.cs8
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/IUpdate.cs9
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Angle.cs224
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/BoundingRectangle.cs579
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/CircleF.cs519
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/EllipseF.cs101
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FastRandom.cs127
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FloatHelper.cs15
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Matrix2.cs1038
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/MatrixExtensions.cs28
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/OrientedRectangle.cs302
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point2.cs380
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point3.cs379
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/PrimitivesHelper.cs143
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RandomExtensions.cs44
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Range.cs84
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Ray2.cs199
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleExtensions.cs71
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleF.cs694
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Segment2.cs317
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeExtensions.cs332
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeF.cs95
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size.cs252
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size2.cs311
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size3.cs289
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Thickness.cs101
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/CyclicalList.cs50
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/IndexableCyclicalLinkedList.cs62
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/LineSegment.cs61
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangle.cs88
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangulator.cs567
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Vertex.cs47
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Vector2Extensions.cs249
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj.DotSettings2
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/OrthographicCamera.cs209
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/GameScreen.cs19
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Screen.cs17
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/ScreenManager.cs78
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/ExpandTransition.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/FadeTransition.cs34
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/Transition.cs58
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/BaseTypeJsonConverter.cs86
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ColorJsonConverter.cs34
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ContentManagerJsonConverter.cs49
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/FloatStringConverter.cs34
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/HslColorJsonConverter.cs33
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentLoader.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentTypeReader.cs16
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/MonoGameJsonSerializerOptionsProvider.cs29
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/NinePatchRegion2DJsonConverter.cs94
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RangeJsonConverter.cs50
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RectangleFJsonConverter.cs31
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Size2JsonConverter.cs45
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/SizeJsonConverter.cs45
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextContentLoader.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegion2DJsonConverter.cs52
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegionService.cs35
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ThicknessJsonConverter.cs31
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Utf8JsonReaderExtensions.cs89
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Vector2JsonConverter.cs46
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polygon.cs179
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polyline.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleDrawableGameComponent.cs47
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleGameComponent.cs80
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/AnimatedSprite.cs50
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Animation.cs86
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/ISpriteBatchDrawable.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Sprite.cs109
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteExtensions.cs36
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheet.cs27
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimation.cs107
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationCycle.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationData.cs21
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationFrame.cs52
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/NinePatchRegion2D.cs84
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlas.cs258
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasExtensions.cs113
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonContentTypeReader.cs41
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonConverter.cs84
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasReader.cs30
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerFile.cs14
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerMeta.cs36
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerPoint.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRectangle.cs24
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRegion.cs33
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerSize.cs18
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureRegion2D.cs54
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/ContinuousClock.cs36
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/CountdownTimer.cs42
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/GameTimer.cs65
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/TimerState.cs10
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/Transform.cs456
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveBatch.cs183
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveDrawing.cs255
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/BoxingViewportAdapter.cs96
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/DefaultViewportAdapter.cs28
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ScalingViewportAdapter.cs27
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ViewportAdapter.cs46
-rw-r--r--Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/WindowViewportAdapter.cs36
-rw-r--r--Plugins/MonoGame.Extended/tests/Directory.Build.props46
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/CollisionComponentTests.cs416
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/Implementation/BasicActor.cs33
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/Implementation/BasicWall.cs20
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/MonoGame.Extended.Collisions.Tests.csproj7
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/QuadTreeTests.cs446
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/SpatialHashTests.cs39
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/packages.config6
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/MonoGame.Extended.Content.Pipeline.Tests.Tiled.csproj24
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/isometric.tmx76
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/isometric_tileset.pngbin0 -> 4599 bytes
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/level01.tmx440
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/template.tx4
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-object-layer.tmx24
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-tileset-base64.tmx11
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-tileset-csv.tmx13
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-tileset-gzip.tmx11
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-tileset-xml.tmx19
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-tileset-zlib.tmx11
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TiledMapImporterProcessorTests.cs205
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/AstridAnimatorImporterTests.cs34
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/AstridAnimatorProcessorTests.cs30
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/MonoGame.Extended.Content.Pipeline.Tests.csproj16
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/TestData/astrid-animator-atlas.json390
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/TestData/astrid-animator.aa15
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/TestData/test-tileset.json93
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/TexturePackerJsonImporterProcessorTests.cs33
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/AspectBuilderTests.cs70
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/AspectTests.cs88
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/BitArrayExtensionsTests.cs20
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/ComponentManagerTests.cs47
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/ComponentMapperTests.cs95
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/ComponentTypeTests.cs20
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/MonoGame.Extended.Entities.Tests.csproj7
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/WorldManagerTests.cs71
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Gui.Tests/Controls/GuiButtonTests.cs122
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Gui.Tests/Controls/GuiControlCollectionTests.cs58
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Gui.Tests/GuiRendererTests.cs34
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Gui.Tests/MonoGame.Extended.Gui.Tests.csproj7
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/AngleTest.cs92
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/AssertExtensions.cs21
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/BitmapFonts/BitmapFontTests.cs79
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Camera2DTests.cs97
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/CollectionAssert.cs14
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Collections/BagTests.cs48
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Collections/DequeTests.cs408
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Content/ContentReaderExtensionsTests.cs23
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/MockGameWindow.cs42
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/MonoGame.Extended.Tests.csproj12
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/OpenTK.dll.config26
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/AssertionModifier.cs25
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/ColourTests.cs126
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/EmitterTests.cs131
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/ParticleBufferTests.cs184
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/Profiles/PointProfileTests.cs33
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/Profiles/RingProfileTests.cs38
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/BoundingRectangleTests.cs396
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/CircleFTests.cs420
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/EllipseFTest.cs38
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/OrientedRectangleTests.cs234
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/Point2Tests.cs356
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/Ray2DTests.cs217
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/RectangleFTests.cs135
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/Segment2DTests.cs251
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/ShapeTests.cs180
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/Size2Tests.cs304
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/RangeTests.cs102
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Serialization/ColorJsonConverterTests.cs66
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Serialization/RectangleFJsonConverterTest.cs36
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Shapes/PolygonFTests.cs87
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Sprites/SpriteSheetAnimationTests.cs910
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Sprites/SpriteTests.cs91
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/TestGame.cs21
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/TestGraphicsDevice.cs12
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/TestHelper.cs27
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/TextureAtlases/TextureAtlasTests.cs236
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/TextureAtlases/TextureRegion2DTests.cs40
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Vector2ExtensionsTests.cs101
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/ViewportAdapters/BoxingViewportAdapterTests.cs41
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/ViewportAdapters/DefaultViewportAdapterTests.cs26
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/WithinDeltaEqualityComparer.cs24
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tiled.Tests/FullMapRendererTest.cs291
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tiled.Tests/MonoGame.Extended.Tiled.Tests.csproj7
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tiled.Tests/TiledTilesetTests.cs130
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tweening.Tests/ColorHandler.cs8
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tweening.Tests/MonoGame.Extended.Tweening.Tests.csproj7
-rw-r--r--Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tweening.Tests/TweenerTests.cs15
-rw-r--r--Plugins/MonoGame.ImGuiNet/.gitignore404
-rw-r--r--Plugins/MonoGame.ImGuiNet/Images/ReadMeBanner.pngbin0 -> 8962 bytes
-rw-r--r--Plugins/MonoGame.ImGuiNet/LICENSE21
-rw-r--r--Plugins/MonoGame.ImGuiNet/MonoGame.ImGuiNet/DrawVertDeclaration.cs29
-rw-r--r--Plugins/MonoGame.ImGuiNet/MonoGame.ImGuiNet/ImGuiRenderer.cs426
-rw-r--r--Plugins/MonoGame.ImGuiNet/MonoGame.ImGuiNet/Monogame.ImGuiNet.csproj57
-rw-r--r--Plugins/MonoGame.ImGuiNet/MonoGame.ImGuiNet/Utils/FilePicker.cs26
-rw-r--r--Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNet.sln31
-rw-r--r--Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/.config/dotnet-tools.json36
-rw-r--r--Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Content/Content.mgcb34
-rw-r--r--Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Content/suzanne.fbxbin0 -> 43756 bytes
-rw-r--r--Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Game1.cs3035
-rw-r--r--Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Icon.bmpbin0 -> 262282 bytes
-rw-r--r--Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Icon.icobin0 -> 147541 bytes
-rw-r--r--Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Monogame.ImGuiNetSamples.csproj33
-rw-r--r--Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Program.cs3
-rw-r--r--Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/app.manifest43
-rw-r--r--Plugins/MonoGame.ImGuiNet/NugetpkgIcon.pngbin0 -> 3417 bytes
-rw-r--r--Plugins/MonoGame.ImGuiNet/README.md39
559 files changed, 46498 insertions, 0 deletions
diff --git a/Plugins/.gitignore b/Plugins/.gitignore
new file mode 100644
index 0000000..1508f45
--- /dev/null
+++ b/Plugins/.gitignore
@@ -0,0 +1 @@
+MonoGame-Samples/ \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/.editorconfig b/Plugins/MonoGame.Extended/.editorconfig
new file mode 100644
index 0000000..e8cef19
--- /dev/null
+++ b/Plugins/MonoGame.Extended/.editorconfig
@@ -0,0 +1,463 @@
+root = true
+
+################################################################################
+### All files
+################################################################################
+[*]
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+file_header_template = Copyright (c) Christopher Whitley. All rights reserved.\nLicensed under the MIT license.\nSee LICENSE file in the project root for full license information.
+
+################################################################################
+### Xml files
+################################################################################
+[*.xml]
+indent_size = 2
+
+################################################################################
+### Yaml files
+################################################################################
+[*.yml]
+indent_size = 2
+
+################################################################################
+### C# files
+################################################################################
+[*.cs]
+
+################################################################################
+###
+###
+### Core EditorConfig Options
+###
+###
+################################################################################
+
+################################################################################
+### Indentation and spacing
+################################################################################
+indent_size = 4
+tab_width = 4
+
+################################################################################
+### New line preferences
+################################################################################
+end_of_line = crlf
+
+###############################################################################3
+###
+###
+### .NET Coding Conventions
+###
+###
+################################################################################
+[*.{cs,vb}]
+
+################################################################################
+### Organize usings
+################################################################################
+dotnet_separate_import_directive_groups = false
+dotnet_sort_system_directives_first = true
+
+################################################################################
+### this. and Me. preferences
+################################################################################
+dotnet_style_qualification_for_event = false:silent
+dotnet_style_qualification_for_field = false:silent
+dotnet_style_qualification_for_method = false:silent
+dotnet_style_qualification_for_property = false:silent
+
+################################################################################
+### Language keywords vs BCL types preferences
+################################################################################
+dotnet_style_predefined_type_for_locals_parameters_members = true:silent
+dotnet_style_predefined_type_for_member_access = true:silent
+
+################################################################################
+### Parentheses preferences
+################################################################################
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
+
+################################################################################
+### Modifier preferences
+################################################################################
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+
+################################################################################
+### Expression-level preferences
+################################################################################
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_prefer_auto_properties = true:suggestion
+dotnet_style_prefer_compound_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_return = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_simplified_interpolation = true:suggestion
+
+################################################################################
+### Field preferences
+################################################################################
+dotnet_style_readonly_field = true:warning
+
+################################################################################
+### Parameter preferences
+################################################################################
+dotnet_code_quality_unused_parameters = all:suggestion
+
+################################################################################
+### Suppression preferences
+################################################################################
+dotnet_remove_unnecessary_suppression_exclusions = none
+
+################################################################################
+###
+###
+### C# Coding Conventions
+###
+###
+################################################################################
+[*.cs]
+
+################################################################################
+### var preferences
+################################################################################
+csharp_style_var_elsewhere = false:silent
+csharp_style_var_for_built_in_types = false:silent
+csharp_style_var_when_type_is_apparent = false:silent
+
+################################################################################
+### Expression-bodied members
+################################################################################
+csharp_style_expression_bodied_accessors = true:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_lambdas = true:suggestion
+csharp_style_expression_bodied_local_functions = false:silent
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
+
+################################################################################
+### Pattern matching preferences
+################################################################################
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_prefer_not_pattern = true:suggestion
+csharp_style_prefer_pattern_matching = true:silent
+csharp_style_prefer_switch_expression = true:suggestion
+
+################################################################################
+### Null-checking preferences
+################################################################################
+csharp_style_conditional_delegate_call = true:suggestion
+
+################################################################################
+### Modifier preferences
+################################################################################
+csharp_prefer_static_local_function = true:warning
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
+
+################################################################################
+### Code-block preferences
+################################################################################
+csharp_prefer_braces = true:silent
+csharp_prefer_simple_using_statement = true:suggestion
+
+################################################################################
+### Expression-level preferences
+################################################################################
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_pattern_local_over_anonymous_function = true:suggestion
+csharp_style_prefer_index_operator = true:suggestion
+csharp_style_prefer_range_operator = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+
+################################################################################
+### 'using' directive preferences
+################################################################################
+csharp_using_directive_placement = outside_namespace:silent
+
+################################################################################
+###
+###
+### C# Formatting Rules
+###
+###
+################################################################################
+
+################################################################################
+### New line preferences
+################################################################################
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_open_brace = all
+csharp_new_line_between_query_expression_clauses = true
+
+################################################################################
+### Indentation preferences
+################################################################################
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+################################################################################
+### Space preferences
+################################################################################
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+################################################################################
+### Wrapping preferences
+################################################################################
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+################################################################################
+###
+###
+### Naming styles
+###
+###
+################################################################################
+[*.{cs,vb}]
+
+################################################################################
+### Naming rules
+################################################################################
+dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces
+dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion
+dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces
+dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase
+
+dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion
+dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters
+dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase
+
+dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods
+dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties
+dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.events_should_be_pascalcase.symbols = events
+dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion
+dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables
+dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase
+
+dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion
+dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants
+dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase
+
+dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion
+dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters
+dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase
+
+dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields
+dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion
+dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields
+dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase
+
+dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion
+dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields
+dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase
+
+dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields
+dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields
+dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields
+dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields
+dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums
+dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions
+dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase
+
+################################################################################
+### Symbol specifications
+################################################################################
+dotnet_naming_symbols.interfaces.applicable_kinds = interface
+dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interfaces.required_modifiers =
+
+dotnet_naming_symbols.enums.applicable_kinds = enum
+dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.enums.required_modifiers =
+
+dotnet_naming_symbols.events.applicable_kinds = event
+dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.events.required_modifiers =
+
+dotnet_naming_symbols.methods.applicable_kinds = method
+dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.methods.required_modifiers =
+
+dotnet_naming_symbols.properties.applicable_kinds = property
+dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.properties.required_modifiers =
+
+dotnet_naming_symbols.public_fields.applicable_kinds = field
+dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
+dotnet_naming_symbols.public_fields.required_modifiers =
+
+dotnet_naming_symbols.private_fields.applicable_kinds = field
+dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_fields.required_modifiers =
+
+dotnet_naming_symbols.private_static_fields.applicable_kinds = field
+dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_static_fields.required_modifiers = static
+
+dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum
+dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types_and_namespaces.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+dotnet_naming_symbols.type_parameters.applicable_kinds = namespace
+dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
+dotnet_naming_symbols.type_parameters.required_modifiers =
+
+dotnet_naming_symbols.private_constant_fields.applicable_kinds = field
+dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_constant_fields.required_modifiers = const
+
+dotnet_naming_symbols.local_variables.applicable_kinds = local
+dotnet_naming_symbols.local_variables.applicable_accessibilities = local
+dotnet_naming_symbols.local_variables.required_modifiers =
+
+dotnet_naming_symbols.local_constants.applicable_kinds = local
+dotnet_naming_symbols.local_constants.applicable_accessibilities = local
+dotnet_naming_symbols.local_constants.required_modifiers = const
+
+dotnet_naming_symbols.parameters.applicable_kinds = parameter
+dotnet_naming_symbols.parameters.applicable_accessibilities = *
+dotnet_naming_symbols.parameters.required_modifiers =
+
+dotnet_naming_symbols.public_constant_fields.applicable_kinds = field
+dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal
+dotnet_naming_symbols.public_constant_fields.required_modifiers = const
+
+dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field
+dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal
+dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static
+
+dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field
+dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static
+
+dotnet_naming_symbols.local_functions.applicable_kinds = local_function
+dotnet_naming_symbols.local_functions.applicable_accessibilities = *
+dotnet_naming_symbols.local_functions.required_modifiers =
+
+################################################################################
+### Naming styles
+################################################################################
+dotnet_naming_style.pascalcase.required_prefix =
+dotnet_naming_style.pascalcase.required_suffix =
+dotnet_naming_style.pascalcase.word_separator =
+dotnet_naming_style.pascalcase.capitalization = pascal_case
+
+dotnet_naming_style.ipascalcase.required_prefix = I
+dotnet_naming_style.ipascalcase.required_suffix =
+dotnet_naming_style.ipascalcase.word_separator =
+dotnet_naming_style.ipascalcase.capitalization = pascal_case
+
+dotnet_naming_style.tpascalcase.required_prefix = T
+dotnet_naming_style.tpascalcase.required_suffix =
+dotnet_naming_style.tpascalcase.word_separator =
+dotnet_naming_style.tpascalcase.capitalization = pascal_case
+
+dotnet_naming_style._camelcase.required_prefix = _
+dotnet_naming_style._camelcase.required_suffix =
+dotnet_naming_style._camelcase.word_separator =
+dotnet_naming_style._camelcase.capitalization = camel_case
+
+dotnet_naming_style.camelcase.required_prefix =
+dotnet_naming_style.camelcase.required_suffix =
+dotnet_naming_style.camelcase.word_separator =
+dotnet_naming_style.camelcase.capitalization = camel_case
+
+dotnet_naming_style.s_camelcase.required_prefix = s_
+dotnet_naming_style.s_camelcase.required_suffix =
+dotnet_naming_style.s_camelcase.word_separator =
+dotnet_naming_style.s_camelcase.capitalization = camel_case
+
+
+################################################################################
+###
+### Warning Supressions
+###
+################################################################################
+[*.{cs,vb}]
diff --git a/Plugins/MonoGame.Extended/.gitattributes b/Plugins/MonoGame.Extended/.gitattributes
new file mode 100644
index 0000000..e9afe0a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/.gitattributes
@@ -0,0 +1,48 @@
+################################################################################
+### Default behavior
+### - Treat as text
+### - Normalize to Unix-style line endings
+################################################################################
+* text eol=lf
+
+################################################################################
+### C# source file behavior
+### - Treat as text
+### - Normalize to Unix-style line endings
+### - Diff as csharp
+################################################################################
+*.cs text eol=lf diff=csharp
+
+################################################################################
+### csproj file behavior
+### - Treat as text
+### - Normalize to Unix-style line endings
+### - Use a union merge when resolving conflicts
+################################################################################
+.csproj text eol=lf merge=union
+
+################################################################################
+### sln file behavior
+### - Treat as text
+### - Normalize to Windows-style line endings
+### - Use a union merge when resolving conflicts
+################################################################################
+*.sln text eol=crlf merge=union
+
+################################################################################
+### image file behavior
+### - Treat as binary
+################################################################################
+*.bmp binary
+*.gif binary
+*.ico binary
+*.jpg binary
+*.jpeg binary
+*.png binary
+*.webp binary
+
+################################################################################
+### MGFXO file behavior
+### - Treat as binary
+################################################################################
+*.mgfxo binary
diff --git a/Plugins/MonoGame.Extended/.github/FUNDING.yml b/Plugins/MonoGame.Extended/.github/FUNDING.yml
new file mode 100644
index 0000000..e6160fa
--- /dev/null
+++ b/Plugins/MonoGame.Extended/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+github:
+ - aristurtledev
+ - craftworkgames
diff --git a/Plugins/MonoGame.Extended/.github/workflows/create-release.yml b/Plugins/MonoGame.Extended/.github/workflows/create-release.yml
new file mode 100644
index 0000000..f42b96e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/.github/workflows/create-release.yml
@@ -0,0 +1,80 @@
+################################################################################
+### Build MonoGame.Extended (Develop)
+### Clones the `develop` branch and performs a build, test, then pack of the
+### Monogame.Extended source code. Once the build job is finished, the deploy
+### job will upload the nupkg files created to the MonoGame.Extended GitHub
+###
+### - Only runs on a push to the `develop` branch
+################################################################################
+name: "Create Release"
+
+on:
+ workflow_dispatch:
+ inputs:
+ prerelease:
+ description: 'Is this a prerelease?'
+ required: true
+ default: 'true'
+ source-feed:
+ description: |
+ Which source feed to publish to?
+ (Valid values are 'NuGet' or 'GitHub')
+ required: true
+ default: GitHub
+
+jobs:
+ build:
+ name: "Build MonoGame.Extended"
+ runs-on: ubuntu-latest
+ env:
+ IS_PRERELEASE: ${{ inputs.prerelease == 'true' }}
+ BUILD_NUMBER: ${{ inputs.prerelease == 'true' && github.run_number || '' }}
+
+ steps:
+ - name: Clone Repository
+ uses: actions/checkout@v4
+
+ - name: Setup Dotnet
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 6.0.x
+
+ - name: Build MonoGame.Extended
+ run: dotnet build --nologo --verbosity minimal --configuration Release
+
+ - name: Test MonoGame.Extended
+ run: dotnet test --nologo --verbosity minimal --configuration Release
+
+ - name: Pack MonoGame.Extended
+ run: dotnet pack --nologo --verbosity minimal --configuration Release
+
+ - name: Upload Artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: build-artifacts
+ path: ./.artifacts/source/package/release/*.nupkg
+
+ deploy:
+ name: "Deploy NuGets"
+ runs-on: ubuntu-latest
+ needs: [ build ]
+ permissions:
+ packages: write
+ contents: write
+
+ steps:
+ - name: "Download Artifacts"
+ uses: actions/download-artifact@v4
+ with:
+ name: build-artifacts
+ path: ./.artifacts
+
+ - name: "Push Packages"
+ env:
+ SOURCE_URL: ${{ inputs.source-feed == 'GitHub' && 'https://nuget.pkg.github.com/craftworkgames/index.json' || inputs.source-feed == 'NuGet' && 'https://api.nuget.org/v3/index.json' }}
+ API_KEY: ${{ inputs.source-feed == 'GitHub' && secrets.GITHUB_TOKEN || inputs.source-feed == 'NuGet' && secrets.NUGET_ACCESS_TOKEN }}
+ run: |
+ PACKAGES=(".artifacts/*.nupkg")
+ for PACKAGE in "${PACKAGES[@]}"; do
+ dotnet nuget push "$PACKAGE" --source "$SOURCE_URL" --skip-duplicate --api-key "$API_KEY"
+ done
diff --git a/Plugins/MonoGame.Extended/.github/workflows/pull-request-test.yml b/Plugins/MonoGame.Extended/.github/workflows/pull-request-test.yml
new file mode 100644
index 0000000..e483e3c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/.github/workflows/pull-request-test.yml
@@ -0,0 +1,32 @@
+################################################################################
+### Pull Request Test
+### Executes tests to ensure that the pull request being submitted is valid.
+### - Only runs on pull requests made to the `develop` branch
+### - Only runs if the pull request was opened or synchronized
+################################################################################
+name: Pull Request Test
+
+on:
+ pull_request:
+ branches:
+ - develop
+ - main
+ types:
+ - opened
+ - synchronize
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Clone Repository
+ uses: actions/checkout@v4
+
+ - name: Setup DotNet
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 6.0.x
+
+ - name: Test MonoGame.Extended
+ run: dotnet test --nologo --verbosity minimal --configuration Release
diff --git a/Plugins/MonoGame.Extended/.gitignore b/Plugins/MonoGame.Extended/.gitignore
new file mode 100644
index 0000000..d64f1ef
--- /dev/null
+++ b/Plugins/MonoGame.Extended/.gitignore
@@ -0,0 +1,29 @@
+################################################################################
+### IDE directories
+################################################################################
+.vs/
+.vscode/
+.idea/
+
+################################################################################
+### User specific files that can be generated by Visual Studio
+################################################################################
+*.suo
+*.user
+
+################################################################################
+### Build and Intermediate directories
+################################################################################
+[Bb]in/
+[Oo]bj/
+
+################################################################################
+### Build artifacts directory
+################################################################################
+.artifacts/
+
+################################################################################
+### OS specific auto generated files
+################################################################################
+.DS_Store
+[Tt]humbs.db
diff --git a/Plugins/MonoGame.Extended/CONTRIBUTING.md b/Plugins/MonoGame.Extended/CONTRIBUTING.md
new file mode 100644
index 0000000..85fdd80
--- /dev/null
+++ b/Plugins/MonoGame.Extended/CONTRIBUTING.md
@@ -0,0 +1,70 @@
+# Contributing to MonoGame.Extended
+
+We're happy that you have chosen to contribute to the MonoGame Extended project.
+
+You are joining a team of dedicated volunteers that are building an extension library to the game framework [MonoGame](http://www.monogame.net/).
+
+Please read this document completely before contributing.
+
+
+## How To Contribute
+
+MonoGame.Extended has a `main` branch for stable releases and a `develop` branch for daily development. New features and fixes are always submitted to the `develop` branch.
+
+If you are looking for ways to help you should start by looking at the [Open Issues](https://github.com/craftworkgames/MonoGame.Extended/issues). Please let us know if you plan to work on an issue so that others are not duplicating work.
+
+The MonoGame.Extended project follows standard [GitHub flow](https://guides.github.com/introduction/flow/index.html). You should learn and be familiar with how to [use Git](https://help.github.com/articles/set-up-git/), how to [create a fork of MonoGame.Extended](https://help.github.com/articles/fork-a-repo/), and how to [submit a Pull Request](https://help.github.com/articles/using-pull-requests/).
+
+After you submit a PR the GitHub actions will trigger and build your changes and verify that all tests pass. Project maintainers and contributors will review your changes and provide constructive feedback to improve your submission.
+
+Once satisfied that your changes are good for MonoGame.Extended we will merge it.
+
+## Quick Guidelines
+
+Here are a few simple rules and suggestions to remember when contributing to MonoGame.Extended.
+
+* :bangbang: **NEVER** commit code that you didn't personally write.
+* :bangbang: **NEVER** use decompiler tools to steal code and submit them as your own work.
+* :bangbang: **NEVER** decompile XNA assemblies and steal Microsoft's copyrighted code.
+* **PLEASE** try keep your PRs focused on a single topic and of a reasonable size or we may ask you to break it up.
+* **PLEASE** be sure to write simple and descriptive commit messages.
+* **DO NOT** surprise us with new APIs or big new features. Open an issue to discuss your ideas first.
+* **DO NOT** reorder type members as it makes it difficult to compare code changes in a PR.
+* **DO** give priority to the existing style of the file you're changing.
+* **DO** try to add to our [unit tests](Source/MonoGame.Extended.Tests) when adding new features or fixing bugs.
+* **DO NOT** send PRs for code style changes or make code changes just for the sake of style.
+* **PLEASE** keep a civil and respectful tone when discussing and reviewing contributions.
+* **PLEASE** tell others about MonoGame.Extended and your contributions via social media.
+
+## Decompiler Tools
+
+We prohibit tools like dotPeek, ILSpy, JustDecompiler, or .NET Reflector which convert compiled assemblies into readable code.
+
+It is **NEVER ACCEPTABLE** to decompile copyrighted assemblies and submit that code to the MonoGame.Extended project.
+
+* It **DOES NOT** matter how much you change the code.
+* It **DOES NOT** matter what country you live in or what your local laws say.
+* It **DOES NOT** matter that XNA is discontinued.
+* It **DOES NOT** matter how small the bit of code you have stolen is.
+* It **DOES NOT** matter what your opinion is of stealing code.
+
+If you did not write the code, you do not have ownership of the code, and you shouldn't submit it to MonoGame.Extended.
+
+If we find a contribution in violation of copyright it will be immediately removed. We will bar that contributor from the MonoGame.Extended project.
+
+
+## Licensing
+
+The MonoGame.Extended project is under the [MIT License](https://opensource.org/licenses/MIT) unless a portion of code is explicitly stated elsewhere. See the [LICENSE](LICENSE) for more details. Third-party libraries used by MonoGame.Extended are under their own licenses, we always seek permission from the original author of those libraries.. Please refer to those libraries for details on the license they use.
+
+We accept contributions in "good faith" that it isn't bound to a conflicting license. By submitting a PR you agree to distribute your work under the MonoGame.Extended license and copyright.
+
+
+## Need More Help?
+
+If you need help, please ask questions in our [Discord](https://discord.gg/xPUEkj9), or you can ask questions in the official [MonoGame Discord](https://discord.gg/monogame).
+
+
+Thanks for reading this guide and helping make MonoGame.Extended great!
+
+ :heart: The MonoGame.Extended Team
diff --git a/Plugins/MonoGame.Extended/Directory.Build.props b/Plugins/MonoGame.Extended/Directory.Build.props
new file mode 100644
index 0000000..52d9ed9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/Directory.Build.props
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project>
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <SolutionDirectory>$(MSBuildThisFileDirectory)</SolutionDirectory>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <MonoGameExtendedVersion>3.9.0</MonoGameExtendedVersion>
+ <IsPrerelease Condition="'$(IS_PRERELEASE)' != ''">-prerelease</IsPrerelease>
+ <BuildNumber Condition="'$(BUILD_NUMBER)' != ''">.$(BUILD_NUMBER)</BuildNumber>
+ <Version>$(MonoGameExtendedVersion)$(IsPrerelease)$(BuildNumber)</Version>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <Authors>craftworkgames and contributors</Authors>
+ <PackageProjectUrl>https://github.com/craftworkgames/MonoGame.Extended</PackageProjectUrl>
+ <RepositoryUrl>https://github.com/craftworkgames/MonoGame.Extended</RepositoryUrl>
+ <RepositoryType>git</RepositoryType>
+ <RepositoryBranch>develop</RepositoryBranch>
+ <NeutralLanguage>en</NeutralLanguage>
+ <PackageIcon>logo-nuget-128.png</PackageIcon>
+ <PackageReadmeFile>README.md</PackageReadmeFile>
+ <PackageLicenseExpression>MIT</PackageLicenseExpression>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <!-- path must be relative to the individual csproj's not this .targets file -->
+ <None Include="../../logos/logo-nuget-128.png" Pack="true" PackagePath="" />
+ <None Include="../../README.md" Pack="true" PackagePath="" />
+ </ItemGroup>
+
+ <!-- Setup Code Analysis using the .editorconfig file -->
+ <PropertyGroup>
+ <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ <AnalysisLevel>latest</AnalysisLevel>
+ <RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
+ <RunAnalyzersDuringLiveAnalysis>true</RunAnalyzersDuringLiveAnalysis>
+ <CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
+ <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
+ <WarningsAsErrors>nullable</WarningsAsErrors>
+ </PropertyGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/LICENSE b/Plugins/MonoGame.Extended/LICENSE
new file mode 100644
index 0000000..1055866
--- /dev/null
+++ b/Plugins/MonoGame.Extended/LICENSE
@@ -0,0 +1,25 @@
+The MIT License (MIT)
+
+Copyright (c) 2015-2024:
+- Dylan Wilson (https://github.com/dylanwilson80)
+- Lucas Girouard-Stranks (https://github.com/lithiumtoast)
+- Christopher Whitley (https://github.com/aristurtledev)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/Plugins/MonoGame.Extended/MonoGame.Extended.sln b/Plugins/MonoGame.Extended/MonoGame.Extended.sln
new file mode 100644
index 0000000..2cfd094
--- /dev/null
+++ b/Plugins/MonoGame.Extended/MonoGame.Extended.sln
@@ -0,0 +1,235 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.28803.156
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "source", "source", "{2BD6F851-7287-4361-85AB-DAE9DF30D932}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended", "source\MonoGame.Extended\MonoGame.Extended.csproj", "{C8717306-E333-418E-86A7-8D9781D7FDAC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Animations", "source\MonoGame.Extended.Animations\MonoGame.Extended.Animations.csproj", "{71A8D50C-DAF1-4D08-99FD-2EACD6EAD5E3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Collisions", "source\MonoGame.Extended.Collisions\MonoGame.Extended.Collisions.csproj", "{7F60B59F-4B84-401F-8A13-AAA3F0B9E4A5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Content.Pipeline", "source\MonoGame.Extended.Content.Pipeline\MonoGame.Extended.Content.Pipeline.csproj", "{4A4066EB-B49A-4E80-B424-9E0BD2147886}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Entities", "source\MonoGame.Extended.Entities\MonoGame.Extended.Entities.csproj", "{CD5C1878-13B6-4FAD-91AE-AEA3C1520E38}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Graphics", "source\MonoGame.Extended.Graphics\MonoGame.Extended.Graphics.csproj", "{56627685-4BA7-44E3-ACA6-F2990DE089F2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Gui", "source\MonoGame.Extended.Gui\MonoGame.Extended.Gui.csproj", "{16E79E22-A41A-47BC-9DC9-287B812A6DD1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Input", "source\MonoGame.Extended.Input\MonoGame.Extended.Input.csproj", "{76B39424-7884-4EEC-A8D5-5126B29941B7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Particles", "source\MonoGame.Extended.Particles\MonoGame.Extended.Particles.csproj", "{BE74938D-9004-4D36-AD50-5C6C141A7180}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Tiled", "source\MonoGame.Extended.Tiled\MonoGame.Extended.Tiled.csproj", "{572C908D-4627-4678-9641-D4950DE43B13}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Tweening", "source\MonoGame.Extended.Tweening\MonoGame.Extended.Tweening.csproj", "{F144A062-4FF3-44EC-9A04-31C654B436B0}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{61C82014-E3A9-4AB3-8705-FBBD5F799BA6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Collisions.Tests", "tests\MonoGame.Extended.Collisions.Tests\MonoGame.Extended.Collisions.Tests.csproj", "{58F88A3C-C09F-4664-A34E-60E89252870A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Content.Pipeline.Tests", "tests\MonoGame.Extended.Content.Pipeline.Tests\MonoGame.Extended.Content.Pipeline.Tests.csproj", "{A127A61A-B26E-4A01-BD4C-9896FDB287AE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Content.Pipeline.Tests.Tiled", "tests\MonoGame.Extended.Content.Pipeline.Tests.Tiled\MonoGame.Extended.Content.Pipeline.Tests.Tiled.csproj", "{0426263A-B8DE-43FC-A91E-21544F61D80E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Entities.Tests", "tests\MonoGame.Extended.Entities.Tests\MonoGame.Extended.Entities.Tests.csproj", "{D3B0E350-B92E-4E49-99A3-68E66CB53F16}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Gui.Tests", "tests\MonoGame.Extended.Gui.Tests\MonoGame.Extended.Gui.Tests.csproj", "{AA52F204-3EE7-40CB-B041-0AB36475DAF0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Tests", "tests\MonoGame.Extended.Tests\MonoGame.Extended.Tests.csproj", "{E1339B07-EB0B-454F-9803-E923843DB5C4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Tiled.Tests", "tests\MonoGame.Extended.Tiled.Tests\MonoGame.Extended.Tiled.Tests.csproj", "{BF0EC593-4BAB-424F-B7E9-BB13EC78A0E5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Tweening.Tests", "tests\MonoGame.Extended.Tweening.Tests\MonoGame.Extended.Tweening.Tests.csproj", "{C2A9982B-7BF7-4243-9E05-CFD31752B6DC}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {C8717306-E333-418E-86A7-8D9781D7FDAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C8717306-E333-418E-86A7-8D9781D7FDAC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C8717306-E333-418E-86A7-8D9781D7FDAC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C8717306-E333-418E-86A7-8D9781D7FDAC}.Debug|x86.Build.0 = Debug|Any CPU
+ {C8717306-E333-418E-86A7-8D9781D7FDAC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C8717306-E333-418E-86A7-8D9781D7FDAC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C8717306-E333-418E-86A7-8D9781D7FDAC}.Release|x86.ActiveCfg = Release|Any CPU
+ {C8717306-E333-418E-86A7-8D9781D7FDAC}.Release|x86.Build.0 = Release|Any CPU
+ {71A8D50C-DAF1-4D08-99FD-2EACD6EAD5E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {71A8D50C-DAF1-4D08-99FD-2EACD6EAD5E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {71A8D50C-DAF1-4D08-99FD-2EACD6EAD5E3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {71A8D50C-DAF1-4D08-99FD-2EACD6EAD5E3}.Debug|x86.Build.0 = Debug|Any CPU
+ {71A8D50C-DAF1-4D08-99FD-2EACD6EAD5E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {71A8D50C-DAF1-4D08-99FD-2EACD6EAD5E3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {71A8D50C-DAF1-4D08-99FD-2EACD6EAD5E3}.Release|x86.ActiveCfg = Release|Any CPU
+ {71A8D50C-DAF1-4D08-99FD-2EACD6EAD5E3}.Release|x86.Build.0 = Release|Any CPU
+ {7F60B59F-4B84-401F-8A13-AAA3F0B9E4A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7F60B59F-4B84-401F-8A13-AAA3F0B9E4A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7F60B59F-4B84-401F-8A13-AAA3F0B9E4A5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7F60B59F-4B84-401F-8A13-AAA3F0B9E4A5}.Debug|x86.Build.0 = Debug|Any CPU
+ {7F60B59F-4B84-401F-8A13-AAA3F0B9E4A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7F60B59F-4B84-401F-8A13-AAA3F0B9E4A5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7F60B59F-4B84-401F-8A13-AAA3F0B9E4A5}.Release|x86.ActiveCfg = Release|Any CPU
+ {7F60B59F-4B84-401F-8A13-AAA3F0B9E4A5}.Release|x86.Build.0 = Release|Any CPU
+ {4A4066EB-B49A-4E80-B424-9E0BD2147886}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4A4066EB-B49A-4E80-B424-9E0BD2147886}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4A4066EB-B49A-4E80-B424-9E0BD2147886}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4A4066EB-B49A-4E80-B424-9E0BD2147886}.Debug|x86.Build.0 = Debug|Any CPU
+ {4A4066EB-B49A-4E80-B424-9E0BD2147886}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4A4066EB-B49A-4E80-B424-9E0BD2147886}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4A4066EB-B49A-4E80-B424-9E0BD2147886}.Release|x86.ActiveCfg = Release|Any CPU
+ {4A4066EB-B49A-4E80-B424-9E0BD2147886}.Release|x86.Build.0 = Release|Any CPU
+ {CD5C1878-13B6-4FAD-91AE-AEA3C1520E38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CD5C1878-13B6-4FAD-91AE-AEA3C1520E38}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CD5C1878-13B6-4FAD-91AE-AEA3C1520E38}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CD5C1878-13B6-4FAD-91AE-AEA3C1520E38}.Debug|x86.Build.0 = Debug|Any CPU
+ {CD5C1878-13B6-4FAD-91AE-AEA3C1520E38}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CD5C1878-13B6-4FAD-91AE-AEA3C1520E38}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CD5C1878-13B6-4FAD-91AE-AEA3C1520E38}.Release|x86.ActiveCfg = Release|Any CPU
+ {CD5C1878-13B6-4FAD-91AE-AEA3C1520E38}.Release|x86.Build.0 = Release|Any CPU
+ {56627685-4BA7-44E3-ACA6-F2990DE089F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {56627685-4BA7-44E3-ACA6-F2990DE089F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {56627685-4BA7-44E3-ACA6-F2990DE089F2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {56627685-4BA7-44E3-ACA6-F2990DE089F2}.Debug|x86.Build.0 = Debug|Any CPU
+ {56627685-4BA7-44E3-ACA6-F2990DE089F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {56627685-4BA7-44E3-ACA6-F2990DE089F2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {56627685-4BA7-44E3-ACA6-F2990DE089F2}.Release|x86.ActiveCfg = Release|Any CPU
+ {56627685-4BA7-44E3-ACA6-F2990DE089F2}.Release|x86.Build.0 = Release|Any CPU
+ {16E79E22-A41A-47BC-9DC9-287B812A6DD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {16E79E22-A41A-47BC-9DC9-287B812A6DD1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {16E79E22-A41A-47BC-9DC9-287B812A6DD1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {16E79E22-A41A-47BC-9DC9-287B812A6DD1}.Debug|x86.Build.0 = Debug|Any CPU
+ {16E79E22-A41A-47BC-9DC9-287B812A6DD1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {16E79E22-A41A-47BC-9DC9-287B812A6DD1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {16E79E22-A41A-47BC-9DC9-287B812A6DD1}.Release|x86.ActiveCfg = Release|Any CPU
+ {16E79E22-A41A-47BC-9DC9-287B812A6DD1}.Release|x86.Build.0 = Release|Any CPU
+ {76B39424-7884-4EEC-A8D5-5126B29941B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {76B39424-7884-4EEC-A8D5-5126B29941B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {76B39424-7884-4EEC-A8D5-5126B29941B7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {76B39424-7884-4EEC-A8D5-5126B29941B7}.Debug|x86.Build.0 = Debug|Any CPU
+ {76B39424-7884-4EEC-A8D5-5126B29941B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {76B39424-7884-4EEC-A8D5-5126B29941B7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {76B39424-7884-4EEC-A8D5-5126B29941B7}.Release|x86.ActiveCfg = Release|Any CPU
+ {76B39424-7884-4EEC-A8D5-5126B29941B7}.Release|x86.Build.0 = Release|Any CPU
+ {BE74938D-9004-4D36-AD50-5C6C141A7180}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BE74938D-9004-4D36-AD50-5C6C141A7180}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BE74938D-9004-4D36-AD50-5C6C141A7180}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {BE74938D-9004-4D36-AD50-5C6C141A7180}.Debug|x86.Build.0 = Debug|Any CPU
+ {BE74938D-9004-4D36-AD50-5C6C141A7180}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BE74938D-9004-4D36-AD50-5C6C141A7180}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BE74938D-9004-4D36-AD50-5C6C141A7180}.Release|x86.ActiveCfg = Release|Any CPU
+ {BE74938D-9004-4D36-AD50-5C6C141A7180}.Release|x86.Build.0 = Release|Any CPU
+ {572C908D-4627-4678-9641-D4950DE43B13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {572C908D-4627-4678-9641-D4950DE43B13}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {572C908D-4627-4678-9641-D4950DE43B13}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {572C908D-4627-4678-9641-D4950DE43B13}.Debug|x86.Build.0 = Debug|Any CPU
+ {572C908D-4627-4678-9641-D4950DE43B13}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {572C908D-4627-4678-9641-D4950DE43B13}.Release|Any CPU.Build.0 = Release|Any CPU
+ {572C908D-4627-4678-9641-D4950DE43B13}.Release|x86.ActiveCfg = Release|Any CPU
+ {572C908D-4627-4678-9641-D4950DE43B13}.Release|x86.Build.0 = Release|Any CPU
+ {F144A062-4FF3-44EC-9A04-31C654B436B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F144A062-4FF3-44EC-9A04-31C654B436B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F144A062-4FF3-44EC-9A04-31C654B436B0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F144A062-4FF3-44EC-9A04-31C654B436B0}.Debug|x86.Build.0 = Debug|Any CPU
+ {F144A062-4FF3-44EC-9A04-31C654B436B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F144A062-4FF3-44EC-9A04-31C654B436B0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F144A062-4FF3-44EC-9A04-31C654B436B0}.Release|x86.ActiveCfg = Release|Any CPU
+ {F144A062-4FF3-44EC-9A04-31C654B436B0}.Release|x86.Build.0 = Release|Any CPU
+ {58F88A3C-C09F-4664-A34E-60E89252870A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {58F88A3C-C09F-4664-A34E-60E89252870A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {58F88A3C-C09F-4664-A34E-60E89252870A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {58F88A3C-C09F-4664-A34E-60E89252870A}.Debug|x86.Build.0 = Debug|Any CPU
+ {58F88A3C-C09F-4664-A34E-60E89252870A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {58F88A3C-C09F-4664-A34E-60E89252870A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {58F88A3C-C09F-4664-A34E-60E89252870A}.Release|x86.ActiveCfg = Release|Any CPU
+ {58F88A3C-C09F-4664-A34E-60E89252870A}.Release|x86.Build.0 = Release|Any CPU
+ {A127A61A-B26E-4A01-BD4C-9896FDB287AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A127A61A-B26E-4A01-BD4C-9896FDB287AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A127A61A-B26E-4A01-BD4C-9896FDB287AE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A127A61A-B26E-4A01-BD4C-9896FDB287AE}.Debug|x86.Build.0 = Debug|Any CPU
+ {A127A61A-B26E-4A01-BD4C-9896FDB287AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A127A61A-B26E-4A01-BD4C-9896FDB287AE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A127A61A-B26E-4A01-BD4C-9896FDB287AE}.Release|x86.ActiveCfg = Release|Any CPU
+ {A127A61A-B26E-4A01-BD4C-9896FDB287AE}.Release|x86.Build.0 = Release|Any CPU
+ {0426263A-B8DE-43FC-A91E-21544F61D80E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0426263A-B8DE-43FC-A91E-21544F61D80E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0426263A-B8DE-43FC-A91E-21544F61D80E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0426263A-B8DE-43FC-A91E-21544F61D80E}.Debug|x86.Build.0 = Debug|Any CPU
+ {0426263A-B8DE-43FC-A91E-21544F61D80E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0426263A-B8DE-43FC-A91E-21544F61D80E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0426263A-B8DE-43FC-A91E-21544F61D80E}.Release|x86.ActiveCfg = Release|Any CPU
+ {0426263A-B8DE-43FC-A91E-21544F61D80E}.Release|x86.Build.0 = Release|Any CPU
+ {D3B0E350-B92E-4E49-99A3-68E66CB53F16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D3B0E350-B92E-4E49-99A3-68E66CB53F16}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D3B0E350-B92E-4E49-99A3-68E66CB53F16}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D3B0E350-B92E-4E49-99A3-68E66CB53F16}.Debug|x86.Build.0 = Debug|Any CPU
+ {D3B0E350-B92E-4E49-99A3-68E66CB53F16}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D3B0E350-B92E-4E49-99A3-68E66CB53F16}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D3B0E350-B92E-4E49-99A3-68E66CB53F16}.Release|x86.ActiveCfg = Release|Any CPU
+ {D3B0E350-B92E-4E49-99A3-68E66CB53F16}.Release|x86.Build.0 = Release|Any CPU
+ {AA52F204-3EE7-40CB-B041-0AB36475DAF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AA52F204-3EE7-40CB-B041-0AB36475DAF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AA52F204-3EE7-40CB-B041-0AB36475DAF0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AA52F204-3EE7-40CB-B041-0AB36475DAF0}.Debug|x86.Build.0 = Debug|Any CPU
+ {AA52F204-3EE7-40CB-B041-0AB36475DAF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AA52F204-3EE7-40CB-B041-0AB36475DAF0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AA52F204-3EE7-40CB-B041-0AB36475DAF0}.Release|x86.ActiveCfg = Release|Any CPU
+ {AA52F204-3EE7-40CB-B041-0AB36475DAF0}.Release|x86.Build.0 = Release|Any CPU
+ {E1339B07-EB0B-454F-9803-E923843DB5C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E1339B07-EB0B-454F-9803-E923843DB5C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E1339B07-EB0B-454F-9803-E923843DB5C4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E1339B07-EB0B-454F-9803-E923843DB5C4}.Debug|x86.Build.0 = Debug|Any CPU
+ {E1339B07-EB0B-454F-9803-E923843DB5C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E1339B07-EB0B-454F-9803-E923843DB5C4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E1339B07-EB0B-454F-9803-E923843DB5C4}.Release|x86.ActiveCfg = Release|Any CPU
+ {E1339B07-EB0B-454F-9803-E923843DB5C4}.Release|x86.Build.0 = Release|Any CPU
+ {BF0EC593-4BAB-424F-B7E9-BB13EC78A0E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BF0EC593-4BAB-424F-B7E9-BB13EC78A0E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BF0EC593-4BAB-424F-B7E9-BB13EC78A0E5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {BF0EC593-4BAB-424F-B7E9-BB13EC78A0E5}.Debug|x86.Build.0 = Debug|Any CPU
+ {BF0EC593-4BAB-424F-B7E9-BB13EC78A0E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BF0EC593-4BAB-424F-B7E9-BB13EC78A0E5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BF0EC593-4BAB-424F-B7E9-BB13EC78A0E5}.Release|x86.ActiveCfg = Release|Any CPU
+ {BF0EC593-4BAB-424F-B7E9-BB13EC78A0E5}.Release|x86.Build.0 = Release|Any CPU
+ {C2A9982B-7BF7-4243-9E05-CFD31752B6DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C2A9982B-7BF7-4243-9E05-CFD31752B6DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C2A9982B-7BF7-4243-9E05-CFD31752B6DC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C2A9982B-7BF7-4243-9E05-CFD31752B6DC}.Debug|x86.Build.0 = Debug|Any CPU
+ {C2A9982B-7BF7-4243-9E05-CFD31752B6DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C2A9982B-7BF7-4243-9E05-CFD31752B6DC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C2A9982B-7BF7-4243-9E05-CFD31752B6DC}.Release|x86.ActiveCfg = Release|Any CPU
+ {C2A9982B-7BF7-4243-9E05-CFD31752B6DC}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {C8717306-E333-418E-86A7-8D9781D7FDAC} = {2BD6F851-7287-4361-85AB-DAE9DF30D932}
+ {71A8D50C-DAF1-4D08-99FD-2EACD6EAD5E3} = {2BD6F851-7287-4361-85AB-DAE9DF30D932}
+ {7F60B59F-4B84-401F-8A13-AAA3F0B9E4A5} = {2BD6F851-7287-4361-85AB-DAE9DF30D932}
+ {4A4066EB-B49A-4E80-B424-9E0BD2147886} = {2BD6F851-7287-4361-85AB-DAE9DF30D932}
+ {CD5C1878-13B6-4FAD-91AE-AEA3C1520E38} = {2BD6F851-7287-4361-85AB-DAE9DF30D932}
+ {56627685-4BA7-44E3-ACA6-F2990DE089F2} = {2BD6F851-7287-4361-85AB-DAE9DF30D932}
+ {16E79E22-A41A-47BC-9DC9-287B812A6DD1} = {2BD6F851-7287-4361-85AB-DAE9DF30D932}
+ {76B39424-7884-4EEC-A8D5-5126B29941B7} = {2BD6F851-7287-4361-85AB-DAE9DF30D932}
+ {BE74938D-9004-4D36-AD50-5C6C141A7180} = {2BD6F851-7287-4361-85AB-DAE9DF30D932}
+ {572C908D-4627-4678-9641-D4950DE43B13} = {2BD6F851-7287-4361-85AB-DAE9DF30D932}
+ {F144A062-4FF3-44EC-9A04-31C654B436B0} = {2BD6F851-7287-4361-85AB-DAE9DF30D932}
+ {58F88A3C-C09F-4664-A34E-60E89252870A} = {61C82014-E3A9-4AB3-8705-FBBD5F799BA6}
+ {A127A61A-B26E-4A01-BD4C-9896FDB287AE} = {61C82014-E3A9-4AB3-8705-FBBD5F799BA6}
+ {0426263A-B8DE-43FC-A91E-21544F61D80E} = {61C82014-E3A9-4AB3-8705-FBBD5F799BA6}
+ {D3B0E350-B92E-4E49-99A3-68E66CB53F16} = {61C82014-E3A9-4AB3-8705-FBBD5F799BA6}
+ {AA52F204-3EE7-40CB-B041-0AB36475DAF0} = {61C82014-E3A9-4AB3-8705-FBBD5F799BA6}
+ {E1339B07-EB0B-454F-9803-E923843DB5C4} = {61C82014-E3A9-4AB3-8705-FBBD5F799BA6}
+ {BF0EC593-4BAB-424F-B7E9-BB13EC78A0E5} = {61C82014-E3A9-4AB3-8705-FBBD5F799BA6}
+ {C2A9982B-7BF7-4243-9E05-CFD31752B6DC} = {61C82014-E3A9-4AB3-8705-FBBD5F799BA6}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {8ED5A62D-25EC-4331-9F99-BDD1E10C02A0}
+ EndGlobalSection
+EndGlobal
diff --git a/Plugins/MonoGame.Extended/README.md b/Plugins/MonoGame.Extended/README.md
new file mode 100644
index 0000000..3025376
--- /dev/null
+++ b/Plugins/MonoGame.Extended/README.md
@@ -0,0 +1,68 @@
+> [!NOTE]
+> Hi MonoGame community, MonoGame.Extended is currently being updated to resolve outstanding bug issues, included those surrounding Tiled, and to update for MonoGame 3.8.1.303. Please bear with me as I work through the backlog. You can follow the progress in the [v4.0.0 Milestones](https://github.com/craftworkgames/MonoGame.Extended/milestone/8) page.
+>
+> - AristurtleDev
+
+![MonoGame.Extended Logo](logos/logo-banner-800.png)
+
+# MonoGame.Extended
+
+MonoGame.Extended is a set of utilities (in the form of libraries/tools) to [MonoGame](http://www.monogame.net/) that makes it easier to make games. Choose what you want, the rest stays out of your way. It makes MonoGame more awesome.
+
+[![Build, Test, Deploy](https://github.com/craftworkgames/MonoGame.Extended/workflows/Build,%20Test,%20Deploy/badge.svg?branch=develop)](https://github.com/craftworkgames/MonoGame.Extended/actions?query=workflow%3A%22Build%2C+Test%2C+Deploy%22) [![Docs](https://img.shields.io/badge/Docs-latest-brightgreen.svg?style=flat)](http://www.monogameextended.net/)
+
+## Getting started
+
+Code is distributed as NuGet packages in the form of libraries (`.dll` files). You can easily install the NuGet packages into your existing MonoGame project using the NuGet Package Manager UI in Visual Studio or by using the command line interface (CLI) in a terminal.
+
+**Current Stable Release**
+> [!WARNING]
+> The current stable release is not compatible with MonoGame 3.8.1.303.
+
+```sh
+dotnet add package MonoGame.Extended --version 3.8.0
+```
+
+**Current Prerelease**
+> [!NOTE]
+> Prerelease is based on current `develop` branch snapshot. There it is not considered stable and may contain bugs
+```sh
+dotnet add package MonoGame.Extended --version 3.9.0-prerelease.4
+```
+
+### Using the Content Pipeline Extensions
+To use the content pipeline extensions, you will need to edit your `.mgcb` file to reference the `.dll`. To see an example of how to do this with NuGet see the samples at https://github.com/craftworkgames/MonoGame.Extended-samples. The important pieces are the `NuGet.config` file and the `.mgcb` file.
+
+## Where to next?
+
+- Check out [the samples](https://github.com/craftworkgames/MonoGame.Extended-samples)
+- Join our live [Discord](https://discord.gg/xPUEkj9)
+- Read the [Documentation](http://www.monogameextended.net/docs)
+- Submit an [issue on GitHub](https://github.com/craftworkgames/MonoGame.Extended/issues)
+- Ask a question on [gamedev stack overflow](http://gamedev.stackexchange.com/questions/tagged/monogame-extended)
+- Post on our [MonoGame community forum](http://community.monogame.net/category/extended)
+- Follow development [on Patreon](https://www.patreon.com/craftworkgames)
+
+## News
+
+We're in the process of developing MonoGame.Extended 4.0! Stay tuned.
+
+## Patreon Supporters
+The patreon has been removed. If you would like to support the maintainers of this project, please consider using the GitHub sponsors link for one of the maintainers.
+
+As a special thanks to those that supported this project through Patreon in the past, their websites were linked in this readme and have been preserved below:
+
+- [PRT Studios](http://prt-studios.com/)
+- [optimuspi](http://www.optimuspi.com/)
+
+
+## Special Thanks
+- Matthew-Davey for letting us use the [Mercury Particle Engine](https://github.com/Matthew-Davey/mercury-particle-engine).
+- John McDonald for [2D XNA Primitives](https://bitbucket.org/C3/2d-xna-primitives/wiki/Home)
+- [LibGDX](https://libgdx.badlogicgames.com) for a whole lot of inspiration.
+- [@prime31](https://github.com/prime31) for [`Nez`](https://github.com/prime31/Nez). Both `MonoGame.Extended` and `Nez` are in communication with each other to share ideas.
+- All of our contributors!
+
+## License
+
+MonoGame.Extended is released under the [The MIT License (MIT)](https://github.com/craftworkgames/MonoGame.Extended/blob/master/LICENSE).
diff --git a/Plugins/MonoGame.Extended/benchmarks/MonoGame.Extended.Benchmarks.Collisions/DifferentPoolSizeCollision.cs b/Plugins/MonoGame.Extended/benchmarks/MonoGame.Extended.Benchmarks.Collisions/DifferentPoolSizeCollision.cs
new file mode 100644
index 0000000..a4ac858
--- /dev/null
+++ b/Plugins/MonoGame.Extended/benchmarks/MonoGame.Extended.Benchmarks.Collisions/DifferentPoolSizeCollision.cs
@@ -0,0 +1,115 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Collisions;
+using MonoGame.Extended.Collisions.Layers;
+using MonoGame.Extended.Collisions.QuadTree;
+
+namespace MonoGame.Extended.Benchmarks.Collisions;
+
+[SimpleJob(RunStrategy.ColdStart, launchCount:3)]
+public class DifferentPoolSizeCollision
+{
+ private const int COMPONENT_BOUNDARY_SIZE = 1000;
+
+ private readonly CollisionComponent _collisionComponent;
+ private readonly Random _random = new Random();
+ private readonly GameTime _gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromMilliseconds(16));
+
+ public DifferentPoolSizeCollision()
+ {
+ var size = new Size2(COMPONENT_BOUNDARY_SIZE, COMPONENT_BOUNDARY_SIZE);
+ _collisionComponent = new CollisionComponent(new RectangleF(Point2.Zero, size));
+ }
+
+ class Collider: ICollisionActor
+ {
+ public Collider(Point2 position)
+ {
+ Bounds = new RectangleF(position, new Size2(1, 1));
+ }
+
+ public IShapeF Bounds { get; set; }
+ public Vector2 Shift { get; set; }
+
+ public Point2 Position {
+ get => Bounds.Position;
+ set => Bounds.Position = value;
+ }
+
+ public void OnCollision(CollisionEventArgs collisionInfo)
+ {
+ }
+ }
+
+ [Params(100, 500, 1000)]
+ public int N { get; set; }
+
+
+ [Params(1, 2)]
+ public int LayersCount { get; set; }
+
+ public int UpdateCount { get; set; } = 100;
+
+
+ private List<Collider> _colliders = new();
+ private List<Layer> _layers = new();
+
+ [GlobalSetup]
+ public void GlobalSetup()
+ {
+ if (LayersCount > 1)
+ {
+ for (int i = 0; i < LayersCount; i++)
+ {
+ var size = new Size2(COMPONENT_BOUNDARY_SIZE, COMPONENT_BOUNDARY_SIZE);
+ var layer = new Layer(new SpatialHash(new Size2(5, 5)));//new QuadTreeSpace(new RectangleF(Point2.Zero, size)))));
+ _collisionComponent.Add(i.ToString(), layer);
+ _layers.Add(layer);
+ }
+ for (int i = 0; i < LayersCount - 1; i++)
+ _collisionComponent.AddCollisionBetweenLayer(_layers[i], _layers[i + 1]);
+
+ }
+
+ for (int i = 0; i < N; i++)
+ {
+ var layer = LayersCount == 1
+ ? _collisionComponent.Layers.First().Value
+ : _layers[i % LayersCount];
+
+ var collider = new Collider(new Point2(
+ _random.Next(COMPONENT_BOUNDARY_SIZE),
+ _random.Next(COMPONENT_BOUNDARY_SIZE)))
+ {
+ Shift = new Vector2(
+ _random.Next(4) - 2,
+ _random.Next(4) - 2),
+ };
+ _colliders.Add(collider);
+ layer.Space.Insert(collider);
+ }
+ }
+
+ [GlobalCleanup]
+ public void GlobalCleanup()
+ {
+ foreach (var collider in _colliders)
+ _collisionComponent.Remove(collider);
+ _colliders.Clear();
+ foreach (var layer in _layers)
+ _collisionComponent.Remove(layer: layer);
+ _layers.Clear();
+ }
+
+ [Benchmark]
+ public void Benchmark()
+ {
+ for (int i = 0; i < UpdateCount; i++)
+ {
+ foreach (var collider in _colliders)
+ collider.Position += collider.Shift;
+ //_collisionComponent.Update(_gameTime);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/benchmarks/MonoGame.Extended.Benchmarks.Collisions/MonoGame.Extended.Benchmarks.Collisions.csproj b/Plugins/MonoGame.Extended/benchmarks/MonoGame.Extended.Benchmarks.Collisions/MonoGame.Extended.Benchmarks.Collisions.csproj
new file mode 100644
index 0000000..530bd13
--- /dev/null
+++ b/Plugins/MonoGame.Extended/benchmarks/MonoGame.Extended.Benchmarks.Collisions/MonoGame.Extended.Benchmarks.Collisions.csproj
@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net7.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="BenchmarkDotNet" Version="0.13.10" />
+ <PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.303" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\src\cs\MonoGame.Extended.Collisions\MonoGame.Extended.Collisions.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/benchmarks/MonoGame.Extended.Benchmarks.Collisions/Program.cs b/Plugins/MonoGame.Extended/benchmarks/MonoGame.Extended.Benchmarks.Collisions/Program.cs
new file mode 100644
index 0000000..647c934
--- /dev/null
+++ b/Plugins/MonoGame.Extended/benchmarks/MonoGame.Extended.Benchmarks.Collisions/Program.cs
@@ -0,0 +1,5 @@
+using BenchmarkDotNet.Running;
+using MonoGame.Extended.Benchmarks.Collisions;
+
+//var summary = BenchmarkRunner.Run<DifferentPoolSizeCollision>();
+var summary = BenchmarkRunner.Run<SpaceAlgorithms>();
diff --git a/Plugins/MonoGame.Extended/benchmarks/MonoGame.Extended.Benchmarks.Collisions/SpaceAlgorithms.cs b/Plugins/MonoGame.Extended/benchmarks/MonoGame.Extended.Benchmarks.Collisions/SpaceAlgorithms.cs
new file mode 100644
index 0000000..7c82a32
--- /dev/null
+++ b/Plugins/MonoGame.Extended/benchmarks/MonoGame.Extended.Benchmarks.Collisions/SpaceAlgorithms.cs
@@ -0,0 +1,104 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Benchmarks.Collisions.Utils;
+using MonoGame.Extended.Collisions;
+using MonoGame.Extended.Collisions.Layers;
+using MonoGame.Extended.Collisions.QuadTree;
+
+namespace MonoGame.Extended.Benchmarks.Collisions;
+
+[SimpleJob(RunStrategy.ColdStart, launchCount:10)]
+public class SpaceAlgorithms
+{
+ private const int COMPONENT_BOUNDARY_SIZE = 1000;
+
+ private readonly Random _random = new ();
+ private ISpaceAlgorithm _space;
+ private ICollisionActor _actor;
+ private RectangleF _bound;
+ private List<Collider> _colliders = new();
+
+ [Params(10, 100, 1000)]
+ public int N { get; set; }
+
+ [Params("SpatialHash", "QuadTree")]
+ public string Algorithm { get; set; }
+
+
+ [GlobalSetup]
+ public void GlobalSetup()
+ {
+ var size = new Size2(COMPONENT_BOUNDARY_SIZE, COMPONENT_BOUNDARY_SIZE);
+ _space = Algorithm switch
+ {
+ "SpatialHash" => new SpatialHash(new Size2(32, 32)),
+ "QuadTree" => new QuadTreeSpace(new RectangleF(Point2.Zero, size)),
+ _ => _space
+ };
+ for (int i = 0; i < N; i++)
+ {
+
+ var rect = GetRandomRectangleF();
+ var actor = new Collider(rect);
+ _colliders.Add(actor);
+ _space.Insert(actor);
+ }
+ }
+
+ [GlobalCleanup]
+ public void GlobalCleanup()
+ {
+ foreach (var collider in _colliders)
+ _space.Remove(collider);
+ _colliders.Clear();
+ }
+
+ [GlobalSetup(Targets = new[] { nameof(Insert), nameof(Remove) })]
+ public void ActorGlobalSetup()
+ {
+ GlobalSetup();
+ var rect = GetRandomRectangleF();
+ _actor = new Collider(rect);
+ }
+
+ [Benchmark]
+ public void Insert()
+ {
+ _space.Insert(_actor);
+ }
+
+ [Benchmark]
+ public void Remove()
+ {
+ _space.Remove(_actor);
+ }
+
+ [Benchmark]
+ public void Reset()
+ {
+ _space.Reset();
+ }
+
+ [GlobalSetup(Target = nameof(Query))]
+ public void QueryGlobalSetup()
+ {
+ GlobalSetup();
+ _bound = GetRandomRectangleF();
+ }
+
+ private RectangleF GetRandomRectangleF()
+ {
+ return new RectangleF(
+ _random.Next(COMPONENT_BOUNDARY_SIZE),
+ _random.Next(COMPONENT_BOUNDARY_SIZE),
+ _random.Next(32, 128),
+ _random.Next(32, 128));
+ }
+
+ [Benchmark]
+ public List<ICollisionActor> Query()
+ {
+ return _space.Query(_bound).ToList();
+ }
+}
diff --git a/Plugins/MonoGame.Extended/benchmarks/MonoGame.Extended.Benchmarks.Collisions/Utils/Collider.cs b/Plugins/MonoGame.Extended/benchmarks/MonoGame.Extended.Benchmarks.Collisions/Utils/Collider.cs
new file mode 100644
index 0000000..8005961
--- /dev/null
+++ b/Plugins/MonoGame.Extended/benchmarks/MonoGame.Extended.Benchmarks.Collisions/Utils/Collider.cs
@@ -0,0 +1,29 @@
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Collisions;
+
+namespace MonoGame.Extended.Benchmarks.Collisions.Utils;
+
+public class Collider: ICollisionActor
+{
+ public Collider(Point2 position)
+ {
+ Bounds = new RectangleF(position, new Size2(1, 1));
+ }
+
+ public Collider(IShapeF shape)
+ {
+ Bounds = shape;
+ }
+
+ public IShapeF Bounds { get; set; }
+ public Vector2 Shift { get; set; }
+
+ public Point2 Position {
+ get => Bounds.Position;
+ set => Bounds.Position = value;
+ }
+
+ public void OnCollision(CollisionEventArgs collisionInfo)
+ {
+ }
+}
diff --git a/Plugins/MonoGame.Extended/logos/github-social-media-large.png b/Plugins/MonoGame.Extended/logos/github-social-media-large.png
new file mode 100644
index 0000000..7385d10
--- /dev/null
+++ b/Plugins/MonoGame.Extended/logos/github-social-media-large.png
Binary files differ
diff --git a/Plugins/MonoGame.Extended/logos/logo-banner-1600.png b/Plugins/MonoGame.Extended/logos/logo-banner-1600.png
new file mode 100644
index 0000000..086a3b4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/logos/logo-banner-1600.png
Binary files differ
diff --git a/Plugins/MonoGame.Extended/logos/logo-banner-800.png b/Plugins/MonoGame.Extended/logos/logo-banner-800.png
new file mode 100644
index 0000000..043a4c7
--- /dev/null
+++ b/Plugins/MonoGame.Extended/logos/logo-banner-800.png
Binary files differ
diff --git a/Plugins/MonoGame.Extended/logos/logo-drop-shadow-512.png b/Plugins/MonoGame.Extended/logos/logo-drop-shadow-512.png
new file mode 100644
index 0000000..e29137b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/logos/logo-drop-shadow-512.png
Binary files differ
diff --git a/Plugins/MonoGame.Extended/logos/logo-nuget-128.png b/Plugins/MonoGame.Extended/logos/logo-nuget-128.png
new file mode 100644
index 0000000..6f1cba2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/logos/logo-nuget-128.png
Binary files differ
diff --git a/Plugins/MonoGame.Extended/logos/logo-nuget-32.png b/Plugins/MonoGame.Extended/logos/logo-nuget-32.png
new file mode 100644
index 0000000..2907719
--- /dev/null
+++ b/Plugins/MonoGame.Extended/logos/logo-nuget-32.png
Binary files differ
diff --git a/Plugins/MonoGame.Extended/logos/logo-nuget-400.png b/Plugins/MonoGame.Extended/logos/logo-nuget-400.png
new file mode 100644
index 0000000..03e081a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/logos/logo-nuget-400.png
Binary files differ
diff --git a/Plugins/MonoGame.Extended/logos/logo-square-1024.png b/Plugins/MonoGame.Extended/logos/logo-square-1024.png
new file mode 100644
index 0000000..e5943eb
--- /dev/null
+++ b/Plugins/MonoGame.Extended/logos/logo-square-1024.png
Binary files differ
diff --git a/Plugins/MonoGame.Extended/logos/logo-square-128.png b/Plugins/MonoGame.Extended/logos/logo-square-128.png
new file mode 100644
index 0000000..7d1c9ce
--- /dev/null
+++ b/Plugins/MonoGame.Extended/logos/logo-square-128.png
Binary files differ
diff --git a/Plugins/MonoGame.Extended/logos/logo-square-32.png b/Plugins/MonoGame.Extended/logos/logo-square-32.png
new file mode 100644
index 0000000..26d39a4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/logos/logo-square-32.png
Binary files differ
diff --git a/Plugins/MonoGame.Extended/logos/logo-square-512.png b/Plugins/MonoGame.Extended/logos/logo-square-512.png
new file mode 100644
index 0000000..12c33a8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/logos/logo-square-512.png
Binary files differ
diff --git a/Plugins/MonoGame.Extended/logos/logo-square-64.png b/Plugins/MonoGame.Extended/logos/logo-square-64.png
new file mode 100644
index 0000000..c164dcf
--- /dev/null
+++ b/Plugins/MonoGame.Extended/logos/logo-square-64.png
Binary files differ
diff --git a/Plugins/MonoGame.Extended/logos/logo-square.svg b/Plugins/MonoGame.Extended/logos/logo-square.svg
new file mode 100644
index 0000000..4cba53c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/logos/logo-square.svg
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
+ sodipodi:docname="logo.svg"
+ xml:space="preserve"
+ style="enable-background:new 0 0 512 512;"
+ viewBox="0 0 512 512"
+ y="0px"
+ x="0px"
+ id="Layer_1"
+ version="1.1"><metadata
+ id="metadata17"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs15" /><sodipodi:namedview
+ inkscape:current-layer="Layer_1"
+ inkscape:window-maximized="1"
+ inkscape:window-y="-8"
+ inkscape:window-x="-8"
+ inkscape:cy="160.72423"
+ inkscape:cx="235.24126"
+ inkscape:zoom="1"
+ showgrid="false"
+ id="namedview13"
+ inkscape:window-height="1377"
+ inkscape:window-width="3440"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0"
+ guidetolerance="10"
+ gridtolerance="10"
+ objecttolerance="10"
+ borderopacity="1"
+ bordercolor="#666666"
+ pagecolor="#ffffff" />
+<style
+ id="style2"
+ type="text/css">
+ .st0{fill:#2E8CBC;}
+ .st1{fill:none;stroke:#FFFFFF;stroke-width:56;stroke-miterlimit:10;}
+</style>
+<rect
+ inkscape:export-filename="C:\Users\JanFokke\Desktop\Logo\1024X1024.png"
+ inkscape:export-ydpi="192"
+ inkscape:export-xdpi="192"
+ id="rect4"
+ height="512"
+ width="512"
+ class="st0"
+ y="0" />
+<path
+ id="path6"
+ d="M 258.83968,267.08434 H 77.377193 v -101.6759 c 0,-50.12655 40.604697,-90.731247 90.731247,-90.731247 v 0 c 50.12655,0 90.73124,40.604697 90.73124,90.731247 z"
+ class="st1" />
+<path
+ sodipodi:nodetypes="cssssccccccsc"
+ id="path8"
+ d="m 258.83968,251.43348 v -86.02504 c 0,-50.12655 40.6047,-90.731247 90.73125,-90.731247 v 0 c 50.12655,0 90.73124,40.604697 90.73124,90.731247 v 52.86271 37.10241 l -186.38759,180.68668 -43.23141,0.10953 -42.57473,-0.2093 V 436.07 c -50.12655,0 -90.731247,-40.60477 -90.731247,-90.73133 v -74.53314"
+ class="st1" />
+<line
+ id="line10"
+ y2="447.12402"
+ x2="448.29178"
+ y1="267.08435"
+ x1="258.83969"
+ class="st1" />
+</svg>
diff --git a/Plugins/MonoGame.Extended/source/Directory.Build.props b/Plugins/MonoGame.Extended/source/Directory.Build.props
new file mode 100644
index 0000000..ae50af1
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/Directory.Build.props
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project>
+
+ <Import Project="$(MSBuildThisFileDirectory)..\Directory.Build.props" />
+
+ <PropertyGroup>
+ <ArtifactsPath>$(SolutionDirectory).artifacts/source</ArtifactsPath>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <IsPackable>true</IsPackable>
+ <NoWarn>NU1701</NoWarn>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="MonoGame.Framework.DesktopGL"
+ Version="3.8.1.303"
+ PrivateAssets="All" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Animations/MonoGame.Extended.Animations.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Animations/MonoGame.Extended.Animations.csproj
new file mode 100644
index 0000000..3745741
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Animations/MonoGame.Extended.Animations.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>Animations to make MonoGame more awesome.</Description>
+ <PackageTags>monogame animations spritesheet sprite</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionComponent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionComponent.cs
new file mode 100644
index 0000000..51467d9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionComponent.cs
@@ -0,0 +1,341 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Diagnostics;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Collisions.Layers;
+using MonoGame.Extended.Collisions.QuadTree;
+
+namespace MonoGame.Extended.Collisions
+{
+ /// <summary>
+ /// Handles basic collision between actors.
+ /// When two actors collide, their OnCollision method is called.
+ /// </summary>
+ public class CollisionComponent : SimpleGameComponent
+ {
+ public const string DEFAULT_LAYER_NAME = "default";
+
+ private Dictionary<string, Layer> _layers = new();
+
+ /// <summary>
+ /// List of collision's layers
+ /// </summary>
+ public IReadOnlyDictionary<string, Layer> Layers => _layers;
+
+ private HashSet<(Layer, Layer)> _layerCollision = new();
+
+ /// <summary>
+ /// Creates component with default layer, which is a collision tree covering the specified area (using <see cref="QuadTree"/>.
+ /// </summary>
+ /// <param name="boundary">Boundary of the collision tree.</param>
+ public CollisionComponent(RectangleF boundary)
+ {
+ SetDefaultLayer(new Layer(new QuadTreeSpace(boundary)));
+ }
+
+ /// <summary>
+ /// Creates component with specifies default layer.
+ /// If layer is null, method creates component without default layer.
+ /// </summary>
+ /// <param name="layer">Default layer</param>
+ public CollisionComponent(Layer layer = null)
+ {
+ if (layer is not null)
+ SetDefaultLayer(layer);
+ }
+
+ /// <summary>
+ /// The main layer has the name from <see cref="DEFAULT_LAYER_NAME"/>.
+ /// The main layer collision with itself and all other layers.
+ /// </summary>
+ /// <param name="layer">Layer to set default</param>
+ public void SetDefaultLayer(Layer layer)
+ {
+ if (_layers.ContainsKey(DEFAULT_LAYER_NAME))
+ Remove(DEFAULT_LAYER_NAME);
+ Add(DEFAULT_LAYER_NAME, layer);
+ foreach (var otherLayer in _layers.Values)
+ AddCollisionBetweenLayer(layer, otherLayer);
+ }
+
+ /// <summary>
+ /// Update the collision tree and process collisions.
+ /// </summary>
+ /// <remarks>
+ /// Boundary shapes are updated if they were changed since the last
+ /// update.
+ /// </remarks>
+ /// <param name="gameTime"></param>
+ public override void Update(GameTime gameTime)
+ {
+ foreach (var layer in _layers.Values)
+ layer.Reset();
+
+ foreach (var (firstLayer, secondLayer) in _layerCollision)
+ foreach (var actor in firstLayer.Space)
+ {
+ var collisions = secondLayer.Space.Query(actor.Bounds.BoundingRectangle);
+ foreach (var other in collisions)
+ if (actor != other && actor.Bounds.Intersects(other.Bounds))
+ {
+ var penetrationVector = CalculatePenetrationVector(actor.Bounds, other.Bounds);
+
+ actor.OnCollision(new CollisionEventArgs
+ {
+ Other = other,
+ PenetrationVector = penetrationVector
+ });
+ other.OnCollision(new CollisionEventArgs
+ {
+ Other = actor,
+ PenetrationVector = -penetrationVector
+ });
+ }
+
+ }
+ }
+
+ /// <summary>
+ /// Inserts the target into the collision tree.
+ /// The target will have its OnCollision called when collisions occur.
+ /// </summary>
+ /// <param name="target">Target to insert.</param>
+ public void Insert(ICollisionActor target)
+ {
+ var layerName = target.LayerName ?? DEFAULT_LAYER_NAME;
+ if (!_layers.TryGetValue(layerName, out var layer))
+ {
+ throw new UndefinedLayerException(layerName);
+ }
+
+ layer.Space.Insert(target);
+ }
+
+ /// <summary>
+ /// Removes the target from the collision tree.
+ /// </summary>
+ /// <param name="target">Target to remove.</param>
+ public void Remove(ICollisionActor target)
+ {
+ if (target.LayerName is not null)
+ _layers[target.LayerName].Space.Remove(target);
+ else
+ foreach (var layer in _layers.Values)
+ if (layer.Space.Remove(target))
+ return;
+ }
+
+ #region Layers
+
+ /// <summary>
+ /// Add the new layer. The name of layer must be unique.
+ /// </summary>
+ /// <param name="name">Name of layer</param>
+ /// <param name="layer">The new layer</param>
+ /// <exception cref="ArgumentNullException"><paramref name="name"/> is null</exception>
+ public void Add(string name, Layer layer)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ throw new ArgumentNullException(nameof(name));
+
+ if (!_layers.TryAdd(name, layer))
+ throw new DuplicateNameException(name);
+
+ if (name != DEFAULT_LAYER_NAME)
+ AddCollisionBetweenLayer(_layers[DEFAULT_LAYER_NAME], layer);
+ }
+
+ /// <summary>
+ /// Remove the layer and all layer's collisions.
+ /// </summary>
+ /// <param name="name">The name of the layer to delete</param>
+ /// <param name="layer">The layer to delete</param>
+ public void Remove(string name = null, Layer layer = null)
+ {
+ name ??= _layers.First(x => x.Value == layer).Key;
+ _layers.Remove(name, out layer);
+ _layerCollision.RemoveWhere(tuple => tuple.Item1 == layer || tuple.Item2 == layer);
+ }
+
+ public void AddCollisionBetweenLayer(Layer a, Layer b)
+ {
+ _layerCollision.Add((a, b));
+ }
+
+ public void AddCollisionBetweenLayer(string nameA, string nameB)
+ {
+ _layerCollision.Add((_layers[nameA], _layers[nameB]));
+ }
+
+ #endregion
+
+ #region Penetration Vectors
+
+ /// <summary>
+ /// Calculate a's penetration into b
+ /// </summary>
+ /// <param name="a">The penetrating shape.</param>
+ /// <param name="b">The shape being penetrated.</param>
+ /// <returns>The distance vector from the edge of b to a's Position</returns>
+ private static Vector2 CalculatePenetrationVector(IShapeF a, IShapeF b)
+ {
+ return a switch
+ {
+ CircleF circleA when b is CircleF circleB => PenetrationVector(circleA, circleB),
+ CircleF circleA when b is RectangleF rectangleB => PenetrationVector(circleA, rectangleB),
+ CircleF circleA when b is OrientedRectangle orientedRectangleB => PenetrationVector(circleA, orientedRectangleB),
+
+ RectangleF rectangleA when b is CircleF circleB => PenetrationVector(rectangleA, circleB),
+ RectangleF rectangleA when b is RectangleF rectangleB => PenetrationVector(rectangleA, rectangleB),
+ RectangleF rectangleA when b is OrientedRectangle orientedRectangleB => PenetrationVector(rectangleA, orientedRectangleB),
+
+ OrientedRectangle orientedRectangleA when b is CircleF circleB => PenetrationVector(orientedRectangleA, circleB),
+ OrientedRectangle orientedRectangleA when b is RectangleF rectangleB => PenetrationVector(orientedRectangleA, rectangleB),
+ OrientedRectangle orientedRectangleA when b is OrientedRectangle orientedRectangleB => PenetrationVector(orientedRectangleA, orientedRectangleB),
+
+ _ => throw new ArgumentOutOfRangeException(nameof(a))
+ };
+ }
+
+ private static Vector2 PenetrationVector(CircleF circ1, CircleF circ2)
+ {
+ if (!circ1.Intersects(circ2))
+ {
+ return Vector2.Zero;
+ }
+
+ var displacement = Point2.Displacement(circ1.Center, circ2.Center);
+
+ Vector2 desiredDisplacement;
+ if (displacement != Vector2.Zero)
+ {
+ desiredDisplacement = displacement.NormalizedCopy() * (circ1.Radius + circ2.Radius);
+ }
+ else
+ {
+ desiredDisplacement = -Vector2.UnitY * (circ1.Radius + circ2.Radius);
+ }
+
+
+ var penetration = displacement - desiredDisplacement;
+ return penetration;
+ }
+
+ private static Vector2 PenetrationVector(CircleF circ, RectangleF rect)
+ {
+ var collisionPoint = rect.ClosestPointTo(circ.Center);
+ var cToCollPoint = collisionPoint - circ.Center;
+
+ if (rect.Contains(circ.Center) || cToCollPoint.Equals(Vector2.Zero))
+ {
+ var displacement = Point2.Displacement(circ.Center, rect.Center);
+
+ Vector2 desiredDisplacement;
+ if (displacement != Vector2.Zero)
+ {
+ // Calculate penetration as only in X or Y direction.
+ // Whichever is lower.
+ var dispx = new Vector2(displacement.X, 0);
+ var dispy = new Vector2(0, displacement.Y);
+ dispx.Normalize();
+ dispy.Normalize();
+
+ dispx *= (circ.Radius + rect.Width / 2);
+ dispy *= (circ.Radius + rect.Height / 2);
+
+ if (dispx.LengthSquared() < dispy.LengthSquared())
+ {
+ desiredDisplacement = dispx;
+ displacement.Y = 0;
+ }
+ else
+ {
+ desiredDisplacement = dispy;
+ displacement.X = 0;
+ }
+ }
+ else
+ {
+ desiredDisplacement = -Vector2.UnitY * (circ.Radius + rect.Height / 2);
+ }
+
+ var penetration = displacement - desiredDisplacement;
+ return penetration;
+ }
+ else
+ {
+ var penetration = circ.Radius * cToCollPoint.NormalizedCopy() - cToCollPoint;
+ return penetration;
+ }
+ }
+
+ private static Vector2 PenetrationVector(CircleF circleA, OrientedRectangle orientedRectangleB)
+ {
+ var rotation = Matrix2.CreateRotationZ(orientedRectangleB.Orientation.Rotation);
+ var circleCenterInRectangleSpace = rotation.Transform(circleA.Center - orientedRectangleB.Center);
+ var circleInRectangleSpace = new CircleF(circleCenterInRectangleSpace, circleA.Radius);
+ var boundingRectangle = new BoundingRectangle(new Point2(), orientedRectangleB.Radii);
+
+ var penetrationVector = PenetrationVector(circleInRectangleSpace, boundingRectangle);
+ var inverseRotation = Matrix2.CreateRotationZ(-orientedRectangleB.Orientation.Rotation);
+ var transformedPenetration = inverseRotation.Transform(penetrationVector);
+
+ return transformedPenetration;
+ }
+
+ private static Vector2 PenetrationVector(RectangleF rect, CircleF circ)
+ {
+ return -PenetrationVector(circ, rect);
+ }
+
+ private static Vector2 PenetrationVector(RectangleF rect1, RectangleF rect2)
+ {
+ var intersectingRectangle = RectangleF.Intersection(rect1, rect2);
+ Debug.Assert(!intersectingRectangle.IsEmpty,
+ "Violation of: !intersect.IsEmpty; Rectangles must intersect to calculate a penetration vector.");
+
+ Vector2 penetration;
+ if (intersectingRectangle.Width < intersectingRectangle.Height)
+ {
+ var d = rect1.Center.X < rect2.Center.X
+ ? intersectingRectangle.Width
+ : -intersectingRectangle.Width;
+ penetration = new Vector2(d, 0);
+ }
+ else
+ {
+ var d = rect1.Center.Y < rect2.Center.Y
+ ? intersectingRectangle.Height
+ : -intersectingRectangle.Height;
+ penetration = new Vector2(0, d);
+ }
+
+ return penetration;
+ }
+
+ private static Vector2 PenetrationVector(RectangleF rectangleA, OrientedRectangle orientedRectangleB)
+ {
+ return PenetrationVector((OrientedRectangle)rectangleA, orientedRectangleB);
+ }
+
+ private static Vector2 PenetrationVector(OrientedRectangle orientedRectangleA, CircleF circleB)
+ {
+ return -PenetrationVector(circleB, orientedRectangleA);
+ }
+
+ private static Vector2 PenetrationVector(OrientedRectangle orientedRectangleA, RectangleF rectangleB)
+ {
+ return -PenetrationVector(rectangleB, orientedRectangleA);
+ }
+
+ private static Vector2 PenetrationVector(OrientedRectangle orientedRectangleA, OrientedRectangle orientedRectangleB)
+ {
+ return OrientedRectangle.Intersects(orientedRectangleA, orientedRectangleB)
+ .MinimumTranslationVector;
+ }
+
+ #endregion
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionEventArgs.cs
new file mode 100644
index 0000000..ca401df
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionEventArgs.cs
@@ -0,0 +1,26 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Collisions
+{
+ /// <summary>
+ /// This class holds data on a collision. It is passed as a parameter to
+ /// OnCollision methods.
+ /// </summary>
+ public class CollisionEventArgs : EventArgs
+ {
+ /// <summary>
+ /// Gets the object being collided with.
+ /// </summary>
+ public ICollisionActor Other { get; internal set; }
+
+ /// <summary>
+ /// Gets a vector representing the overlap between the two objects.
+ /// </summary>
+ /// <remarks>
+ /// This vector starts at the edge of <see cref="Other"/> and ends at
+ /// the Actor's location.
+ /// </remarks>
+ public Vector2 PenetrationVector { get; internal set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ICollisionActor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ICollisionActor.cs
new file mode 100644
index 0000000..6a05592
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ICollisionActor.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace MonoGame.Extended.Collisions
+{
+ /// <summary>
+ /// An actor that can be collided with.
+ /// </summary>
+ public interface ICollisionActor
+ {
+ /// <summary>
+ /// A name of layer, which will contains this actor.
+ /// If it equals null, an actor will insert into a default layer
+ /// </summary>
+ string LayerName { get => null; }
+
+ /// <summary>
+ /// A bounds of an actor. It is using for collision calculating
+ /// </summary>
+ IShapeF Bounds { get; }
+
+ /// <summary>
+ /// It will called, when collision with an another actor fires
+ /// </summary>
+ /// <param name="collisionInfo">Data about collision</param>
+ void OnCollision(CollisionEventArgs collisionInfo);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ISpaceAlgorithm.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ISpaceAlgorithm.cs
new file mode 100644
index 0000000..a95f737
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ISpaceAlgorithm.cs
@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Collisions;
+
+/// <summary>
+/// Interface, which split space for optimization of collisions.
+/// </summary>
+public interface ISpaceAlgorithm
+{
+ /// <summary>
+ /// Inserts the actor into the space.
+ /// The actor will have its OnCollision called when collisions occur.
+ /// </summary>
+ /// <param name="actor">Actor to insert.</param>
+ void Insert(ICollisionActor actor);
+
+ /// <summary>
+ /// Removes the actor into the space.
+ /// </summary>
+ /// <param name="actor">Actor to remove.</param>
+ bool Remove(ICollisionActor actor);
+
+ /// <summary>
+ /// Removes the actor into the space.
+ /// The actor will have its OnCollision called when collisions occur.
+ /// </summary>
+ /// <param name="actor">Actor to remove.</param>
+ IEnumerable<ICollisionActor> Query(RectangleF boundsBoundingRectangle);
+
+ /// <summary>
+ /// for foreach
+ /// </summary>
+ /// <returns></returns>
+ List<ICollisionActor>.Enumerator GetEnumerator();
+
+ /// <summary>
+ /// Restructure the space with new positions.
+ /// </summary>
+ void Reset();
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/Layer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/Layer.cs
new file mode 100644
index 0000000..6e97ac8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/Layer.cs
@@ -0,0 +1,39 @@
+using System;
+
+namespace MonoGame.Extended.Collisions.Layers;
+
+/// <summary>
+/// Layer is a group of collision's actors.
+/// </summary>
+public class Layer
+{
+ /// <summary>
+ /// If this property equals true, layer always will reset collision space.
+ /// </summary>
+ public bool IsDynamic { get; set; } = true;
+
+
+ /// <summary>
+ /// The space, which contain actors.
+ /// </summary>
+ public readonly ISpaceAlgorithm Space;
+
+ /// <summary>
+ /// Constructor for layer
+ /// </summary>
+ /// <param name="spaceAlgorithm">A space algorithm for actors</param>
+ /// <exception cref="ArgumentNullException"><paramref name="spaceAlgorithm"/> is null</exception>
+ public Layer(ISpaceAlgorithm spaceAlgorithm)
+ {
+ Space = spaceAlgorithm ?? throw new ArgumentNullException(nameof(spaceAlgorithm));
+ }
+
+ /// <summary>
+ /// Restructure a inner collection, if layer is dynamic, because actors can change own position
+ /// </summary>
+ public virtual void Reset()
+ {
+ if (IsDynamic)
+ Space.Reset();
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/UndefinedLayerException.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/UndefinedLayerException.cs
new file mode 100644
index 0000000..a27b5b6
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/UndefinedLayerException.cs
@@ -0,0 +1,18 @@
+namespace MonoGame.Extended.Collisions.Layers;
+
+using System;
+
+/// <summary>
+/// Thrown when the collision system has no layer defined with the specified name
+/// </summary>
+public class UndefinedLayerException : Exception
+{
+ /// <summary>
+ /// Thrown when the collision system has no layer defined with the specified name
+ /// </summary>
+ /// <param name="layerName">The undefined layer name</param>
+ public UndefinedLayerException(string layerName)
+ : base($"Layer with name '{layerName}' is undefined")
+ {
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/MonoGame.Extended.Collisions.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/MonoGame.Extended.Collisions.csproj
new file mode 100644
index 0000000..bd0729c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/MonoGame.Extended.Collisions.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>Collisions to make MonoGame more awesome.</Description>
+ <PackageTags>monogame collisions</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTree.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTree.cs
new file mode 100644
index 0000000..a46699a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTree.cs
@@ -0,0 +1,310 @@
+using System;
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Collisions.QuadTree
+{
+ /// <summary>
+ /// Class for doing collision handling with a quad tree.
+ /// </summary>
+ public class QuadTree
+ {
+ /// <summary>
+ /// The default maximum depth.
+ /// </summary>
+ public const int DefaultMaxDepth = 7;
+
+ /// <summary>
+ /// The default maximum objects per node.
+ /// </summary>
+ public const int DefaultMaxObjectsPerNode = 25;
+
+ /// <summary>
+ /// Contains the children of this node.
+ /// </summary>
+ protected List<QuadTree> Children = new List<QuadTree>();
+
+ /// <summary>
+ /// Contains the data for this node in the quadtree.
+ /// </summary>
+ protected HashSet<QuadtreeData> Contents = new HashSet<QuadtreeData>();
+
+ /// <summary>
+ /// Creates a quad tree with the given bounds.
+ /// </summary>
+ /// <param name="bounds">The bounds of the new quad tree.</param>
+ public QuadTree(RectangleF bounds)
+ {
+ CurrentDepth = 0;
+ NodeBounds = bounds;
+ }
+
+ /// <summary>
+ /// Gets or sets the current depth for this node in the quadtree.
+ /// </summary>
+ protected int CurrentDepth { get; set; }
+ /// <summary>
+ /// Gets or sets the maximum depth of the quadtree.
+ /// </summary>
+ protected int MaxDepth { get; set; } = DefaultMaxDepth;
+ /// <summary>
+ /// Gets or sets the maximum objects per node in this quadtree.
+ /// </summary>
+ protected int MaxObjectsPerNode { get; set; } = DefaultMaxObjectsPerNode;
+
+ /// <summary>
+ /// Gets the bounds of the area contained in this quad tree.
+ /// </summary>
+ public RectangleF NodeBounds { get; protected set; }
+
+ /// <summary>
+ /// Gets whether the current node is a leaf node.
+ /// </summary>
+ public bool IsLeaf => Children.Count == 0;
+
+ /// <summary>
+ /// Counts the number of unique targets in the current Quadtree.
+ /// </summary>
+ /// <returns>Returns the targets of objects found.</returns>
+ public int NumTargets()
+ {
+ List<QuadtreeData> dirtyItems = new List<QuadtreeData>();
+ var objectCount = 0;
+
+ // Do BFS on nodes to count children.
+ var process = new Queue<QuadTree>();
+ process.Enqueue(this);
+ while (process.Count > 0)
+ {
+ var processing = process.Dequeue();
+ if (!processing.IsLeaf)
+ {
+ foreach (var child in processing.Children)
+ {
+ process.Enqueue(child);
+ }
+ }
+ else
+ {
+ foreach (var data in processing.Contents)
+ {
+ if (data.Dirty == false)
+ {
+ objectCount++;
+ data.MarkDirty();
+ dirtyItems.Add(data);
+ }
+ }
+ }
+ }
+ foreach (var quadtreeData in dirtyItems)
+ {
+ quadtreeData.MarkClean();
+ }
+ return objectCount;
+ }
+
+ /// <summary>
+ /// Inserts the data into the tree.
+ /// </summary>
+ /// <param name="data">Data being inserted.</param>
+ public void Insert(QuadtreeData data)
+ {
+ var actorBounds = data.Bounds;
+
+ // Object doesn't fit into this node.
+ if (!NodeBounds.Intersects(actorBounds))
+ {
+ return;
+ }
+
+ if (IsLeaf && Contents.Count >= MaxObjectsPerNode)
+ {
+ Split();
+ }
+
+ if (IsLeaf)
+ {
+ AddToLeaf(data);
+ }
+ else
+ {
+ foreach (var child in Children)
+ {
+ child.Insert(data);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Removes data from the Quadtree
+ /// </summary>
+ /// <param name="data">The data to be removed.</param>
+ public void Remove(QuadtreeData data)
+ {
+ if (IsLeaf)
+ {
+ data.RemoveParent(this);
+ Contents.Remove(data);
+ }
+ else
+ {
+ throw new InvalidOperationException($"Cannot remove from a non leaf {nameof(QuadTree)}");
+ }
+ }
+
+ /// <summary>
+ /// Removes unnecessary leaf nodes and simplifies the quad tree.
+ /// </summary>
+ public void Shake()
+ {
+ if (IsLeaf)
+ {
+ return;
+ }
+
+ List<QuadtreeData> dirtyItems = new List<QuadtreeData>();
+
+ var numObjects = NumTargets();
+ if (numObjects == 0)
+ {
+ Children.Clear();
+ }
+ else if (numObjects < MaxObjectsPerNode)
+ {
+ var process = new Queue<QuadTree>();
+ process.Enqueue(this);
+ while (process.Count > 0)
+ {
+ var processing = process.Dequeue();
+ if (!processing.IsLeaf)
+ {
+ foreach (var subTree in processing.Children)
+ {
+ process.Enqueue(subTree);
+ }
+ }
+ else
+ {
+ foreach (var data in processing.Contents)
+ {
+ if (data.Dirty == false)
+ {
+ AddToLeaf(data);
+ data.MarkDirty();
+ dirtyItems.Add(data);
+ }
+ }
+ }
+ }
+ Children.Clear();
+ }
+
+ foreach (var quadtreeData in dirtyItems)
+ {
+ quadtreeData.MarkClean();
+ }
+ }
+
+ private void AddToLeaf(QuadtreeData data)
+ {
+ data.AddParent(this);
+ Contents.Add(data);
+ }
+
+ /// <summary>
+ /// Splits a quadtree into quadrants.
+ /// </summary>
+ public void Split()
+ {
+ if (CurrentDepth + 1 >= MaxDepth) return;
+
+ var min = NodeBounds.TopLeft;
+ var max = NodeBounds.BottomRight;
+ var center = NodeBounds.Center;
+
+ RectangleF[] childAreas =
+ {
+ RectangleF.CreateFrom(min, center),
+ RectangleF.CreateFrom(new Point2(center.X, min.Y), new Point2(max.X, center.Y)),
+ RectangleF.CreateFrom(center, max),
+ RectangleF.CreateFrom(new Point2(min.X, center.Y), new Point2(center.X, max.Y))
+ };
+
+ for (var i = 0; i < childAreas.Length; ++i)
+ {
+ var node = new QuadTree(childAreas[i]);
+ Children.Add(node);
+ Children[i].CurrentDepth = CurrentDepth + 1;
+ }
+
+ foreach (QuadtreeData contentQuadtree in Contents)
+ {
+ foreach (QuadTree childQuadtree in Children)
+ {
+ childQuadtree.Insert(contentQuadtree);
+ }
+ }
+ Clear();
+ }
+
+ /// <summary>
+ /// Clear current node and all children
+ /// </summary>
+ public void ClearAll()
+ {
+ foreach (QuadTree childQuadtree in Children)
+ childQuadtree.ClearAll();
+ Clear();
+ }
+
+ private void Clear()
+ {
+ foreach (QuadtreeData quadtreeData in Contents)
+ {
+ quadtreeData.RemoveParent(this);
+ }
+ Contents.Clear();
+ }
+
+ /// <summary>
+ /// Queries the quadtree for targets that intersect with the given area.
+ /// </summary>
+ /// <param name="area">The area to query for overlapping targets</param>
+ /// <returns>A unique list of targets intersected by area.</returns>
+ public List<QuadtreeData> Query(ref RectangleF area)
+ {
+ var recursiveResult = new List<QuadtreeData>();
+ QueryWithoutReset(ref area, recursiveResult);
+ foreach (var quadtreeData in recursiveResult)
+ {
+ quadtreeData.MarkClean();
+ }
+ return recursiveResult;
+ }
+
+ private void QueryWithoutReset(ref RectangleF area, List<QuadtreeData> recursiveResult)
+ {
+ if (!NodeBounds.Intersects(area))
+ return;
+
+ if (IsLeaf)
+ {
+ foreach (QuadtreeData quadtreeData in Contents)
+ {
+ if (quadtreeData.Dirty == false && quadtreeData.Bounds.Intersects(area))
+ {
+ recursiveResult.Add(quadtreeData);
+ quadtreeData.MarkDirty();
+ }
+ }
+ }
+ else
+ {
+ for (int i = 0, size = Children.Count; i < size; i++)
+ {
+ Children[i].QueryWithoutReset(ref area, recursiveResult);
+ }
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeData.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeData.cs
new file mode 100644
index 0000000..db6da3e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeData.cs
@@ -0,0 +1,88 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MonoGame.Extended.Collisions.QuadTree;
+
+/// <summary>
+/// Data structure for the quad tree.
+/// Holds the entity and collision data for it.
+/// </summary>
+public class QuadtreeData
+{
+ private readonly ICollisionActor _target;
+ private readonly HashSet<QuadTree> _parents = new();
+
+ /// <summary>
+ /// Initialize a new instance of QuadTreeData.
+ /// </summary>
+ /// <param name="target"></param>
+ public QuadtreeData(ICollisionActor target)
+ {
+ _target = target;
+ Bounds = _target.Bounds.BoundingRectangle;
+ }
+
+ /// <summary>
+ /// Remove a parent node.
+ /// </summary>
+ /// <param name="parent"></param>
+ public void RemoveParent(QuadTree parent)
+ {
+ _parents.Remove(parent);
+ }
+
+ /// <summary>
+ /// Add a parent node.
+ /// </summary>
+ /// <param name="parent"></param>
+ public void AddParent(QuadTree parent)
+ {
+ _parents.Add(parent);
+ Bounds = _target.Bounds.BoundingRectangle;
+ }
+
+ /// <summary>
+ /// Remove all parent nodes from this node.
+ /// </summary>
+ public void RemoveFromAllParents()
+ {
+ foreach (var parent in _parents.ToList())
+ {
+ parent.Remove(this);
+ }
+
+ _parents.Clear();
+ }
+
+ /// <summary>
+ /// Gets the bounding box for collision detection.
+ /// </summary>
+ public RectangleF Bounds { get; set; }
+
+ /// <summary>
+ /// Gets the collision actor target.
+ /// </summary>
+ public ICollisionActor Target => _target;
+
+ /// <summary>
+ /// Gets or sets whether Target has had its collision handled this
+ /// iteration.
+ /// </summary>
+ public bool Dirty { get; private set; }
+
+ /// <summary>
+ /// Mark node as dirty.
+ /// </summary>
+ public void MarkDirty()
+ {
+ Dirty = true;
+ }
+
+ /// <summary>
+ /// Mark node as clean, i.e. not dirty.
+ /// </summary>
+ public void MarkClean()
+ {
+ Dirty = false;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeSpace.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeSpace.cs
new file mode 100644
index 0000000..3e9625f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeSpace.cs
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MonoGame.Extended.Collisions.QuadTree;
+
+public class QuadTreeSpace: ISpaceAlgorithm
+{
+ private readonly QuadTree _collisionTree;
+ private readonly List<ICollisionActor> _actors = new();
+ private readonly Dictionary<ICollisionActor, QuadtreeData> _targetDataDictionary = new();
+
+ public QuadTreeSpace(RectangleF boundary)
+ {
+ _collisionTree = new QuadTree(boundary);
+ }
+
+ /// <summary>
+ /// Inserts the target into the collision tree.
+ /// The target will have its OnCollision called when collisions occur.
+ /// </summary>
+ /// <param name="target">Target to insert.</param>
+ public void Insert(ICollisionActor target)
+ {
+ if (!_targetDataDictionary.ContainsKey(target))
+ {
+ var data = new QuadtreeData(target);
+ _targetDataDictionary.Add(target, data);
+ _collisionTree.Insert(data);
+ _actors.Add(target);
+ }
+ }
+
+ /// <summary>
+ /// Removes the target from the collision tree.
+ /// </summary>
+ /// <param name="target">Target to remove.</param>
+ public bool Remove(ICollisionActor target)
+ {
+ if (_targetDataDictionary.ContainsKey(target))
+ {
+ var data = _targetDataDictionary[target];
+ data.RemoveFromAllParents();
+ _targetDataDictionary.Remove(target);
+ _collisionTree.Shake();
+ _actors.Remove(target);
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Restructure a inner collection, if layer is dynamic, because actors can change own position
+ /// </summary>
+ public void Reset()
+ {
+ _collisionTree.ClearAll();
+ foreach (var value in _targetDataDictionary.Values)
+ {
+ _collisionTree.Insert(value);
+ }
+ _collisionTree.Shake();
+ }
+
+ /// <summary>
+ /// foreach support
+ /// </summary>
+ /// <returns></returns>
+ public List<ICollisionActor>.Enumerator GetEnumerator() => _actors.GetEnumerator();
+
+ /// <inheritdoc cref="QuadTree.Query"/>
+ public IEnumerable<ICollisionActor> Query(RectangleF boundsBoundingRectangle)
+ {
+ return _collisionTree.Query(ref boundsBoundingRectangle).Select(x => x.Target);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/SpatialHash.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/SpatialHash.cs
new file mode 100644
index 0000000..2b0920e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/SpatialHash.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace MonoGame.Extended.Collisions;
+
+public class SpatialHash: ISpaceAlgorithm
+{
+ private readonly Dictionary<int, List<ICollisionActor>> _dictionary = new();
+ private readonly List<ICollisionActor> _actors = new();
+
+ private readonly Size2 _size;
+
+ public SpatialHash(Size2 size)
+ {
+ _size = size;
+ }
+
+ public void Insert(ICollisionActor actor)
+ {
+ InsertToHash(actor);
+ _actors.Add(actor);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void InsertToHash(ICollisionActor actor)
+ {
+ var rect = actor.Bounds.BoundingRectangle;
+ for (var x = rect.Left; x < rect.Right; x+=_size.Width)
+ for (var y = rect.Top; y < rect.Bottom; y+=_size.Height)
+ AddToCell(x, y, actor);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void AddToCell(float x, float y, ICollisionActor actor)
+ {
+ var index = GetIndex(x, y);
+ if (_dictionary.TryGetValue(index, out var actors))
+ actors.Add(actor);
+ else
+ _dictionary[index] = new() { actor };
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private int GetIndex(float x, float y)
+ {
+ return (int)(x / _size.Width) << 16 + (int)(y / _size.Height);
+ }
+
+ public bool Remove(ICollisionActor actor)
+ {
+ foreach (var actors in _dictionary.Values)
+ actors.Remove(actor);
+ return _actors.Remove(actor);
+ }
+
+ public IEnumerable<ICollisionActor> Query(RectangleF boundsBoundingRectangle)
+ {
+ var results = new HashSet<ICollisionActor>();
+ var bounds = boundsBoundingRectangle.BoundingRectangle;
+
+ for (var x = boundsBoundingRectangle.Left; x < boundsBoundingRectangle.Right; x+=_size.Width)
+ for (var y = boundsBoundingRectangle.Top; y < boundsBoundingRectangle.Bottom; y+=_size.Height)
+ if (_dictionary.TryGetValue(GetIndex(x, y), out var actors))
+ foreach (var actor in actors)
+ if (bounds.Intersects(actor.Bounds))
+ results.Add(actor);
+ return results;
+ }
+
+ public List<ICollisionActor>.Enumerator GetEnumerator() => _actors.GetEnumerator();
+
+ public void Reset()
+ {
+ _dictionary.Clear();
+ foreach (var actor in _actors)
+ InsertToHash(actor);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/packages.config b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/packages.config
new file mode 100644
index 0000000..28c0144
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/packages.config
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="MonoGame.Framework.Portable" version="3.6.0.1625" targetFramework="portable45-net45+win8+wpa81" />
+</packages> \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorAnimation.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorAnimation.cs
new file mode 100644
index 0000000..14638d4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorAnimation.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Content.Pipeline.Animations
+{
+ public class AstridAnimatorAnimation
+ {
+ public string Name { get; set; }
+ public int FramesPerSecond { get; set; }
+ public List<string> Frames { get; set; }
+ public bool IsLooping { get; set; }
+ public bool IsReversed { get; set; }
+ public bool IsPingPong { get; set; }
+
+ public AstridAnimatorAnimation(string name, int framesPerSecond)
+ {
+ Name = name;
+ FramesPerSecond = framesPerSecond;
+ Frames = new List<string>();
+ IsLooping = true;
+ IsReversed = false;
+ IsPingPong = false;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorFile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorFile.cs
new file mode 100644
index 0000000..765f2f2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorFile.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Content.Pipeline.Animations
+{
+ public class AstridAnimatorFile
+ {
+ public string TextureAtlas { get; set; }
+ public List<AstridAnimatorAnimation> Animations { get; set; }
+
+ public AstridAnimatorFile()
+ {
+ Animations = new List<AstridAnimatorAnimation>();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorImporter.cs
new file mode 100644
index 0000000..855a0d3
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorImporter.cs
@@ -0,0 +1,18 @@
+using System.IO;
+using System.Text.Json;
+using Microsoft.Xna.Framework.Content.Pipeline;
+
+namespace MonoGame.Extended.Content.Pipeline.Animations
+{
+ [ContentImporter(".aa", DefaultProcessor = "AstridAnimatorProcessor",
+ DisplayName = "Astrid Animator Importer - MonoGame.Extended")]
+ public class AstridAnimatorImporter : ContentImporter<ContentImporterResult<AstridAnimatorFile>>
+ {
+ public override ContentImporterResult<AstridAnimatorFile> Import(string filename, ContentImporterContext context)
+ {
+ var json = File.ReadAllText(filename);
+ var data = JsonSerializer.Deserialize<AstridAnimatorFile>(json);
+ return new ContentImporterResult<AstridAnimatorFile>(filename, data);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessor.cs
new file mode 100644
index 0000000..e22d65b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessor.cs
@@ -0,0 +1,24 @@
+using System.IO;
+using System.Linq;
+using Microsoft.Xna.Framework.Content.Pipeline;
+
+namespace MonoGame.Extended.Content.Pipeline.Animations
+{
+ [ContentProcessor(DisplayName = "Astrid Animator Processor - MonoGame.Extended")]
+ public class AstridAnimatorProcessor :
+ ContentProcessor<ContentImporterResult<AstridAnimatorFile>, AstridAnimatorProcessorResult>
+ {
+ public override AstridAnimatorProcessorResult Process(ContentImporterResult<AstridAnimatorFile> input,
+ ContentProcessorContext context)
+ {
+ var data = input.Data;
+ var directory = Path.GetDirectoryName(input.FilePath);
+ var frames = data.Animations
+ .SelectMany(i => i.Frames)
+ .OrderBy(f => f)
+ .Distinct();
+
+ return new AstridAnimatorProcessorResult(directory, data, frames);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessorResult.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessorResult.cs
new file mode 100644
index 0000000..de92ec3
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessorResult.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.IO;
+
+namespace MonoGame.Extended.Content.Pipeline.Animations
+{
+ public class AstridAnimatorProcessorResult
+ {
+ public string TextureAtlasAssetName { get; private set; }
+ public string Directory { get; private set; }
+ public AstridAnimatorFile Data { get; private set; }
+ public List<string> Frames { get; private set; }
+
+ public AstridAnimatorProcessorResult(string directory, AstridAnimatorFile data, IEnumerable<string> frames)
+ {
+ Directory = directory;
+ Data = data;
+ Frames = new List<string>(frames);
+ TextureAtlasAssetName = Path.GetFileNameWithoutExtension(data.TextureAtlas);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorWriter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorWriter.cs
new file mode 100644
index 0000000..6f56f3f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorWriter.cs
@@ -0,0 +1,40 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
+
+namespace MonoGame.Extended.Content.Pipeline.Animations
+{
+ [ContentTypeWriter]
+ public class AstridAnimatorWriter : ContentTypeWriter<AstridAnimatorProcessorResult>
+ {
+ public override string GetRuntimeReader(TargetPlatform targetPlatform)
+ {
+ return "MonoGame.Extended.Animations.SpriteSheets.SpriteSheetAnimationFactoryReader, MonoGame.Extended.Animations";
+ }
+
+ protected override void Write(ContentWriter writer, AstridAnimatorProcessorResult input)
+ {
+ var data = input.Data;
+
+ writer.Write(input.TextureAtlasAssetName);
+ writer.Write(input.Frames.Count);
+
+ foreach (var frame in input.Frames)
+ writer.Write(frame);
+
+ writer.Write(data.Animations.Count);
+
+ foreach (var animation in data.Animations)
+ {
+ writer.Write(animation.Name);
+ writer.Write(animation.FramesPerSecond);
+ writer.Write(animation.IsLooping);
+ writer.Write(animation.IsReversed);
+ writer.Write(animation.IsPingPong);
+ writer.Write(animation.Frames.Count);
+
+ foreach (var frame in animation.Frames)
+ writer.Write(input.Frames.IndexOf(frame));
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontChar.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontChar.cs
new file mode 100644
index 0000000..c012da4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontChar.cs
@@ -0,0 +1,41 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.BitmapFonts
+{
+ // ---- AngelCode BmFont XML serializer ----------------------
+ // ---- By DeadlyDan @ deadlydan@gmail.com -------------------
+ // ---- There's no license restrictions, use as you will. ----
+ // ---- Credits to http://www.angelcode.com/ -----------------
+ public class BitmapFontChar
+ {
+ [XmlAttribute("id")]
+ public int Id { get; set; }
+
+ [XmlAttribute("x")]
+ public int X { get; set; }
+
+ [XmlAttribute("y")]
+ public int Y { get; set; }
+
+ [XmlAttribute("width")]
+ public int Width { get; set; }
+
+ [XmlAttribute("height")]
+ public int Height { get; set; }
+
+ [XmlAttribute("xoffset")]
+ public int XOffset { get; set; }
+
+ [XmlAttribute("yoffset")]
+ public int YOffset { get; set; }
+
+ [XmlAttribute("xadvance")]
+ public int XAdvance { get; set; }
+
+ [XmlAttribute("page")]
+ public int Page { get; set; }
+
+ [XmlAttribute("chnl")]
+ public int Channel { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontCommon.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontCommon.cs
new file mode 100644
index 0000000..6247fcf
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontCommon.cs
@@ -0,0 +1,41 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.BitmapFonts
+{
+ // ---- AngelCode BmFont XML serializer ----------------------
+ // ---- By DeadlyDan @ deadlydan@gmail.com -------------------
+ // ---- There's no license restrictions, use as you will. ----
+ // ---- Credits to http://www.angelcode.com/ -----------------
+ public class BitmapFontCommon
+ {
+ [XmlAttribute("lineHeight")]
+ public int LineHeight { get; set; }
+
+ [XmlAttribute("base")]
+ public int Base { get; set; }
+
+ [XmlAttribute("scaleW")]
+ public int ScaleW { get; set; }
+
+ [XmlAttribute("scaleH")]
+ public int ScaleH { get; set; }
+
+ [XmlAttribute("pages")]
+ public int Pages { get; set; }
+
+ [XmlAttribute("packed")]
+ public int Packed { get; set; }
+
+ [XmlAttribute("alphaChnl")]
+ public int AlphaChannel { get; set; }
+
+ [XmlAttribute("redChnl")]
+ public int RedChannel { get; set; }
+
+ [XmlAttribute("greenChnl")]
+ public int GreenChannel { get; set; }
+
+ [XmlAttribute("blueChnl")]
+ public int BlueChannel { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontFile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontFile.cs
new file mode 100644
index 0000000..db77270
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontFile.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.BitmapFonts
+{
+ // ---- AngelCode BmFont XML serializer ----------------------
+ // ---- By DeadlyDan @ deadlydan@gmail.com -------------------
+ // ---- There's no license restrictions, use as you will. ----
+ // ---- Credits to http://www.angelcode.com/ -----------------
+ [XmlRoot("font")]
+ public class BitmapFontFile
+ {
+ [XmlElement("info")]
+ public BitmapFontInfo Info { get; set; }
+
+ [XmlElement("common")]
+ public BitmapFontCommon Common { get; set; }
+
+ [XmlArray("pages")]
+ [XmlArrayItem("page")]
+ public List<BitmapFontPage> Pages { get; set; }
+
+ [XmlArray("chars")]
+ [XmlArrayItem("char")]
+ public List<BitmapFontChar> Chars { get; set; }
+
+ [XmlArray("kernings")]
+ [XmlArrayItem("kerning")]
+ public List<BitmapFontKerning> Kernings { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontImporter.cs
new file mode 100644
index 0000000..ea4c528
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontImporter.cs
@@ -0,0 +1,22 @@
+using System.IO;
+using System.Xml.Serialization;
+using Microsoft.Xna.Framework.Content.Pipeline;
+
+namespace MonoGame.Extended.Content.Pipeline.BitmapFonts
+{
+ [ContentImporter(".fnt", DefaultProcessor = "BitmapFontProcessor",
+ DisplayName = "BMFont Importer - MonoGame.Extended")]
+ public class BitmapFontImporter : ContentImporter<BitmapFontFile>
+ {
+ public override BitmapFontFile Import(string filename, ContentImporterContext context)
+ {
+ context.Logger.LogMessage("Importing XML file: {0}", filename);
+
+ using (var streamReader = new StreamReader(filename))
+ {
+ var deserializer = new XmlSerializer(typeof(BitmapFontFile));
+ return (BitmapFontFile)deserializer.Deserialize(streamReader);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontInfo.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontInfo.cs
new file mode 100644
index 0000000..1f50cf8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontInfo.cs
@@ -0,0 +1,47 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.BitmapFonts
+{
+ // ---- AngelCode BmFont XML serializer ----------------------
+ // ---- By DeadlyDan @ deadlydan@gmail.com -------------------
+ // ---- There's no license restrictions, use as you will. ----
+ // ---- Credits to http://www.angelcode.com/ -----------------
+ public class BitmapFontInfo
+ {
+ [XmlAttribute("face")]
+ public string Face { get; set; }
+
+ [XmlAttribute("size")]
+ public int Size { get; set; }
+
+ [XmlAttribute("bold")]
+ public int Bold { get; set; }
+
+ [XmlAttribute("italic")]
+ public int Italic { get; set; }
+
+ [XmlAttribute("charset")]
+ public string CharSet { get; set; }
+
+ [XmlAttribute("unicode")]
+ public int Unicode { get; set; }
+
+ [XmlAttribute("stretchH")]
+ public int StretchHeight { get; set; }
+
+ [XmlAttribute("smooth")]
+ public int Smooth { get; set; }
+
+ [XmlAttribute("aa")]
+ public int SuperSampling { get; set; }
+
+ [XmlAttribute("padding")]
+ public string Padding { get; set; }
+
+ [XmlAttribute("spacing")]
+ public string Spacing { get; set; }
+
+ [XmlAttribute("outline")]
+ public int OutLine { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontKerning.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontKerning.cs
new file mode 100644
index 0000000..77caf13
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontKerning.cs
@@ -0,0 +1,20 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.BitmapFonts
+{
+ // ---- AngelCode BmFont XML serializer ----------------------
+ // ---- By DeadlyDan @ deadlydan@gmail.com -------------------
+ // ---- There's no license restrictions, use as you will. ----
+ // ---- Credits to http://www.angelcode.com/ -----------------
+ public class BitmapFontKerning
+ {
+ [XmlAttribute("first")]
+ public int First { get; set; }
+
+ [XmlAttribute("second")]
+ public int Second { get; set; }
+
+ [XmlAttribute("amount")]
+ public int Amount { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontPage.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontPage.cs
new file mode 100644
index 0000000..3841ff5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontPage.cs
@@ -0,0 +1,17 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.BitmapFonts
+{
+ // ---- AngelCode BmFont XML serializer ----------------------
+ // ---- By DeadlyDan @ deadlydan@gmail.com -------------------
+ // ---- There's no license restrictions, use as you will. ----
+ // ---- Credits to http://www.angelcode.com/ -----------------
+ public class BitmapFontPage
+ {
+ [XmlAttribute("id")]
+ public int Id { get; set; }
+
+ [XmlAttribute("file")]
+ public string File { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessor.cs
new file mode 100644
index 0000000..a859eb5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessor.cs
@@ -0,0 +1,33 @@
+using System;
+using System.IO;
+using Microsoft.Xna.Framework.Content.Pipeline;
+
+namespace MonoGame.Extended.Content.Pipeline.BitmapFonts
+{
+ [ContentProcessor(DisplayName = "BMFont Processor - MonoGame.Extended")]
+ public class BitmapFontProcessor : ContentProcessor<BitmapFontFile, BitmapFontProcessorResult>
+ {
+ public override BitmapFontProcessorResult Process(BitmapFontFile bitmapFontFile, ContentProcessorContext context)
+ {
+ try
+ {
+ context.Logger.LogMessage("Processing BMFont");
+ var result = new BitmapFontProcessorResult(bitmapFontFile);
+
+ foreach (var fontPage in bitmapFontFile.Pages)
+ {
+ var assetName = Path.GetFileNameWithoutExtension(fontPage.File);
+ context.Logger.LogMessage("Expected texture asset: {0}", assetName);
+ result.TextureAssets.Add(assetName);
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ context.Logger.LogMessage("Error {0}", ex);
+ throw;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessorResult.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessorResult.cs
new file mode 100644
index 0000000..5841acc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessorResult.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Content.Pipeline.BitmapFonts
+{
+ public class BitmapFontProcessorResult
+ {
+ public List<string> TextureAssets { get; private set; }
+ public BitmapFontFile FontFile { get; private set; }
+
+ public BitmapFontProcessorResult(BitmapFontFile fontFile)
+ {
+ FontFile = fontFile;
+ TextureAssets = new List<string>();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontWriter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontWriter.cs
new file mode 100644
index 0000000..343c40c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontWriter.cs
@@ -0,0 +1,52 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
+
+namespace MonoGame.Extended.Content.Pipeline.BitmapFonts
+{
+ [ContentTypeWriter]
+ public class BitmapFontWriter : ContentTypeWriter<BitmapFontProcessorResult>
+ {
+ protected override void Write(ContentWriter writer, BitmapFontProcessorResult result)
+ {
+ writer.Write(result.TextureAssets.Count);
+
+ foreach (var textureAsset in result.TextureAssets)
+ writer.Write(textureAsset);
+
+ var fontFile = result.FontFile;
+ writer.Write(fontFile.Common.LineHeight);
+ writer.Write(fontFile.Chars.Count);
+
+ foreach (var c in fontFile.Chars)
+ {
+ writer.Write(c.Id);
+ writer.Write(c.Page);
+ writer.Write(c.X);
+ writer.Write(c.Y);
+ writer.Write(c.Width);
+ writer.Write(c.Height);
+ writer.Write(c.XOffset);
+ writer.Write(c.YOffset);
+ writer.Write(c.XAdvance);
+ }
+
+ writer.Write(fontFile.Kernings.Count);
+ foreach(var k in fontFile.Kernings)
+ {
+ writer.Write(k.First);
+ writer.Write(k.Second);
+ writer.Write(k.Amount);
+ }
+ }
+
+ public override string GetRuntimeType(TargetPlatform targetPlatform)
+ {
+ return "MonoGame.Extended.BitmapFonts.BitmapFont, MonoGame.Extended";
+ }
+
+ public override string GetRuntimeReader(TargetPlatform targetPlatform)
+ {
+ return "MonoGame.Extended.BitmapFonts.BitmapFontReader, MonoGame.Extended";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterContextExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterContextExtensions.cs
new file mode 100644
index 0000000..7f63815
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterContextExtensions.cs
@@ -0,0 +1,15 @@
+using System.IO;
+using Microsoft.Xna.Framework.Content.Pipeline;
+
+namespace MonoGame.Extended.Content.Pipeline;
+
+public static class ContentImporterContextExtensions
+{
+ public static string AddDependencyWithLogging(this ContentImporterContext context, string filePath, string source)
+ {
+ source = Path.Combine(Path.GetDirectoryName(filePath), source);
+ ContentLogger.Log($"Adding dependency '{source}'");
+ context.AddDependency(source);
+ return source;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterResult.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterResult.cs
new file mode 100644
index 0000000..e302ca7
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterResult.cs
@@ -0,0 +1,14 @@
+namespace MonoGame.Extended.Content.Pipeline
+{
+ public class ContentImporterResult<T>
+ {
+ public ContentImporterResult(string filePath, T data)
+ {
+ FilePath = filePath;
+ Data = data;
+ }
+
+ public string FilePath { get; }
+ public T Data { get; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentItem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentItem.cs
new file mode 100644
index 0000000..e69d48e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentItem.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework.Content.Pipeline;
+
+namespace MonoGame.Extended.Content.Pipeline
+{
+ public interface IExternalReferenceRepository
+ {
+ ExternalReference<TInput> GetExternalReference<TInput>(string source);
+ }
+
+ public class ContentItem<T> : ContentItem, IExternalReferenceRepository
+ {
+ public ContentItem(T data)
+ {
+ Data = data;
+ }
+
+ public T Data { get; }
+
+ private readonly Dictionary<string, ContentItem> _externalReferences = new Dictionary<string, ContentItem>();
+
+ public void BuildExternalReference<TInput>(ContentProcessorContext context, string source, OpaqueDataDictionary parameters = null)
+ {
+ var sourceAsset = new ExternalReference<TInput>(source);
+ var externalReference = context.BuildAsset<TInput, TInput>(sourceAsset, "", parameters, "", "");
+ _externalReferences.Add(source, externalReference);
+ }
+
+ public ExternalReference<TInput> GetExternalReference<TInput>(string source)
+ {
+ if (source is not null && _externalReferences.TryGetValue(source, out var contentItem))
+ return contentItem as ExternalReference<TInput>;
+
+ return null;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentLogger.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentLogger.cs
new file mode 100644
index 0000000..83848a1
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentLogger.cs
@@ -0,0 +1,14 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+
+namespace MonoGame.Extended.Content.Pipeline
+{
+ public class ContentLogger
+ {
+ public static ContentBuildLogger Logger { get; set; }
+
+ public static void Log(string message)
+ {
+ Logger?.LogMessage(message);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentWriterExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentWriterExtensions.cs
new file mode 100644
index 0000000..b8cf9d2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentWriterExtensions.cs
@@ -0,0 +1,79 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
+
+namespace MonoGame.Extended.Content.Pipeline
+{
+ public static class ContentWriterExtensions
+ {
+ public static void Write(this ContentWriter contentWriter, Color value)
+ {
+ contentWriter.Write(value.R);
+ contentWriter.Write(value.G);
+ contentWriter.Write(value.B);
+ contentWriter.Write(value.A);
+ }
+
+ public static void Write(this ContentWriter contentWriter, Matrix value)
+ {
+ contentWriter.Write(value.M11);
+ contentWriter.Write(value.M12);
+ contentWriter.Write(value.M13);
+ contentWriter.Write(value.M14);
+ contentWriter.Write(value.M21);
+ contentWriter.Write(value.M22);
+ contentWriter.Write(value.M23);
+ contentWriter.Write(value.M24);
+ contentWriter.Write(value.M31);
+ contentWriter.Write(value.M32);
+ contentWriter.Write(value.M33);
+ contentWriter.Write(value.M34);
+ contentWriter.Write(value.M41);
+ contentWriter.Write(value.M42);
+ contentWriter.Write(value.M43);
+ contentWriter.Write(value.M44);
+ }
+
+ public static void Write(this ContentWriter contentWriter, Quaternion value)
+ {
+ contentWriter.Write(value.X);
+ contentWriter.Write(value.Y);
+ contentWriter.Write(value.Z);
+ contentWriter.Write(value.W);
+ }
+
+ public static void Write(this ContentWriter contentWriter, Vector2 value)
+ {
+ contentWriter.Write(value.X);
+ contentWriter.Write(value.Y);
+ }
+
+ public static void Write(this ContentWriter contentWriter, Vector3 value)
+ {
+ contentWriter.Write(value.X);
+ contentWriter.Write(value.Y);
+ contentWriter.Write(value.Z);
+ }
+
+ public static void Write(this ContentWriter contentWriter, Vector4 value)
+ {
+ contentWriter.Write(value.X);
+ contentWriter.Write(value.Y);
+ contentWriter.Write(value.Z);
+ contentWriter.Write(value.W);
+ }
+
+ public static void Write(this ContentWriter contentWriter, BoundingSphere value)
+ {
+ contentWriter.Write(value.Center);
+ contentWriter.Write(value.Radius);
+ }
+
+ public static void Write(this ContentWriter contentWriter, Rectangle value)
+ {
+ contentWriter.Write(value.X);
+ contentWriter.Write(value.Y);
+ contentWriter.Write(value.Width);
+ contentWriter.Write(value.Height);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentImporter.cs
new file mode 100644
index 0000000..437d7eb
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentImporter.cs
@@ -0,0 +1,15 @@
+using System.IO;
+using Microsoft.Xna.Framework.Content.Pipeline;
+
+namespace MonoGame.Extended.Content.Pipeline.Json
+{
+ [ContentImporter(".json", DefaultProcessor = nameof(JsonContentProcessor), DisplayName = "JSON Importer - MonoGame.Extended")]
+ public class JsonContentImporter : ContentImporter<ContentImporterResult<string>>
+ {
+ public override ContentImporterResult<string> Import(string filename, ContentImporterContext context)
+ {
+ var json = File.ReadAllText(filename);
+ return new ContentImporterResult<string>(filename, json);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessor.cs
new file mode 100644
index 0000000..6be4ac3
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessor.cs
@@ -0,0 +1,31 @@
+using System;
+using System.ComponentModel;
+using Microsoft.Xna.Framework.Content.Pipeline;
+
+namespace MonoGame.Extended.Content.Pipeline.Json
+{
+ [ContentProcessor(DisplayName = "JSON Processor - MonoGame.Extended")]
+ public class JsonContentProcessor : ContentProcessor<ContentImporterResult<string>, JsonContentProcessorResult>
+ {
+ [DefaultValue(typeof(Type), "System.Object")]
+ public string ContentType { get; set; }
+
+ public override JsonContentProcessorResult Process(ContentImporterResult<string> input, ContentProcessorContext context)
+ {
+ try
+ {
+ var output = new JsonContentProcessorResult
+ {
+ ContentType = ContentType,
+ Json = input.Data
+ };
+ return output;
+ }
+ catch (Exception ex)
+ {
+ context.Logger.LogMessage("Error {0}", ex);
+ throw;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessorResult.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessorResult.cs
new file mode 100644
index 0000000..eaef99e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessorResult.cs
@@ -0,0 +1,8 @@
+namespace MonoGame.Extended.Content.Pipeline.Json
+{
+ public class JsonContentProcessorResult
+ {
+ public string ContentType { get; set; }
+ public string Json { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentTypeWriter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentTypeWriter.cs
new file mode 100644
index 0000000..6efa696
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentTypeWriter.cs
@@ -0,0 +1,27 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
+
+namespace MonoGame.Extended.Content.Pipeline.Json
+{
+ [ContentTypeWriter]
+ public class JsonContentTypeWriter : ContentTypeWriter<JsonContentProcessorResult>
+ {
+ private string _runtimeType;
+
+ protected override void Write(ContentWriter writer, JsonContentProcessorResult result)
+ {
+ _runtimeType = result.ContentType;
+ writer.Write(result.Json);
+ }
+
+ public override string GetRuntimeReader(TargetPlatform targetPlatform)
+ {
+ return _runtimeType;// "MonoGame.Extended.Serialization.SpriteFactoryContentTypeReader, MonoGame.Extended";
+ }
+
+ public override string GetRuntimeType(TargetPlatform targetPlatform)
+ {
+ return _runtimeType;// "MonoGame.Extended.Serialization.SpriteFactoryContentTypeReader, MonoGame.Extended";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/MonoGame.Extended.Content.Pipeline.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/MonoGame.Extended.Content.Pipeline.csproj
new file mode 100644
index 0000000..70b81e8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/MonoGame.Extended.Content.Pipeline.csproj
@@ -0,0 +1,25 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <ItemGroup>
+ <Content Include="$(ArtifactsPath)/bin/MonoGame.Extended.Content.Pipeline/release/*.dll" Pack="True" PackagePath="tools" />
+ </ItemGroup>
+
+ <PropertyGroup>
+ <Description>Content Pipeline importers and processors to make MonoGame more awesome.</Description>
+ <PackageTags>monogame content importer processor reader tiled texturepacker bmfont animations</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Autofac" Version="5.2.0" />
+
+ <PackageReference Include="MonoGame.Framework.Content.Pipeline"
+ Version="3.8.1.303"
+ PrivateAssets="All" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended.Tiled\MonoGame.Extended.Tiled.csproj" />
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/PathExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/PathExtensions.cs
new file mode 100644
index 0000000..b85afb5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/PathExtensions.cs
@@ -0,0 +1,14 @@
+using System;
+using System.IO;
+
+namespace MonoGame.Extended.Content.Pipeline
+{
+ public static class PathExtensions
+ {
+ public static string GetApplicationFullPath(params string[] pathParts)
+ {
+ var path = Path.Combine(pathParts);
+ return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, path);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentImporter.cs
new file mode 100644
index 0000000..3c4607d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentImporter.cs
@@ -0,0 +1,10 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using MonoGame.Extended.Content.Pipeline.Json;
+
+namespace MonoGame.Extended.Content.Pipeline.SpriteFactory
+{
+ [ContentImporter(".sf", DefaultProcessor = nameof(SpriteFactoryContentProcessor), DisplayName = "Sprite Factory Importer - MonoGame.Extended")]
+ public class SpriteFactoryContentImporter : JsonContentImporter
+ {
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentProcessor.cs
new file mode 100644
index 0000000..4920f33
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentProcessor.cs
@@ -0,0 +1,15 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using MonoGame.Extended.Content.Pipeline.Json;
+
+namespace MonoGame.Extended.Content.Pipeline.SpriteFactory
+{
+ [ContentProcessor(DisplayName = "Sprite Factory Processor - MonoGame.Extended")]
+ public class SpriteFactoryContentProcessor : JsonContentProcessor
+ {
+ public SpriteFactoryContentProcessor()
+ {
+ ContentType = "MonoGame.Extended MonoGame.Extended.Animations.SpriteFactory.SpriteFactoryFileReader, MonoGame.Extended.Animations";
+ }
+
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerJsonImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerJsonImporter.cs
new file mode 100644
index 0000000..a80e654
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerJsonImporter.cs
@@ -0,0 +1,18 @@
+using System.IO;
+using System.Text.Json;
+using Microsoft.Xna.Framework.Content.Pipeline;
+using MonoGame.Extended.TextureAtlases;
+
+
+namespace MonoGame.Extended.Content.Pipeline.TextureAtlases
+{
+ [ContentImporter(".json", DefaultProcessor = "TexturePackerProcessor", DisplayName = "TexturePacker JSON Importer - MonoGame.Extended")]
+ public class TexturePackerJsonImporter : ContentImporter<TexturePackerFile>
+ {
+ public override TexturePackerFile Import(string filename, ContentImporterContext context)
+ {
+ var json = File.ReadAllText(filename);
+ return JsonSerializer.Deserialize<TexturePackerFile>(json);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessor.cs
new file mode 100644
index 0000000..1f14ee7
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessor.cs
@@ -0,0 +1,24 @@
+using System;
+using Microsoft.Xna.Framework.Content.Pipeline;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Content.Pipeline.TextureAtlases
+{
+ [ContentProcessor(DisplayName = "TexturePacker Processor - MonoGame.Extended")]
+ public class TexturePackerProcessor : ContentProcessor<TexturePackerFile, TexturePackerProcessorResult>
+ {
+ public override TexturePackerProcessorResult Process(TexturePackerFile input, ContentProcessorContext context)
+ {
+ try
+ {
+ var output = new TexturePackerProcessorResult {Data = input};
+ return output;
+ }
+ catch (Exception ex)
+ {
+ context.Logger.LogMessage("Error {0}", ex);
+ throw;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessorResult.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessorResult.cs
new file mode 100644
index 0000000..a996259
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessorResult.cs
@@ -0,0 +1,9 @@
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Content.Pipeline.TextureAtlases
+{
+ public class TexturePackerProcessorResult
+ {
+ public TexturePackerFile Data { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerWriter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerWriter.cs
new file mode 100644
index 0000000..3fd15ff
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerWriter.cs
@@ -0,0 +1,45 @@
+using System.Diagnostics;
+using System.IO;
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
+
+namespace MonoGame.Extended.Content.Pipeline.TextureAtlases
+{
+ [ContentTypeWriter]
+ public class TexturePackerWriter : ContentTypeWriter<TexturePackerProcessorResult>
+ {
+ protected override void Write(ContentWriter writer, TexturePackerProcessorResult result)
+ {
+ var data = result.Data;
+ var metadata = data.Metadata;
+
+ var assetName = Path.GetFileNameWithoutExtension(metadata.Image);
+ Debug.Assert(assetName != null, "assetName != null");
+
+ writer.Write(assetName);
+ writer.Write(data.Regions.Count);
+
+ foreach (var region in data.Regions)
+ {
+ var regionName = Path.ChangeExtension(region.Filename, null);
+ Debug.Assert(regionName != null, "regionName != null");
+
+ writer.Write(regionName);
+ writer.Write(region.Frame.X);
+ writer.Write(region.Frame.Y);
+ writer.Write(region.Frame.Width);
+ writer.Write(region.Frame.Height);
+ }
+ }
+
+ public override string GetRuntimeType(TargetPlatform targetPlatform)
+ {
+ return "MonoGame.Extended.TextureAtlases.TextureAtlas, MonoGame.Extended";
+ }
+
+ public override string GetRuntimeReader(TargetPlatform targetPlatform)
+ {
+ return "MonoGame.Extended.TextureAtlases.TextureAtlasReader, MonoGame.Extended";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/ContentWriterExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/ContentWriterExtensions.cs
new file mode 100644
index 0000000..28ded17
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/ContentWriterExtensions.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ public static class ContentWriterExtensions
+ {
+ // ReSharper disable once SuggestBaseTypeForParameter
+ public static void WriteTiledMapProperties(this ContentWriter writer, IReadOnlyCollection<TiledMapPropertyContent> value)
+ {
+ if (value == null)
+ {
+ writer.Write(0);
+ return;
+ }
+ writer.Write(value.Count);
+ foreach (var property in value)
+ {
+ writer.Write(property.Name);
+ writer.Write(property.Value ?? string.Empty);
+ WriteTiledMapProperties(writer, property.Properties);
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledContentItem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledContentItem.cs
new file mode 100644
index 0000000..b376252
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledContentItem.cs
@@ -0,0 +1,22 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled;
+
+public class TiledContentItem<T>: ContentItem<T>
+{
+ public TiledContentItem(T data) : base(data)
+ {
+ }
+
+ public void BuildExternalReference<T>(ContentProcessorContext context, TiledMapImageContent image)
+ {
+ var parameters = new OpaqueDataDictionary
+ {
+ { "ColorKeyColor", image.TransparentColor },
+ { "ColorKeyEnabled", true }
+ };
+ BuildExternalReference<Texture2DContent>(context, image.Source, parameters);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapContentItem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapContentItem.cs
new file mode 100644
index 0000000..b0b7b5d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapContentItem.cs
@@ -0,0 +1,12 @@
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ public class TiledMapContentItem : TiledContentItem<TiledMapContent>
+ {
+ public TiledMapContentItem(TiledMapContent data)
+ : base(data)
+ {
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapImporter.cs
new file mode 100644
index 0000000..70e2bee
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapImporter.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml.Serialization;
+using Microsoft.Xna.Framework.Content.Pipeline;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ [ContentImporter(".tmx", DefaultProcessor = "TiledMapProcessor", DisplayName = "Tiled Map Importer - MonoGame.Extended")]
+ public class TiledMapImporter : ContentImporter<TiledMapContentItem>
+ {
+ public override TiledMapContentItem Import(string filePath, ContentImporterContext context)
+ {
+ try
+ {
+ if (filePath == null)
+ throw new ArgumentNullException(nameof(filePath));
+
+ ContentLogger.Logger = context.Logger;
+ ContentLogger.Log($"Importing '{filePath}'");
+
+ var map = DeserializeTiledMapContent(filePath, context);
+
+ if (map.Width > ushort.MaxValue || map.Height > ushort.MaxValue)
+ throw new InvalidContentException($"The map '{filePath} is much too large. The maximum supported width and height for a Tiled map is {ushort.MaxValue}.");
+
+ ContentLogger.Log($"Imported '{filePath}'");
+
+ return new TiledMapContentItem(map);
+
+ }
+ catch (Exception e)
+ {
+ context.Logger.LogImportantMessage(e.StackTrace);
+ throw;
+ }
+ }
+
+ private static TiledMapContent DeserializeTiledMapContent(string mapFilePath, ContentImporterContext context)
+ {
+ using (var reader = new StreamReader(mapFilePath))
+ {
+ var mapSerializer = new XmlSerializer(typeof(TiledMapContent));
+ var map = (TiledMapContent)mapSerializer.Deserialize(reader);
+
+ map.FilePath = mapFilePath;
+
+ for (var i = 0; i < map.Tilesets.Count; i++)
+ {
+ var tileset = map.Tilesets[i];
+
+ string getTilesetSource(string source)
+ => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(mapFilePath), source));
+
+ if (!string.IsNullOrWhiteSpace(tileset.Source))
+ {
+ tileset.Source = getTilesetSource(tileset.Source);
+ ContentLogger.Log($"Adding dependency for {tileset.Source}");
+ // We depend on the tileset. If the tileset changes, the map also needs to rebuild.
+ context.AddDependency(tileset.Source);
+ }
+ else
+ {
+ tileset.Image.Source = getTilesetSource(tileset.Image.Source);
+ ContentLogger.Log($"Adding dependency for {tileset.Image.Source}");
+ context.AddDependency(tileset.Image.Source);
+ }
+ }
+
+ ImportLayers(context, map.Layers, Path.GetDirectoryName(mapFilePath));
+
+ map.Name = mapFilePath;
+ return map;
+ }
+ }
+
+ private static void ImportLayers(ContentImporterContext context, List<TiledMapLayerContent> layers, string path)
+ {
+ for (var i = 0; i < layers.Count; i++)
+ {
+ if (layers[i] is TiledMapImageLayerContent imageLayer)
+ {
+ imageLayer.Image.Source = Path.Combine(path, imageLayer.Image.Source);
+ ContentLogger.Log($"Adding dependency for '{imageLayer.Image.Source}'");
+
+ // Tell the pipeline that we depend on this image and need to rebuild the map if the image changes.
+ // (Maybe the image is a different size)
+ context.AddDependency(imageLayer.Image.Source);
+ }
+ if (layers[i] is TiledMapObjectLayerContent objectLayer)
+ foreach (var obj in objectLayer.Objects)
+ if (!String.IsNullOrWhiteSpace(obj.TemplateSource))
+ {
+ obj.TemplateSource = Path.Combine(path, obj.TemplateSource);
+ ContentLogger.Log($"Adding dependency for '{obj.TemplateSource}'");
+ // Tell the pipeline that we depend on this template and need to rebuild the map if the template changes.
+ // (Templates are loaded into objects on process, so all objects which depend on the template file
+ // need the change to the template)
+ context.AddDependency(obj.TemplateSource);
+ }
+ if (layers[i] is TiledMapGroupLayerContent groupLayer)
+ // Yay recursion!
+ ImportLayers(context, groupLayer.Layers, path);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapObjectTemplateImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapObjectTemplateImporter.cs
new file mode 100644
index 0000000..eeddcaa
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapObjectTemplateImporter.cs
@@ -0,0 +1,54 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using System;
+using System.IO;
+using System.Xml.Serialization;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ [ContentImporter(".tx", DefaultProcessor = "TiledMapObjectTemplateProcessor", DisplayName = "Tiled Map Object Template Importer - MonoGame.Extended")]
+ public class TiledMapObjectTemplateImporter : ContentImporter<TiledMapObjectTemplateContent>
+ {
+ public override TiledMapObjectTemplateContent Import(string filePath, ContentImporterContext context)
+ {
+ try
+ {
+ if (filePath == null)
+ throw new ArgumentNullException(nameof(filePath));
+
+ ContentLogger.Logger = context.Logger;
+ ContentLogger.Log($"Importing '{filePath}'");
+
+ var template = DeserializeTileMapObjectTemplateContent(filePath, context);
+
+ ContentLogger.Log($"Imported '{filePath}'");
+
+ return template;
+ }
+ catch (Exception e)
+ {
+ context.Logger.LogImportantMessage(e.StackTrace);
+ return null;
+ }
+ }
+
+ private static TiledMapObjectTemplateContent DeserializeTileMapObjectTemplateContent(string filePath, ContentImporterContext context)
+ {
+ using (var reader = new StreamReader(filePath))
+ {
+ var templateSerializer = new XmlSerializer(typeof(TiledMapObjectTemplateContent));
+ var template = (TiledMapObjectTemplateContent)templateSerializer.Deserialize(reader);
+
+ if (!string.IsNullOrWhiteSpace(template.Tileset?.Source))
+ {
+ template.Tileset.Source = Path.Combine(Path.GetDirectoryName(filePath), template.Tileset.Source);
+ ContentLogger.Log($"Adding dependency '{template.Tileset.Source}'");
+ // We depend on this tileset.
+ context.AddDependency(template.Tileset.Source);
+ }
+
+ return template;
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapProcessor.cs
new file mode 100644
index 0000000..9668a72
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapProcessor.cs
@@ -0,0 +1,324 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+using MonoGame.Extended.Tiled;
+using MonoGame.Extended.Tiled.Serialization;
+using MonoGame.Framework.Utilities.Deflate;
+using CompressionMode = System.IO.Compression.CompressionMode;
+using GZipStream = System.IO.Compression.GZipStream;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ public static class TiledMapContentHelper
+ {
+ public static void Process(TiledMapObjectContent obj, ContentProcessorContext context)
+ {
+ if (!string.IsNullOrWhiteSpace(obj.TemplateSource))
+ {
+ var externalReference = new ExternalReference<TiledMapObjectLayerContent>(obj.TemplateSource);
+ var template = context.BuildAndLoadAsset<TiledMapObjectLayerContent, TiledMapObjectTemplateContent>(externalReference, "");
+
+ // Nothing says a template can't reference another template.
+ // Yay recusion!
+ Process(template.Object, context);
+
+ if (!obj._globalIdentifier.HasValue && template.Object._globalIdentifier.HasValue)
+ obj.GlobalIdentifier = template.Object.GlobalIdentifier;
+
+ if (!obj._height.HasValue && template.Object._height.HasValue)
+ obj.Height = template.Object.Height;
+
+ if (!obj._identifier.HasValue && template.Object._identifier.HasValue)
+ obj.Identifier = template.Object.Identifier;
+
+ if (!obj._rotation.HasValue && template.Object._rotation.HasValue)
+ obj.Rotation = template.Object.Rotation;
+
+ if (!obj._visible.HasValue && template.Object._visible.HasValue)
+ obj.Visible = template.Object.Visible;
+
+ if (!obj._width.HasValue && template.Object._width.HasValue)
+ obj.Width = template.Object.Width;
+
+ if (!obj._x.HasValue && template.Object._x.HasValue)
+ obj.X = template.Object.X;
+
+ if (!obj._y.HasValue && template.Object._y.HasValue)
+ obj.Y = template.Object.Y;
+
+ if (obj.Ellipse == null && template.Object.Ellipse != null)
+ obj.Ellipse = template.Object.Ellipse;
+
+ if (string.IsNullOrWhiteSpace(obj.Name) && !string.IsNullOrWhiteSpace(template.Object.Name))
+ obj.Name = template.Object.Name;
+
+ if (obj.Polygon == null && template.Object.Polygon != null)
+ obj.Polygon = template.Object.Polygon;
+
+ if (obj.Polyline == null && template.Object.Polyline != null)
+ obj.Polyline = template.Object.Polyline;
+
+ foreach (var tProperty in template.Object.Properties)
+ {
+ if (!obj.Properties.Exists(p => p.Name == tProperty.Name))
+ obj.Properties.Add(tProperty);
+ }
+
+ if (string.IsNullOrWhiteSpace(obj.Type) && !string.IsNullOrWhiteSpace(template.Object.Type))
+ obj.Type = template.Object.Type;
+
+ if (string.IsNullOrWhiteSpace(obj.Class) && !string.IsNullOrWhiteSpace(template.Object.Class))
+ obj.Class = template.Object.Class;
+ }
+ }
+ }
+
+
+ [ContentProcessor(DisplayName = "Tiled Map Processor - MonoGame.Extended")]
+ public class TiledMapProcessor : ContentProcessor<TiledMapContentItem, TiledMapContentItem>
+ {
+ public override TiledMapContentItem Process(TiledMapContentItem contentItem, ContentProcessorContext context)
+ {
+ try
+ {
+ ContentLogger.Logger = context.Logger;
+ var map = contentItem.Data;
+
+ if (map.Orientation == TiledMapOrientationContent.Hexagonal || map.Orientation == TiledMapOrientationContent.Staggered)
+ throw new NotSupportedException($"{map.Orientation} Tiled Maps are currently not implemented!");
+
+ foreach (var tileset in map.Tilesets)
+ {
+ if (string.IsNullOrWhiteSpace(tileset.Source))
+ {
+ // Load the Texture2DContent for the tileset as it will be saved into the map content file.
+ contentItem.BuildExternalReference<Texture2DContent>(context, tileset.Image);
+ }
+ else
+ {
+ // Link to the tileset for the content loader to load at runtime.
+ //var externalReference = new ExternalReference<TiledMapTilesetContent>(tileset.Source);
+ //tileset.Content = context.BuildAsset<TiledMapTilesetContent, TiledMapTilesetContent>(externalReference, "");
+ contentItem.BuildExternalReference<TiledMapTilesetContent>(context, tileset.Source);
+ }
+ }
+
+ ProcessLayers(contentItem, map, context, map.Layers);
+
+ return contentItem;
+ }
+ catch (Exception ex)
+ {
+ context.Logger.LogImportantMessage(ex.Message);
+ throw;
+ }
+ }
+
+ private static void ProcessLayers(TiledMapContentItem contentItem, TiledMapContent map, ContentProcessorContext context, List<TiledMapLayerContent> layers)
+ {
+ foreach (var layer in layers)
+ {
+ switch (layer)
+ {
+ case TiledMapImageLayerContent imageLayer:
+ ContentLogger.Log($"Processing image layer '{imageLayer.Name}'");
+ contentItem.BuildExternalReference<Texture2DContent>(context, imageLayer.Image);
+ ContentLogger.Log($"Processed image layer '{imageLayer.Name}'");
+ break;
+
+ case TiledMapTileLayerContent tileLayer when tileLayer.Data.Chunks.Count > 0:
+ throw new NotSupportedException($"{map.FilePath} contains data chunks. These are currently not supported.");
+
+ case TiledMapTileLayerContent tileLayer:
+ var data = tileLayer.Data;
+ var encodingType = data.Encoding ?? "xml";
+ var compressionType = data.Compression ?? "xml";
+
+ ContentLogger.Log($"Processing tile layer '{tileLayer.Name}': Encoding: '{encodingType}', Compression: '{compressionType}'");
+ var tileData = DecodeTileLayerData(encodingType, tileLayer);
+ var tiles = CreateTiles(map.RenderOrder, map.Width, map.Height, tileData);
+ tileLayer.Tiles = tiles;
+ ContentLogger.Log($"Processed tile layer '{tileLayer}': {tiles.Length} tiles");
+ break;
+
+ case TiledMapObjectLayerContent objectLayer:
+ ContentLogger.Log($"Processing object layer '{objectLayer.Name}'");
+
+ foreach (var obj in objectLayer.Objects)
+ TiledMapContentHelper.Process(obj, context);
+
+ ContentLogger.Log($"Processed object layer '{objectLayer.Name}'");
+ break;
+
+ case TiledMapGroupLayerContent groupLayer:
+ ProcessLayers(contentItem, map, context, groupLayer.Layers);
+ break;
+ }
+ }
+ }
+
+ private static List<TiledMapTileContent> DecodeTileLayerData(string encodingType, TiledMapTileLayerContent tileLayer)
+ {
+ List<TiledMapTileContent> tiles;
+
+ switch (encodingType)
+ {
+ case "xml":
+ tiles = tileLayer.Data.Tiles;
+ break;
+ case "csv":
+ tiles = DecodeCommaSeperatedValuesData(tileLayer.Data);
+ break;
+ case "base64":
+ tiles = DecodeBase64Data(tileLayer.Data, tileLayer.Width, tileLayer.Height);
+ break;
+ default:
+ throw new NotSupportedException($"The tile layer encoding '{encodingType}' is not supported.");
+ }
+
+ return tiles;
+ }
+
+ private static TiledMapTile[] CreateTiles(TiledMapTileDrawOrderContent renderOrder, int mapWidth, int mapHeight, List<TiledMapTileContent> tileData)
+ {
+ TiledMapTile[] tiles;
+
+ switch (renderOrder)
+ {
+ case TiledMapTileDrawOrderContent.LeftDown:
+ tiles = CreateTilesInLeftDownOrder(tileData, mapWidth, mapHeight).ToArray();
+ break;
+ case TiledMapTileDrawOrderContent.LeftUp:
+ tiles = CreateTilesInLeftUpOrder(tileData, mapWidth, mapHeight).ToArray();
+ break;
+ case TiledMapTileDrawOrderContent.RightDown:
+ tiles = CreateTilesInRightDownOrder(tileData, mapWidth, mapHeight).ToArray();
+ break;
+ case TiledMapTileDrawOrderContent.RightUp:
+ tiles = CreateTilesInRightUpOrder(tileData, mapWidth, mapHeight).ToArray();
+ break;
+ default:
+ throw new NotSupportedException($"{renderOrder} is not supported.");
+ }
+
+ return tiles.ToArray();
+ }
+
+ private static IEnumerable<TiledMapTile> CreateTilesInLeftDownOrder(List<TiledMapTileContent> tileLayerData, int mapWidth, int mapHeight)
+ {
+ for (var y = 0; y < mapHeight; y++)
+ {
+ for (var x = mapWidth - 1; x >= 0; x--)
+ {
+ var dataIndex = x + y * mapWidth;
+ var globalIdentifier = tileLayerData[dataIndex].GlobalIdentifier;
+ if (globalIdentifier == 0)
+ continue;
+ var tile = new TiledMapTile(globalIdentifier, (ushort)x, (ushort)y);
+ yield return tile;
+ }
+ }
+ }
+
+ private static IEnumerable<TiledMapTile> CreateTilesInLeftUpOrder(List<TiledMapTileContent> tileLayerData, int mapWidth, int mapHeight)
+ {
+ for (var y = mapHeight - 1; y >= 0; y--)
+ {
+ for (var x = mapWidth - 1; x >= 0; x--)
+ {
+ var dataIndex = x + y * mapWidth;
+ var globalIdentifier = tileLayerData[dataIndex].GlobalIdentifier;
+ if (globalIdentifier == 0)
+ continue;
+ var tile = new TiledMapTile(globalIdentifier, (ushort)x, (ushort)y);
+ yield return tile;
+ }
+ }
+ }
+
+ private static IEnumerable<TiledMapTile> CreateTilesInRightDownOrder(List<TiledMapTileContent> tileLayerData, int mapWidth, int mapHeight)
+ {
+ for (var y = 0; y < mapHeight; y++)
+ {
+ for (var x = 0; x < mapWidth; x++)
+ {
+ var dataIndex = x + y * mapWidth;
+ var globalIdentifier = tileLayerData[dataIndex].GlobalIdentifier;
+ if (globalIdentifier == 0)
+ continue;
+ var tile = new TiledMapTile(globalIdentifier, (ushort)x, (ushort)y);
+ yield return tile;
+ }
+ }
+ }
+
+ private static IEnumerable<TiledMapTile> CreateTilesInRightUpOrder(List<TiledMapTileContent> tileLayerData, int mapWidth, int mapHeight)
+ {
+ for (var y = mapHeight - 1; y >= 0; y--)
+ {
+ for (var x = mapWidth - 1; x >= 0; x--)
+ {
+ var dataIndex = x + y * mapWidth;
+ var globalIdentifier = tileLayerData[dataIndex].GlobalIdentifier;
+ if (globalIdentifier == 0)
+ continue;
+ var tile = new TiledMapTile(globalIdentifier, (ushort)x, (ushort)y);
+ yield return tile;
+ }
+ }
+ }
+
+ private static List<TiledMapTileContent> DecodeBase64Data(TiledMapTileLayerDataContent data, int width, int height)
+ {
+ var tileList = new List<TiledMapTileContent>();
+ var encodedData = data.Value.Trim();
+ var decodedData = Convert.FromBase64String(encodedData);
+
+ using (var stream = OpenStream(decodedData, data.Compression))
+ {
+ using (var reader = new BinaryReader(stream))
+ {
+ data.Tiles = new List<TiledMapTileContent>();
+
+ for (var y = 0; y < width; y++)
+ {
+ for (var x = 0; x < height; x++)
+ {
+ var gid = reader.ReadUInt32();
+ tileList.Add(new TiledMapTileContent
+ {
+ GlobalIdentifier = gid
+ });
+ }
+ }
+ }
+ }
+
+ return tileList;
+ }
+
+ private static List<TiledMapTileContent> DecodeCommaSeperatedValuesData(TiledMapTileLayerDataContent data)
+ {
+ return data.Value
+ .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(uint.Parse)
+ .Select(x => new TiledMapTileContent { GlobalIdentifier = x })
+ .ToList();
+ }
+
+ private static Stream OpenStream(byte[] decodedData, string compressionMode)
+ {
+ var memoryStream = new MemoryStream(decodedData, false);
+
+ return compressionMode switch
+ {
+ "gzip" => new GZipStream(memoryStream, CompressionMode.Decompress),
+ "zlib" => new ZlibStream(memoryStream, Framework.Utilities.Deflate.CompressionMode.Decompress),
+ _ => memoryStream
+ };
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetContentItem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetContentItem.cs
new file mode 100644
index 0000000..d4b2221
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetContentItem.cs
@@ -0,0 +1,14 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ public class TiledMapTilesetContentItem : TiledContentItem<TiledMapTilesetContent>
+ {
+ public TiledMapTilesetContentItem(TiledMapTilesetContent data)
+ : base(data)
+ {
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetImporter.cs
new file mode 100644
index 0000000..c848bde
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetImporter.cs
@@ -0,0 +1,60 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using System;
+using System.IO;
+using System.Xml.Serialization;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ [ContentImporter(".tsx", DefaultProcessor = "TiledMapTilesetProcessor", DisplayName = "Tiled Map Tileset Importer - MonoGame.Extended")]
+ public class TiledMapTilesetImporter : ContentImporter<TiledMapTilesetContentItem>
+ {
+ public override TiledMapTilesetContentItem Import(string filePath, ContentImporterContext context)
+ {
+ try
+ {
+ if (filePath == null)
+ throw new ArgumentNullException(nameof(filePath));
+
+ ContentLogger.Logger = context.Logger;
+ ContentLogger.Log($"Importing '{filePath}'");
+
+ var tileset = DeserializeTiledMapTilesetContent(filePath, context);
+
+ ContentLogger.Log($"Imported '{filePath}'");
+
+ return new TiledMapTilesetContentItem(tileset);
+ }
+ catch (Exception e)
+ {
+ context.Logger.LogImportantMessage(e.StackTrace);
+ throw;
+ }
+ }
+
+ private TiledMapTilesetContent DeserializeTiledMapTilesetContent(string filePath, ContentImporterContext context)
+ {
+ using (var reader = new StreamReader(filePath))
+ {
+ var tilesetSerializer = new XmlSerializer(typeof(TiledMapTilesetContent));
+ var tileset = (TiledMapTilesetContent)tilesetSerializer.Deserialize(reader);
+
+ if (tileset.Image is not null)
+ tileset.Image.Source = context.AddDependencyWithLogging(filePath, tileset.Image.Source);
+
+ foreach (var tile in tileset.Tiles)
+ {
+ foreach (var obj in tile.Objects)
+ {
+ if (!string.IsNullOrWhiteSpace(obj.TemplateSource))
+ obj.TemplateSource = context.AddDependencyWithLogging(filePath, obj.TemplateSource);
+ }
+ if (tile.Image is not null)
+ tile.Image.Source = context.AddDependencyWithLogging(filePath, tile.Image.Source);
+ }
+
+ return tileset;
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetProcessor.cs
new file mode 100644
index 0000000..5682602
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetProcessor.cs
@@ -0,0 +1,44 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+using System;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ [ContentProcessor(DisplayName = "Tiled Map Tileset Processor - MonoGame.Extended")]
+ public class TiledMapTilesetProcessor : ContentProcessor<TiledMapTilesetContentItem, TiledMapTilesetContentItem>
+ {
+ public override TiledMapTilesetContentItem Process(TiledMapTilesetContentItem contentItem, ContentProcessorContext context)
+ {
+ try
+ {
+ var tileset = contentItem.Data;
+
+ ContentLogger.Logger = context.Logger;
+ ContentLogger.Log($"Processing tileset '{tileset.Name}'");
+
+ // Build the Texture2D asset and load it as it will be saved as part of this tileset file.
+ if (tileset.Image is not null)
+ contentItem.BuildExternalReference<Texture2DContent>(context, tileset.Image);
+
+ foreach (var tile in tileset.Tiles)
+ {
+ foreach (var obj in tile.Objects)
+ {
+ TiledMapContentHelper.Process(obj, context);
+ }
+ if (tile.Image is not null)
+ contentItem.BuildExternalReference<Texture2DContent>(context, tile.Image);
+ }
+
+ ContentLogger.Log($"Processed tileset '{tileset.Name}'");
+
+ return contentItem;
+ }
+ catch (Exception ex)
+ {
+ context.Logger.LogImportantMessage(ex.Message);
+ throw ex;
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetWriter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetWriter.cs
new file mode 100644
index 0000000..48690ad
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetWriter.cs
@@ -0,0 +1,145 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
+using MonoGame.Extended.Tiled;
+using System;
+using System.Globalization;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ [ContentTypeWriter]
+ public class TiledMapTilesetWriter : ContentTypeWriter<TiledMapTilesetContentItem>
+ {
+ public override string GetRuntimeReader(TargetPlatform targetPlatform) => "MonoGame.Extended.Tiled.TiledMapTilesetReader, MonoGame.Extended.Tiled";
+
+ public override string GetRuntimeType(TargetPlatform targetPlatform) => "MonoGame.Extended.Tiled.TiledMapTileset, MonoGame.Extended.Tiled";
+
+ protected override void Write(ContentWriter writer, TiledMapTilesetContentItem contentItem)
+ {
+ try
+ {
+ WriteTileset(writer, contentItem.Data, contentItem);
+ }
+ catch (Exception ex)
+ {
+ ContentLogger.Logger.LogImportantMessage(ex.StackTrace);
+ throw;
+ }
+ }
+
+ public static void WriteTileset(ContentWriter writer, TiledMapTilesetContent tileset, IExternalReferenceRepository externalReferenceRepository)
+ {
+ var externalReference = externalReferenceRepository.GetExternalReference<Texture2DContent>(tileset.Image?.Source);
+ writer.WriteExternalReference(externalReference);
+ writer.Write(tileset.Class ?? tileset.Type ?? string.Empty);
+ writer.Write(tileset.TileWidth);
+ writer.Write(tileset.TileHeight);
+ writer.Write(tileset.TileCount);
+ writer.Write(tileset.Spacing);
+ writer.Write(tileset.Margin);
+ writer.Write(tileset.Columns);
+ writer.Write(tileset.Tiles.Count);
+
+ foreach (var tilesetTile in tileset.Tiles)
+ WriteTilesetTile(writer, tilesetTile, externalReferenceRepository);
+
+ writer.WriteTiledMapProperties(tileset.Properties);
+ }
+
+ private static void WriteTilesetTile(ContentWriter writer, TiledMapTilesetTileContent tilesetTile,
+ IExternalReferenceRepository externalReferenceRepository)
+ {
+ var externalReference = externalReferenceRepository.GetExternalReference<Texture2DContent>(tilesetTile.Image?.Source);
+ writer.WriteExternalReference(externalReference);
+
+ writer.Write(tilesetTile.LocalIdentifier);
+ writer.Write(tilesetTile.Type);
+ writer.Write(tilesetTile.Frames.Count);
+ writer.Write(tilesetTile.Objects.Count);
+
+ foreach (var @object in tilesetTile.Objects)
+ WriteObject(writer, @object);
+
+ foreach (var frame in tilesetTile.Frames)
+ {
+ writer.Write(frame.TileIdentifier);
+ writer.Write(frame.Duration);
+ }
+
+ writer.WriteTiledMapProperties(tilesetTile.Properties);
+ }
+
+ private static void WriteObject(ContentWriter writer, TiledMapObjectContent @object)
+ {
+ var type = GetObjectType(@object);
+
+ writer.Write((byte)type);
+
+ writer.Write(@object.Identifier);
+ writer.Write(@object.Name ?? string.Empty);
+ writer.Write(@object.Class ?? @object.Type ?? string.Empty);
+ writer.Write(@object.X);
+ writer.Write(@object.Y);
+ writer.Write(@object.Width);
+ writer.Write(@object.Height);
+ writer.Write(@object.Rotation);
+ writer.Write(@object.Visible);
+
+ writer.WriteTiledMapProperties(@object.Properties);
+
+ switch (type)
+ {
+ case TiledMapObjectType.Rectangle:
+ case TiledMapObjectType.Ellipse:
+ break;
+ case TiledMapObjectType.Tile:
+ writer.Write(@object.GlobalIdentifier);
+ break;
+ case TiledMapObjectType.Polygon:
+ WritePolyPoints(writer, @object.Polygon.Points);
+ break;
+ case TiledMapObjectType.Polyline:
+ WritePolyPoints(writer, @object.Polyline.Points);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ // ReSharper disable once SuggestBaseTypeForParameter
+ private static void WritePolyPoints(ContentWriter writer, string @string)
+ {
+ var stringPoints = @string.Split(' ');
+
+ writer.Write(stringPoints.Length);
+
+ foreach (var stringPoint in stringPoints)
+ {
+ var xy = stringPoint.Split(',');
+ var x = float.Parse(xy[0], CultureInfo.InvariantCulture.NumberFormat);
+ writer.Write(x);
+ var y = float.Parse(xy[1], CultureInfo.InvariantCulture.NumberFormat);
+ writer.Write(y);
+ }
+ }
+
+ public static TiledMapObjectType GetObjectType(TiledMapObjectContent content)
+ {
+ if (content.GlobalIdentifier > 0)
+ return TiledMapObjectType.Tile;
+
+ if (content.Ellipse != null)
+ return TiledMapObjectType.Ellipse;
+
+ if (content.Polygon != null)
+ return TiledMapObjectType.Polygon;
+
+ // ReSharper disable once ConvertIfStatementToReturnStatement
+ if (content.Polyline != null)
+ return TiledMapObjectType.Polyline;
+
+ return TiledMapObjectType.Rectangle;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapWriter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapWriter.cs
new file mode 100644
index 0000000..126debb
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapWriter.cs
@@ -0,0 +1,227 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content.Pipeline;
+using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
+using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
+using MonoGame.Extended.Tiled;
+using MonoGame.Extended.Tiled.Serialization;
+
+namespace MonoGame.Extended.Content.Pipeline.Tiled
+{
+ [ContentTypeWriter]
+ public class TiledMapWriter : ContentTypeWriter<TiledMapContentItem>
+ {
+ private TiledMapContentItem _contentItem;
+
+ protected override void Write(ContentWriter writer, TiledMapContentItem contentItem)
+ {
+ _contentItem = contentItem;
+
+ var map = contentItem.Data;
+
+ try
+ {
+ WriteMetaData(writer, map);
+ WriteTilesets(writer, map.Tilesets);
+ WriteLayers(writer, map.Layers);
+ }
+ catch (Exception ex)
+ {
+ ContentLogger.Logger.LogImportantMessage(ex.StackTrace);
+ throw;
+ }
+ }
+
+ private static void WriteMetaData(ContentWriter writer, TiledMapContent map)
+ {
+ writer.Write(map.Class ?? map.Type ?? string.Empty);
+ writer.Write(map.Width);
+ writer.Write(map.Height);
+ writer.Write(map.TileWidth);
+ writer.Write(map.TileHeight);
+ writer.Write(ColorHelper.FromHex(map.BackgroundColor));
+ writer.Write((byte)map.RenderOrder);
+ writer.Write((byte)map.Orientation);
+ writer.WriteTiledMapProperties(map.Properties);
+ }
+
+ private void WriteTilesets(ContentWriter writer, IReadOnlyCollection<TiledMapTilesetContent> tilesets)
+ {
+ writer.Write(tilesets.Count);
+
+ foreach (var tileset in tilesets)
+ WriteTileset(writer, tileset);
+ }
+
+ private void WriteTileset(ContentWriter writer, TiledMapTilesetContent tileset)
+ {
+ writer.Write(tileset.FirstGlobalIdentifier);
+
+ if (!string.IsNullOrWhiteSpace(tileset.Source))
+ {
+ writer.Write(true);
+ writer.WriteExternalReference(_contentItem.GetExternalReference<TiledMapTilesetContent>(tileset.Source));
+ }
+ else
+ {
+ writer.Write(false);
+ TiledMapTilesetWriter.WriteTileset(writer, tileset, _contentItem);
+ }
+ }
+
+ private void WriteLayers(ContentWriter writer, IReadOnlyCollection<TiledMapLayerContent> layers)
+ {
+ writer.Write(layers.Count);
+
+ foreach (var layer in layers)
+ WriteLayer(writer, layer);
+ }
+
+ private void WriteLayer(ContentWriter writer, TiledMapLayerContent layer)
+ {
+ writer.Write((byte)layer.LayerType);
+
+ writer.Write(layer.Name ?? string.Empty);
+ writer.Write(layer.Class ?? layer.Type ?? string.Empty);
+ writer.Write(layer.Visible);
+ writer.Write(layer.Opacity);
+ writer.Write(layer.OffsetX);
+ writer.Write(layer.OffsetY);
+ writer.Write(layer.ParallaxX);
+ writer.Write(layer.ParallaxY);
+
+ writer.WriteTiledMapProperties(layer.Properties);
+
+ switch (layer.LayerType)
+ {
+ case TiledMapLayerType.ImageLayer:
+ WriteImageLayer(writer, (TiledMapImageLayerContent)layer);
+ break;
+ case TiledMapLayerType.TileLayer:
+ WriteTileLayer(writer, (TiledMapTileLayerContent)layer);
+ break;
+ case TiledMapLayerType.ObjectLayer:
+ WriteObjectLayer(writer, (TiledMapObjectLayerContent)layer);
+ break;
+ case TiledMapLayerType.GroupLayer:
+ WriteLayers(writer, ((TiledMapGroupLayerContent)layer).Layers);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(layer.LayerType));
+ }
+ }
+
+ private void WriteImageLayer(ContentWriter writer, TiledMapImageLayerContent imageLayer)
+ {
+ var externalReference = _contentItem.GetExternalReference<Texture2DContent>(imageLayer.Image.Source);
+ writer.WriteExternalReference(externalReference);
+ writer.Write(new Vector2(imageLayer.X, imageLayer.Y));
+ }
+
+ // ReSharper disable once SuggestBaseTypeForParameter
+ private static void WriteTileLayer(ContentWriter writer, TiledMapTileLayerContent tileLayer)
+ {
+ writer.Write(tileLayer.Width);
+ writer.Write(tileLayer.Height);
+
+ writer.Write(tileLayer.Tiles.Length);
+
+ foreach (var tile in tileLayer.Tiles)
+ {
+ writer.Write(tile.GlobalTileIdentifierWithFlags);
+ writer.Write(tile.X);
+ writer.Write(tile.Y);
+ }
+ }
+
+ private static void WriteObjectLayer(ContentWriter writer, TiledMapObjectLayerContent layer)
+ {
+ writer.Write(ColorHelper.FromHex(layer.Color));
+ writer.Write((byte)layer.DrawOrder);
+
+ writer.Write(layer.Objects.Count);
+
+ foreach (var @object in layer.Objects)
+ WriteObject(writer, @object);
+ }
+
+
+ private static void WriteObject(ContentWriter writer, TiledMapObjectContent @object)
+ {
+ var type = GetObjectType(@object);
+
+ writer.Write((byte)type);
+
+ writer.Write(@object.Identifier);
+ writer.Write(@object.Name ?? string.Empty);
+ writer.Write(@object.Class ?? @object.Type ?? string.Empty);
+ writer.Write(@object.X);
+ writer.Write(@object.Y);
+ writer.Write(@object.Width);
+ writer.Write(@object.Height);
+ writer.Write(@object.Rotation);
+ writer.Write(@object.Visible);
+
+ writer.WriteTiledMapProperties(@object.Properties);
+
+ switch (type)
+ {
+ case TiledMapObjectType.Rectangle:
+ case TiledMapObjectType.Ellipse:
+ break;
+ case TiledMapObjectType.Tile:
+ writer.Write(@object.GlobalIdentifier);
+ break;
+ case TiledMapObjectType.Polygon:
+ WritePolyPoints(writer, @object.Polygon.Points);
+ break;
+ case TiledMapObjectType.Polyline:
+ WritePolyPoints(writer, @object.Polyline.Points);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ // ReSharper disable once SuggestBaseTypeForParameter
+ private static void WritePolyPoints(ContentWriter writer, string @string)
+ {
+ var stringPoints = @string.Split(' ');
+
+ writer.Write(stringPoints.Length);
+
+ foreach (var stringPoint in stringPoints)
+ {
+ var xy = stringPoint.Split(',');
+ var x = float.Parse(xy[0], CultureInfo.InvariantCulture.NumberFormat);
+ writer.Write(x);
+ var y = float.Parse(xy[1], CultureInfo.InvariantCulture.NumberFormat);
+ writer.Write(y);
+ }
+ }
+
+ public static TiledMapObjectType GetObjectType(TiledMapObjectContent content)
+ {
+ if (content.GlobalIdentifier > 0)
+ return TiledMapObjectType.Tile;
+
+ if (content.Ellipse != null)
+ return TiledMapObjectType.Ellipse;
+
+ if (content.Polygon != null)
+ return TiledMapObjectType.Polygon;
+
+ // ReSharper disable once ConvertIfStatementToReturnStatement
+ if (content.Polyline != null)
+ return TiledMapObjectType.Polyline;
+
+ return TiledMapObjectType.Rectangle;
+ }
+
+ public override string GetRuntimeType(TargetPlatform targetPlatform) => "MonoGame.Extended.Tiled.TiledMap, MonoGame.Extended.Tiled";
+
+ public override string GetRuntimeReader(TargetPlatform targetPlatform) => "MonoGame.Extended.Tiled.TiledMapReader, MonoGame.Extended.Tiled";
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/readme.txt b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/readme.txt
new file mode 100644
index 0000000..54f1cda
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/readme.txt
@@ -0,0 +1,15 @@
+# MonoGame.Extended.Content.Pipeline
+
+This NuGet package is intended to be used with the MonoGame Content Pipeline tool. You'll need to
+make some changes to your Content Pipline file (usually Content.mgcb) for everything to work
+properly.
+
+Add a reference to the `MonoGame.Extended.Content.Pipeline.dll` now installed in your `packages` folder
+to the [MonoGame Content Pipeline tool](http://www.monogame.net/documentation/?page=Pipeline).
+
+You can do this either with the Reference Editor in the GUI or by manually adding a reference line to
+your `Content.mgcb` file using a text editor.
+
+**Remember**: the versions need to match exactly for everything to work.
+
+For more information, take a look at the [installation guide](http://craftworkgames.github.io/MonoGame.Extended/installation/).
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Aspect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Aspect.cs
new file mode 100644
index 0000000..e6fb606
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Aspect.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Specialized;
+
+namespace MonoGame.Extended.Entities
+{
+ public class Aspect
+ {
+ internal Aspect()
+ {
+ AllSet = new BitVector32();
+ ExclusionSet = new BitVector32();
+ OneSet = new BitVector32();
+ }
+
+ public BitVector32 AllSet;
+ public BitVector32 ExclusionSet;
+ public BitVector32 OneSet;
+
+ public static AspectBuilder All(params Type[] types)
+ {
+ return new AspectBuilder().All(types);
+ }
+
+ public static AspectBuilder One(params Type[] types)
+ {
+ return new AspectBuilder().One(types);
+ }
+
+ public static AspectBuilder Exclude(params Type[] types)
+ {
+ return new AspectBuilder().Exclude(types);
+ }
+
+ public bool IsInterested(BitVector32 componentBits)
+ {
+ if (AllSet.Data != 0 && (componentBits.Data & AllSet.Data) != AllSet.Data)
+ return false;
+
+ if (ExclusionSet.Data != 0 && (componentBits.Data & ExclusionSet.Data) != 0)
+ return false;
+
+ if (OneSet.Data != 0 && (componentBits.Data & OneSet.Data) == 0)
+ return false;
+
+ return true;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/AspectBuilder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/AspectBuilder.cs
new file mode 100644
index 0000000..d749795
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/AspectBuilder.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Specialized;
+using MonoGame.Extended.Collections;
+
+namespace MonoGame.Extended.Entities
+{
+ public class AspectBuilder
+ {
+ public AspectBuilder()
+ {
+ AllTypes = new Bag<Type>();
+ ExclusionTypes = new Bag<Type>();
+ OneTypes = new Bag<Type>();
+ }
+
+ public Bag<Type> AllTypes { get; }
+ public Bag<Type> ExclusionTypes { get; }
+ public Bag<Type> OneTypes { get; }
+
+ public AspectBuilder All(params Type[] types)
+ {
+ foreach (var type in types)
+ AllTypes.Add(type);
+
+ return this;
+ }
+
+ public AspectBuilder One(params Type[] types)
+ {
+ foreach (var type in types)
+ OneTypes.Add(type);
+
+ return this;
+ }
+
+ public AspectBuilder Exclude(params Type[] types)
+ {
+ foreach (var type in types)
+ ExclusionTypes.Add(type);
+
+ return this;
+ }
+
+ public Aspect Build(ComponentManager componentManager)
+ {
+ var aspect = new Aspect();
+ Associate(componentManager, AllTypes, ref aspect.AllSet);
+ Associate(componentManager, OneTypes, ref aspect.OneSet);
+ Associate(componentManager, ExclusionTypes, ref aspect.ExclusionSet);
+ return aspect;
+ }
+
+ // ReSharper disable once ParameterTypeCanBeEnumerable.Local
+ private static void Associate(ComponentManager componentManager, Bag<Type> types, ref BitVector32 bits)
+ {
+ foreach (var type in types)
+ {
+ var id = componentManager.GetComponentTypeId(type);
+ bits[1 << id] = true;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/BitArrayExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/BitArrayExtensions.cs
new file mode 100644
index 0000000..2ca0737
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/BitArrayExtensions.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections;
+
+namespace MonoGame.Extended.Entities
+{
+ public static class BitArrayExtensions
+ {
+ public static bool IsEmpty(this BitArray bitArray)
+ {
+ for (var i = 0; i < bitArray.Length; i++)
+ {
+ if (bitArray[i])
+ return false;
+ }
+
+ return true;
+ }
+
+ public static bool ContainsAll(this BitArray bitArray, BitArray other)
+ {
+ var otherBitsLength = other.Length;
+ var bitsLength = bitArray.Length;
+
+ for (var i = bitsLength; i < otherBitsLength; i++)
+ {
+ if (other[i])
+ return false;
+ }
+
+ var s = Math.Min(bitsLength, otherBitsLength);
+
+ for (var i = 0; s > i; i++)
+ {
+ if ((bitArray[i] & other[i]) != other[i])
+ return false;
+ }
+
+ return true;
+ }
+
+ public static bool Intersects(this BitArray bitArray, BitArray other)
+ {
+ var s = Math.Min(bitArray.Length, other.Length);
+
+ for (var i = 0; s > i; i++)
+ {
+ if (bitArray[i] & other[i])
+ return true;
+ }
+
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentManager.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentManager.cs
new file mode 100644
index 0000000..a59c87c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentManager.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Collections;
+using MonoGame.Extended.Entities.Systems;
+
+namespace MonoGame.Extended.Entities
+{
+ public interface IComponentMapperService
+ {
+ ComponentMapper<T> GetMapper<T>() where T : class;
+ }
+
+ public class ComponentManager : UpdateSystem, IComponentMapperService
+ {
+ public ComponentManager()
+ {
+ _componentMappers = new Bag<ComponentMapper>();
+ _componentTypes = new Dictionary<Type, int>();
+ }
+
+ private readonly Bag<ComponentMapper> _componentMappers;
+ private readonly Dictionary<Type, int> _componentTypes;
+
+ public Action<int> ComponentsChanged;
+
+ private ComponentMapper<T> CreateMapperForType<T>(int componentTypeId)
+ where T : class
+ {
+ // TODO: We can probably do better than this without a huge performance penalty by creating our own bit vector that grows after the first 32 bits.
+ if (componentTypeId >= 32)
+ throw new InvalidOperationException("Component type limit exceeded. We currently only allow 32 component types for performance reasons.");
+
+ var mapper = new ComponentMapper<T>(componentTypeId, ComponentsChanged);
+ _componentMappers[componentTypeId] = mapper;
+ return mapper;
+ }
+
+ public ComponentMapper GetMapper(int componentTypeId)
+ {
+ return _componentMappers[componentTypeId];
+ }
+
+ public ComponentMapper<T> GetMapper<T>()
+ where T : class
+ {
+ var componentTypeId = GetComponentTypeId(typeof(T));
+
+ if (_componentMappers[componentTypeId] != null)
+ return _componentMappers[componentTypeId] as ComponentMapper<T>;
+
+ return CreateMapperForType<T>(componentTypeId);
+ }
+
+ public int GetComponentTypeId(Type type)
+ {
+ if (_componentTypes.TryGetValue(type, out var id))
+ return id;
+
+ id = _componentTypes.Count;
+ _componentTypes.Add(type, id);
+ return id;
+ }
+
+ public BitVector32 CreateComponentBits(int entityId)
+ {
+ var componentBits = new BitVector32();
+ var mask = BitVector32.CreateMask();
+
+ for (var componentId = 0; componentId < _componentMappers.Count; componentId++)
+ {
+ componentBits[mask] = _componentMappers[componentId]?.Has(entityId) ?? false;
+ mask = BitVector32.CreateMask(mask);
+ }
+
+ return componentBits;
+ }
+
+ public void Destroy(int entityId)
+ {
+ foreach (var componentMapper in _componentMappers)
+ componentMapper?.Delete(entityId);
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentMapper.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentMapper.cs
new file mode 100644
index 0000000..35b196c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentMapper.cs
@@ -0,0 +1,69 @@
+using System;
+using MonoGame.Extended.Collections;
+
+namespace MonoGame.Extended.Entities
+{
+ public abstract class ComponentMapper
+ {
+ protected ComponentMapper(int id, Type componentType)
+ {
+ Id = id;
+ ComponentType = componentType;
+ }
+
+ public int Id { get; }
+ public Type ComponentType { get; }
+ public abstract bool Has(int entityId);
+ public abstract void Delete(int entityId);
+ }
+
+ public class ComponentMapper<T> : ComponentMapper
+ where T : class
+ {
+ public event Action<int> OnPut;
+ public event Action<int> OnDelete;
+
+ private readonly Action<int> _onCompositionChanged;
+
+ public ComponentMapper(int id, Action<int> onCompositionChanged)
+ : base(id, typeof(T))
+ {
+ _onCompositionChanged = onCompositionChanged;
+ Components = new Bag<T>();
+ }
+
+ public Bag<T> Components { get; }
+
+ public void Put(int entityId, T component)
+ {
+ Components[entityId] = component;
+ _onCompositionChanged(entityId);
+ OnPut?.Invoke(entityId);
+ }
+
+ public T Get(Entity entity)
+ {
+ return Get(entity.Id);
+ }
+
+ public T Get(int entityId)
+ {
+ return Components[entityId];
+ }
+
+ public override bool Has(int entityId)
+ {
+ if (entityId >= Components.Count)
+ return false;
+
+ return Components[entityId] != null;
+ }
+
+ public override void Delete(int entityId)
+ {
+ Components[entityId] = null;
+ _onCompositionChanged(entityId);
+ OnDelete?.Invoke(entityId);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentType.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentType.cs
new file mode 100644
index 0000000..ebef996
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentType.cs
@@ -0,0 +1,46 @@
+using System;
+
+namespace MonoGame.Extended.Entities
+{
+ //public class ComponentType : IEquatable<ComponentType>
+ //{
+ // public ComponentType(Type type, int id)
+ // {
+ // Type = type;
+ // Id = id;
+ // }
+
+ // public Type Type { get; }
+ // public int Id { get; }
+
+ // public bool Equals(ComponentType other)
+ // {
+ // if (ReferenceEquals(null, other)) return false;
+ // if (ReferenceEquals(this, other)) return true;
+ // return Id == other.Id;
+ // }
+
+ // public override bool Equals(object obj)
+ // {
+ // if (ReferenceEquals(null, obj)) return false;
+ // if (ReferenceEquals(this, obj)) return true;
+ // if (obj.GetType() != GetType()) return false;
+ // return Equals((ComponentType) obj);
+ // }
+
+ // public override int GetHashCode()
+ // {
+ // return Id;
+ // }
+
+ // public static bool operator ==(ComponentType left, ComponentType right)
+ // {
+ // return Equals(left, right);
+ // }
+
+ // public static bool operator !=(ComponentType left, ComponentType right)
+ // {
+ // return !Equals(left, right);
+ // }
+ //}
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Entity.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Entity.cs
new file mode 100644
index 0000000..a56ccff
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Entity.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Specialized;
+
+namespace MonoGame.Extended.Entities
+{
+ public class Entity : IEquatable<Entity>
+ {
+ private readonly EntityManager _entityManager;
+ private readonly ComponentManager _componentManager;
+
+ internal Entity(int id, EntityManager entityManager, ComponentManager componentManager)
+ {
+ Id = id;
+
+ _entityManager = entityManager;
+ _componentManager = componentManager;
+ }
+
+ public int Id { get; }
+
+ public BitVector32 ComponentBits => _entityManager.GetComponentBits(Id);
+
+ public void Attach<T>(T component)
+ where T : class
+ {
+ var mapper = _componentManager.GetMapper<T>();
+ mapper.Put(Id, component);
+ }
+
+ public void Detach<T>()
+ where T : class
+ {
+ var mapper = _componentManager.GetMapper<T>();
+ mapper.Delete(Id);
+ }
+
+ public T Get<T>()
+ where T : class
+ {
+ var mapper = _componentManager.GetMapper<T>();
+ return mapper.Get(Id);
+ }
+
+
+ public bool Has<T>()
+ where T : class
+ {
+ return _componentManager.GetMapper<T>().Has(Id);
+ }
+
+ public void Destroy()
+ {
+ _entityManager.Destroy(Id);
+ }
+
+ public bool Equals(Entity other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+ return Id == other.Id;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != GetType()) return false;
+ return Equals((Entity) obj);
+ }
+
+ public override int GetHashCode()
+ {
+ // ReSharper disable once NonReadonlyMemberInGetHashCode
+ return Id;
+ }
+
+ public static bool operator ==(Entity left, Entity right)
+ {
+ return Equals(left, right);
+ }
+
+ public static bool operator !=(Entity left, Entity right)
+ {
+ return !Equals(left, right);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntityManager.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntityManager.cs
new file mode 100644
index 0000000..5e1d28a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntityManager.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Diagnostics;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Collections;
+using MonoGame.Extended.Entities.Systems;
+
+namespace MonoGame.Extended.Entities
+{
+ public class EntityManager : UpdateSystem
+ {
+ private const int _defaultBagSize = 128;
+
+ public EntityManager(ComponentManager componentManager)
+ {
+ _componentManager = componentManager;
+ _addedEntities = new Bag<int>(_defaultBagSize);
+ _removedEntities = new Bag<int>(_defaultBagSize);
+ _changedEntities = new Bag<int>(_defaultBagSize);
+ _entityToComponentBits = new Bag<BitVector32>(_defaultBagSize);
+ _componentManager.ComponentsChanged += OnComponentsChanged;
+
+ _entityBag = new Bag<Entity>(_defaultBagSize);
+ _entityPool = new Pool<Entity>(() => new Entity(_nextId++, this, _componentManager), _defaultBagSize);
+ }
+
+ private readonly ComponentManager _componentManager;
+ private int _nextId;
+
+ public int Capacity => _entityBag.Capacity;
+ public IEnumerable<int> Entities => _entityBag.Where(e => e != null).Select(e => e.Id);
+ public int ActiveCount { get; private set; }
+
+ private readonly Bag<Entity> _entityBag;
+ private readonly Pool<Entity> _entityPool;
+ private readonly Bag<int> _addedEntities;
+ private readonly Bag<int> _removedEntities;
+ private readonly Bag<int> _changedEntities;
+ private readonly Bag<BitVector32> _entityToComponentBits;
+
+ public event Action<int> EntityAdded;
+ public event Action<int> EntityRemoved;
+ public event Action<int> EntityChanged;
+
+ public Entity Create()
+ {
+ var entity = _entityPool.Obtain();
+ var id = entity.Id;
+ Debug.Assert(_entityBag[id] == null);
+ _entityBag[id] = entity;
+ _addedEntities.Add(id);
+ _entityToComponentBits[id] = new BitVector32(0);
+ return entity;
+ }
+
+ public void Destroy(int entityId)
+ {
+ if (!_removedEntities.Contains(entityId))
+ _removedEntities.Add(entityId);
+ }
+
+ public void Destroy(Entity entity)
+ {
+ Destroy(entity.Id);
+ }
+
+ public Entity Get(int entityId)
+ {
+ return _entityBag[entityId];
+ }
+
+ public BitVector32 GetComponentBits(int entityId)
+ {
+ return _entityToComponentBits[entityId];
+ }
+
+ private void OnComponentsChanged(int entityId)
+ {
+ _changedEntities.Add(entityId);
+ _entityToComponentBits[entityId] = _componentManager.CreateComponentBits(entityId);
+ EntityChanged?.Invoke(entityId);
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ foreach (var entityId in _addedEntities)
+ {
+ _entityToComponentBits[entityId] = _componentManager.CreateComponentBits(entityId);
+ ActiveCount++;
+ EntityAdded?.Invoke(entityId);
+ }
+
+ foreach (var entityId in _changedEntities)
+ {
+ _entityToComponentBits[entityId] = _componentManager.CreateComponentBits(entityId);
+ EntityChanged?.Invoke(entityId);
+ }
+
+ foreach (var entityId in _removedEntities)
+ {
+ // we must notify subscribers before removing it from the pool
+ // otherwise an entity system could still be using the entity when the same id is obtained.
+ EntityRemoved?.Invoke(entityId);
+
+ var entity = _entityBag[entityId];
+ _entityBag[entityId] = null;
+ _componentManager.Destroy(entityId);
+ _entityToComponentBits[entityId] = default(BitVector32);
+ ActiveCount--;
+ _entityPool.Free(entity);
+ }
+
+ _addedEntities.Clear();
+ _removedEntities.Clear();
+ _changedEntities.Clear();
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntitySubscription.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntitySubscription.cs
new file mode 100644
index 0000000..6766b0f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntitySubscription.cs
@@ -0,0 +1,61 @@
+using System;
+using MonoGame.Extended.Collections;
+
+namespace MonoGame.Extended.Entities
+{
+ internal class EntitySubscription : IDisposable
+ {
+ private readonly Bag<int> _activeEntities;
+ private readonly EntityManager _entityManager;
+ private readonly Aspect _aspect;
+ private bool _rebuildActives;
+
+ internal EntitySubscription(EntityManager entityManager, Aspect aspect)
+ {
+ _entityManager = entityManager;
+ _aspect = aspect;
+ _activeEntities = new Bag<int>(entityManager.Capacity);
+ _rebuildActives = true;
+
+ _entityManager.EntityAdded += OnEntityAdded;
+ _entityManager.EntityRemoved += OnEntityRemoved;
+ _entityManager.EntityChanged += OnEntityChanged;
+ }
+
+ private void OnEntityAdded(int entityId)
+ {
+ if (_aspect.IsInterested(_entityManager.GetComponentBits(entityId)))
+ _activeEntities.Add(entityId);
+ }
+
+ private void OnEntityRemoved(int entityId) => _rebuildActives = true;
+ private void OnEntityChanged(int entityId) => _rebuildActives = true;
+
+ public void Dispose()
+ {
+ _entityManager.EntityAdded -= OnEntityAdded;
+ _entityManager.EntityRemoved -= OnEntityRemoved;
+ }
+
+ public Bag<int> ActiveEntities
+ {
+ get
+ {
+ if (_rebuildActives)
+ RebuildActives();
+
+ return _activeEntities;
+ }
+ }
+
+ private void RebuildActives()
+ {
+ _activeEntities.Clear();
+
+ foreach (var entity in _entityManager.Entities)
+ OnEntityAdded(entity);
+
+ _rebuildActives = false;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/MonoGame.Extended.Entities.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/MonoGame.Extended.Entities.csproj
new file mode 100644
index 0000000..21ca693
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/MonoGame.Extended.Entities.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>An Entity Component System to make MonoGame more awesome.</Description>
+ <PackageTags>monogame ecs entity component system</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/DrawSystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/DrawSystem.cs
new file mode 100644
index 0000000..b9e63eb
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/DrawSystem.cs
@@ -0,0 +1,16 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Entities.Systems
+{
+ public interface IDrawSystem : ISystem
+ {
+ void Draw(GameTime gameTime);
+ }
+
+ public abstract class DrawSystem : IDrawSystem
+ {
+ public virtual void Dispose() { }
+ public virtual void Initialize(World world) { }
+ public abstract void Draw(GameTime gameTime);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityDrawSystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityDrawSystem.cs
new file mode 100644
index 0000000..5a0d6b9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityDrawSystem.cs
@@ -0,0 +1,14 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Entities.Systems
+{
+ public abstract class EntityDrawSystem : EntitySystem, IDrawSystem
+ {
+ protected EntityDrawSystem(AspectBuilder aspect)
+ : base(aspect)
+ {
+ }
+
+ public abstract void Draw(GameTime gameTime);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityProcessingSystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityProcessingSystem.cs
new file mode 100644
index 0000000..c4d9339
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityProcessingSystem.cs
@@ -0,0 +1,26 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Entities.Systems
+{
+ public abstract class EntityProcessingSystem : EntityUpdateSystem
+ {
+ protected EntityProcessingSystem(AspectBuilder aspectBuilder)
+ : base(aspectBuilder)
+ {
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ Begin();
+
+ foreach (var entityId in ActiveEntities)
+ Process(gameTime, entityId);
+
+ End();
+ }
+
+ public virtual void Begin() { }
+ public abstract void Process(GameTime gameTime, int entityId);
+ public virtual void End() { }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntitySystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntitySystem.cs
new file mode 100644
index 0000000..2c95107
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntitySystem.cs
@@ -0,0 +1,51 @@
+using MonoGame.Extended.Collections;
+
+namespace MonoGame.Extended.Entities.Systems
+{
+ public abstract class EntitySystem : ISystem
+ {
+ protected EntitySystem(AspectBuilder aspectBuilder)
+ {
+ _aspectBuilder = aspectBuilder;
+ }
+
+ public void Dispose()
+ {
+ if (_world != null)
+ {
+ _world.EntityManager.EntityAdded -= OnEntityAdded;
+ _world.EntityManager.EntityRemoved -= OnEntityRemoved;
+ }
+ }
+
+ private readonly AspectBuilder _aspectBuilder;
+ private EntitySubscription _subscription;
+
+ private World _world;
+
+ protected virtual void OnEntityChanged(int entityId) { }
+ protected virtual void OnEntityAdded(int entityId) { }
+ protected virtual void OnEntityRemoved(int entityId) { }
+
+ public Bag<int> ActiveEntities => _subscription.ActiveEntities;
+
+ public virtual void Initialize(World world)
+ {
+ _world = world;
+
+ var aspect = _aspectBuilder.Build(_world.ComponentManager);
+ _subscription = new EntitySubscription(_world.EntityManager, aspect);
+ _world.EntityManager.EntityAdded += OnEntityAdded;
+ _world.EntityManager.EntityRemoved += OnEntityRemoved;
+ _world.EntityManager.EntityChanged += OnEntityChanged;
+
+ Initialize(world.ComponentManager);
+ }
+
+ public abstract void Initialize(IComponentMapperService mapperService);
+
+ protected void DestroyEntity(int entityId) => _world.DestroyEntity(entityId);
+ protected Entity CreateEntity() => _world.CreateEntity();
+ protected Entity GetEntity(int entityId) => _world.GetEntity(entityId);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityUpdateSystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityUpdateSystem.cs
new file mode 100644
index 0000000..97a61d5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityUpdateSystem.cs
@@ -0,0 +1,14 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Entities.Systems
+{
+ public abstract class EntityUpdateSystem : EntitySystem, IUpdateSystem
+ {
+ protected EntityUpdateSystem(AspectBuilder aspectBuilder)
+ : base(aspectBuilder)
+ {
+ }
+
+ public abstract void Update(GameTime gameTime);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/ISystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/ISystem.cs
new file mode 100644
index 0000000..d4511fc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/ISystem.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace MonoGame.Extended.Entities.Systems
+{
+ public interface ISystem : IDisposable
+ {
+ void Initialize(World world);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/UpdateSystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/UpdateSystem.cs
new file mode 100644
index 0000000..e85c964
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/UpdateSystem.cs
@@ -0,0 +1,16 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Entities.Systems
+{
+ public interface IUpdateSystem : ISystem
+ {
+ void Update(GameTime gameTime);
+ }
+
+ public abstract class UpdateSystem : IUpdateSystem
+ {
+ public virtual void Dispose() { }
+ public virtual void Initialize(World world) { }
+ public abstract void Update(GameTime gameTime);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/World.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/World.cs
new file mode 100644
index 0000000..022d4b8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/World.cs
@@ -0,0 +1,84 @@
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Collections;
+using MonoGame.Extended.Entities.Systems;
+
+namespace MonoGame.Extended.Entities
+{
+ public class World : SimpleDrawableGameComponent
+ {
+ private readonly Bag<IUpdateSystem> _updateSystems;
+ private readonly Bag<IDrawSystem> _drawSystems;
+
+ internal World()
+ {
+ _updateSystems = new Bag<IUpdateSystem>();
+ _drawSystems = new Bag<IDrawSystem>();
+
+ RegisterSystem(ComponentManager = new ComponentManager());
+ RegisterSystem(EntityManager = new EntityManager(ComponentManager));
+ }
+
+ public override void Dispose()
+ {
+ foreach (var updateSystem in _updateSystems)
+ updateSystem.Dispose();
+
+ foreach (var drawSystem in _drawSystems)
+ drawSystem.Dispose();
+
+ _updateSystems.Clear();
+ _drawSystems.Clear();
+
+ base.Dispose();
+ }
+
+ internal EntityManager EntityManager { get; }
+ internal ComponentManager ComponentManager { get; }
+
+ public int EntityCount => EntityManager.ActiveCount;
+
+ internal void RegisterSystem(ISystem system)
+ {
+ // ReSharper disable once ConvertIfStatementToSwitchStatement
+ if (system is IUpdateSystem updateSystem)
+ _updateSystems.Add(updateSystem);
+
+ if (system is IDrawSystem drawSystem)
+ _drawSystems.Add(drawSystem);
+
+ system.Initialize(this);
+ }
+
+ public Entity GetEntity(int entityId)
+ {
+ return EntityManager.Get(entityId);
+ }
+
+ public Entity CreateEntity()
+ {
+ return EntityManager.Create();
+ }
+
+ public void DestroyEntity(int entityId)
+ {
+ EntityManager.Destroy(entityId);
+ }
+
+ public void DestroyEntity(Entity entity)
+ {
+ EntityManager.Destroy(entity);
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ foreach (var system in _updateSystems)
+ system.Update(gameTime);
+ }
+
+ public override void Draw(GameTime gameTime)
+ {
+ foreach (var system in _drawSystems)
+ system.Draw(gameTime);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/WorldBuilder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/WorldBuilder.cs
new file mode 100644
index 0000000..7f03323
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/WorldBuilder.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using MonoGame.Extended.Entities.Systems;
+
+namespace MonoGame.Extended.Entities
+{
+ public class WorldBuilder
+ {
+ private readonly List<ISystem> _systems = new List<ISystem>();
+
+ public WorldBuilder AddSystem(ISystem system)
+ {
+ _systems.Add(system);
+ return this;
+ }
+
+ public World Build()
+ {
+ var world = new World();
+
+ foreach (var system in _systems)
+ world.RegisterSystem(system);
+
+ return world;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher.cs
new file mode 100644
index 0000000..3cb5704
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher.cs
@@ -0,0 +1,387 @@
+using System;
+using System.Runtime.CompilerServices;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics
+{
+ /// <summary>
+ /// Minimizes draw calls to a <see cref="GraphicsDevice" /> by sorting them and attempting to merge them together
+ /// before submitting them.
+ /// </summary>
+ /// <typeparam name="TDrawCallInfo">The type of the information for a draw call.</typeparam>
+ /// <seealso cref="IDisposable" />
+ public abstract class Batcher<TDrawCallInfo> : IDisposable
+ where TDrawCallInfo : struct, IBatchDrawCallInfo<TDrawCallInfo>, IComparable<TDrawCallInfo>
+ {
+ internal const int DefaultBatchMaximumDrawCallsCount = 2048;
+ private BlendState _blendState;
+ private SamplerState _samplerState;
+ private DepthStencilState _depthStencilState;
+ private RasterizerState _rasterizerState;
+ private readonly Effect _defaultEffect;
+ private Effect _currentEffect;
+ private Matrix? _viewMatrix;
+ private Matrix? _projectionMatrix;
+
+ /// <summary>
+ /// The array of <see cref="TDrawCallInfo" /> structs currently enqueued.
+ /// </summary>
+ protected TDrawCallInfo[] DrawCalls;
+
+ /// <summary>
+ /// The number of <see cref="TDrawCallInfo" /> structs currently enqueued.
+ /// </summary>
+ protected int EnqueuedDrawCallCount;
+
+ /// <summary>
+ /// Gets the <see cref="GraphicsDevice" /> associated with this <see cref="Batcher{TDrawCallInfo}" />.
+ /// </summary>
+ /// <value>
+ /// The <see cref="GraphicsDevice" /> associated with this <see cref="Batcher{TDrawCallInfo}" />.
+ /// </value>
+ public GraphicsDevice GraphicsDevice { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether batching is currently in progress by being within a <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair block of code.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if batching has begun; otherwise, <c>false</c>.
+ /// </value>
+ public bool HasBegun { get; internal set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Batcher{TDrawCallInfo}" /> class.
+ /// </summary>
+ /// <param name="graphicsDevice">The graphics device.</param>
+ /// <param name="defaultEffect">The default effect.</param>
+ /// <param name="maximumDrawCallsCount">
+ /// The maximum number of <see cref="TDrawCallInfo" /> structs that can be enqueued before a
+ /// <see cref="Batcher{TDrawCallInfo}.Flush" />
+ /// is required. The default value is <code>2048</code>.
+ /// </param>
+ /// <exception cref="ArgumentNullException">
+ /// <paramref name="graphicsDevice" /> is
+ /// null.
+ /// </exception>
+ /// <exception cref="ArgumentOutOfRangeException">
+ /// <paramref name="maximumDrawCallsCount" /> is less than or equal
+ /// <code>0</code>.
+ /// </exception>
+ protected Batcher(GraphicsDevice graphicsDevice, Effect defaultEffect,
+ int maximumDrawCallsCount = DefaultBatchMaximumDrawCallsCount)
+ {
+ if (graphicsDevice == null)
+ throw new ArgumentNullException(nameof(graphicsDevice));
+
+ if (maximumDrawCallsCount <= 0)
+ throw new ArgumentOutOfRangeException(nameof(maximumDrawCallsCount));
+
+ GraphicsDevice = graphicsDevice;
+ _defaultEffect = defaultEffect;
+ DrawCalls = new TDrawCallInfo[maximumDrawCallsCount];
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="diposing">
+ /// <c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only
+ /// unmanaged resources.
+ /// </param>
+ protected virtual void Dispose(bool diposing)
+ {
+ if (!diposing)
+ return;
+ _defaultEffect.Dispose();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected void EnsureHasBegun([CallerMemberName] string callerMemberName = null)
+ {
+ if (!HasBegun)
+ throw new InvalidOperationException(
+ $"The {nameof(Begin)} method must be called before the {callerMemberName} method can be called.");
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected void EnsureHasNotBegun([CallerMemberName] string callerMemberName = null)
+ {
+ if (HasBegun)
+ throw new InvalidOperationException(
+ $"The {nameof(End)} method must be called before the {callerMemberName} method can be called.");
+ }
+
+ /// <summary>
+ /// Begins the batch operation using an optional <see cref="BlendState" />, <see cref="SamplerState" />,
+ /// <see cref="DepthStencilState" />, <see cref="RasterizerState" />, <see cref="Effect" />, world-to-view
+ /// <see cref="Matrix" />, or view-to-projection <see cref="Matrix" />.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// The default objects for <paramref name="blendState" />, <paramref name="samplerState" />,
+ /// <paramref name="depthStencilState" />, and <paramref name="rasterizerState" /> are
+ /// <see cref="BlendState.AlphaBlend" />, <see cref="SamplerState.LinearClamp" />,
+ /// <see cref="DepthStencilState.None" /> and <see cref="RasterizerState.CullCounterClockwise" /> respectively.
+ /// Passing
+ /// <code>null</code> for any of the previously mentioned parameters result in using their default object.
+ /// </para>
+ /// </remarks>
+ /// <param name="blendState">The <see cref="BlendState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" />, <see cref="End" /> pair.</param>
+ /// <param name="samplerState">
+ /// The texture <see cref="SamplerState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair.
+ /// </param>
+ /// <param name="depthStencilState">
+ /// The <see cref="DepthStencilState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair.
+ /// </param>
+ /// <param name="rasterizerState">
+ /// The <see cref="RasterizerState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair.
+ /// </param>
+ /// <param name="effect">The <see cref="Effect" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and <see cref="End" /> pair.</param>
+ /// <param name="viewMatrix">
+ /// The world-to-view transformation matrix to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair.
+ /// </param>
+ /// <param name="projectionMatrix">
+ /// The view-to-projection transformation matrix to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair.
+ /// </param>
+ /// <exception cref="InvalidOperationException">
+ /// <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> cannot be invoked again until <see cref="End" /> has been invoked.
+ /// </exception>
+ /// <remarks>
+ /// <para>
+ /// This method must be called before any enqueuing of draw calls. When all the geometry have been enqueued for
+ /// drawing, call <see cref="End" />.
+ /// </para>
+ /// </remarks>
+ public virtual void Begin(Matrix? viewMatrix = null, Matrix? projectionMatrix = null, BlendState blendState = null, SamplerState samplerState = null,
+ DepthStencilState depthStencilState = null, RasterizerState rasterizerState = null, Effect effect = null)
+ {
+ var viewMatrix1 = viewMatrix ?? Matrix.Identity;
+ var projectionMatrix1 = projectionMatrix ?? Matrix.CreateOrthographicOffCenter(0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height, 0, 0, -1);
+ Begin(ref viewMatrix1, ref projectionMatrix1, blendState, samplerState, depthStencilState, rasterizerState, effect);
+ }
+
+ /// <summary>
+ /// Begins the batch operation using an optional <see cref="BlendState" />, <see cref="SamplerState" />,
+ /// <see cref="DepthStencilState" />, <see cref="RasterizerState" />, <see cref="Effect" />, world-to-view
+ /// <see cref="Matrix" />, or view-to-projection <see cref="Matrix" />.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// The default objects for <paramref name="blendState" />, <paramref name="samplerState" />,
+ /// <paramref name="depthStencilState" />, and <paramref name="rasterizerState" /> are
+ /// <see cref="BlendState.AlphaBlend" />, <see cref="SamplerState.LinearClamp" />,
+ /// <see cref="DepthStencilState.None" /> and <see cref="RasterizerState.CullCounterClockwise" /> respectively.
+ /// Passing
+ /// <code>null</code> for any of the previously mentioned parameters result in using their default object.
+ /// </para>
+ /// </remarks>
+ /// <param name="blendState">The <see cref="BlendState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" />, <see cref="End" /> pair.</param>
+ /// <param name="samplerState">
+ /// The texture <see cref="SamplerState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair.
+ /// </param>
+ /// <param name="depthStencilState">
+ /// The <see cref="DepthStencilState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair.
+ /// </param>
+ /// <param name="rasterizerState">
+ /// The <see cref="RasterizerState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair.
+ /// </param>
+ /// <param name="effect">The <see cref="Effect" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and <see cref="End" /> pair.</param>
+ /// <param name="viewMatrix">
+ /// The world-to-view transformation matrix to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair.
+ /// </param>
+ /// <param name="projectionMatrix">
+ /// The view-to-projection transformation matrix to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and
+ /// <see cref="End" /> pair.
+ /// </param>
+ /// <exception cref="InvalidOperationException">
+ /// <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> cannot be invoked again until <see cref="End" /> has been invoked.
+ /// </exception>
+ /// <remarks>
+ /// <para>
+ /// This method must be called before any enqueuing of draw calls. When all the geometry have been enqueued for
+ /// drawing, call <see cref="End" />.
+ /// </para>
+ /// </remarks>
+ public virtual void Begin(ref Matrix viewMatrix, ref Matrix projectionMatrix, BlendState blendState = null, SamplerState samplerState = null,
+ DepthStencilState depthStencilState = null, RasterizerState rasterizerState = null, Effect effect = null)
+ {
+ EnsureHasNotBegun();
+ HasBegun = true;
+
+ // Store the states to be applied on End()
+ // This ensures that two or more batchers will not affect each other
+ _blendState = blendState ?? BlendState.AlphaBlend;
+ _samplerState = samplerState ?? SamplerState.PointClamp;
+ _depthStencilState = depthStencilState ?? DepthStencilState.None;
+ _rasterizerState = rasterizerState ?? RasterizerState.CullCounterClockwise;
+ _currentEffect = effect ?? _defaultEffect;
+ _projectionMatrix = projectionMatrix;
+ _viewMatrix = viewMatrix;
+ }
+
+ /// <summary>
+ /// Flushes the batched geometry to the <see cref="GraphicsDevice" /> and restores it's state to how it was before
+ /// <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> was called.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">
+ /// <see cref="End" /> cannot be invoked until <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> has been invoked.
+ /// </exception>
+ /// <remarks>
+ /// <para>
+ /// This method must be called after all enqueuing of draw calls.
+ /// </para>
+ /// </remarks>
+ public void End()
+ {
+ EnsureHasBegun();
+ Flush();
+ HasBegun = false;
+ }
+
+ /// <summary>
+ /// Sorts then submits the (sorted) enqueued draw calls to the <see cref="GraphicsDevice" /> for
+ /// rendering without ending the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and <see cref="End" /> pair.
+ /// </summary>
+ protected void Flush()
+ {
+ if (EnqueuedDrawCallCount == 0)
+ return;
+ SortDrawCallsAndBindBuffers();
+ ApplyStates();
+ SubmitDrawCalls();
+ RestoreStates();
+ }
+
+ /// <summary>
+ /// Sorts the enqueued draw calls and binds any used <see cref="VertexBuffer" /> or <see cref="IndexBuffer" /> to the <see cref="GraphicsDevice" />.
+ /// </summary>
+ protected abstract void SortDrawCallsAndBindBuffers();
+
+ private void ApplyStates()
+ {
+ var oldBlendState = GraphicsDevice.BlendState;
+ var oldSamplerState = GraphicsDevice.SamplerStates[0];
+ var oldDepthStencilState = GraphicsDevice.DepthStencilState;
+ var oldRasterizerState = GraphicsDevice.RasterizerState;
+
+ GraphicsDevice.BlendState = _blendState;
+
+ GraphicsDevice.SamplerStates[0] = _samplerState;
+ GraphicsDevice.DepthStencilState = _depthStencilState;
+ GraphicsDevice.RasterizerState = _rasterizerState;
+
+ _blendState = oldBlendState;
+ _samplerState = oldSamplerState;
+ _depthStencilState = oldDepthStencilState;
+ _rasterizerState = oldRasterizerState;
+
+ var viewMatrix = _viewMatrix ?? Matrix.Identity;
+ var projectionMatrix = _projectionMatrix ??
+ Matrix.CreateOrthographicOffCenter(0, GraphicsDevice.Viewport.Width,
+ GraphicsDevice.Viewport.Height, 0, 0f, -1f);
+
+ var matrixChainEffect = _currentEffect as IMatrixChainEffect;
+ if (matrixChainEffect != null)
+ {
+ matrixChainEffect.World = Matrix.Identity;
+ matrixChainEffect.SetView(ref viewMatrix);
+ matrixChainEffect.SetProjection(ref projectionMatrix);
+ }
+ else
+ {
+ var effectMatrices = _currentEffect as IEffectMatrices;
+ if (effectMatrices == null)
+ return;
+ effectMatrices.World = Matrix.Identity;
+ effectMatrices.View = viewMatrix;
+ effectMatrices.Projection = projectionMatrix;
+ }
+ }
+
+ private void RestoreStates()
+ {
+ GraphicsDevice.BlendState = _blendState;
+ GraphicsDevice.SamplerStates[0] = _samplerState;
+ GraphicsDevice.DepthStencilState = _depthStencilState;
+ GraphicsDevice.RasterizerState = _rasterizerState;
+ }
+
+ /// <summary>
+ /// Enqueues draw call information.
+ /// </summary>
+ /// <param name="drawCall">The draw call information.</param>
+ /// <remarks>
+ /// <para>
+ /// If possible, the <paramref name="drawCall" /> is merged with the last enqueued draw call information instead of
+ /// being
+ /// enqueued.
+ /// </para>
+ /// <para>
+ /// If the enqueue buffer is full, a <see cref="Flush" /> is invoked and then afterwards
+ /// <paramref name="drawCall" /> is enqueued.
+ /// </para>
+ /// </remarks>
+ protected void Enqueue(ref TDrawCallInfo drawCall)
+ {
+ if (EnqueuedDrawCallCount > 0 && drawCall.TryMerge(ref DrawCalls[EnqueuedDrawCallCount - 1]))
+ return;
+ if (EnqueuedDrawCallCount >= DrawCalls.Length)
+ Flush();
+ DrawCalls[EnqueuedDrawCallCount++] = drawCall;
+ }
+
+ /* It might be better to have derived classes just implement the for loop instead of having this virtual method call...
+ * However, if the derived class is only going to override this method once and the code is short, which should both be
+ * true, then maybe we can get away with this virtual method call by having it inlined. So tell the JIT or AOT compiler
+ * we would like it be so. This does NOT guarantee the compiler will respect our wishes.
+ */
+
+ /// <summary>
+ /// Submits a draw operation to the <see cref="GraphicsDevice" /> using the specified <see cref="TDrawCallInfo"/>.
+ /// </summary>
+ /// <param name="drawCall">The draw call information.</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected abstract void InvokeDrawCall(ref TDrawCallInfo drawCall);
+
+ private void SubmitDrawCalls()
+ {
+ if (EnqueuedDrawCallCount == 0)
+ return;
+
+ for (var i = 0; i < EnqueuedDrawCallCount; i++)
+ {
+ DrawCalls[i].SetState(_currentEffect);
+
+ foreach (var pass in _currentEffect.CurrentTechnique.Passes)
+ {
+ pass.Apply();
+ InvokeDrawCall(ref DrawCalls[i]);
+ }
+ }
+
+ Array.Clear(DrawCalls, 0, EnqueuedDrawCallCount);
+ EnqueuedDrawCallCount = 0;
+ }
+ }
+}
+ \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher2D.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher2D.cs
new file mode 100644
index 0000000..e7ec533
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher2D.cs
@@ -0,0 +1,450 @@
+using System;
+using System.Text;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.BitmapFonts;
+using MonoGame.Extended.Graphics.Effects;
+using MonoGame.Extended.Graphics.Geometry;
+
+namespace MonoGame.Extended.Graphics
+{
+ /// <summary>
+ /// A general purpose <see cref="Batcher{TDrawCallInfo}" /> for two-dimensional geometry that change
+ /// frequently between frames such as sprites and shapes.
+ /// </summary>
+ /// <seealso cref="IDisposable" />
+ /// <remarks>
+ /// <para>For drawing user interfaces, consider using <see cref="UIBatcher(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> instead because it supports scissor rectangles.</para>
+ /// </remarks>
+ public sealed class Batcher2D : Batcher<Batcher2D.DrawCallInfo>
+ {
+
+ internal const int DefaultMaximumVerticesCount = 8192;
+ internal const int DefaultMaximumIndicesCount = 12288;
+
+ private readonly VertexBuffer _vertexBuffer;
+ private readonly IndexBuffer _indexBuffer;
+ private readonly VertexPositionColorTexture[] _vertices;
+ private int _vertexCount;
+ private readonly ushort[] _indices;
+ private int _indexCount;
+ private readonly ushort[] _sortedIndices;
+ private readonly GeometryBuilder2D _geometryBuilder;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Batcher2D" /> class.
+ /// </summary>
+ /// <param name="graphicsDevice">The graphics device.</param>
+ /// <param name="maximumVerticesCount">
+ /// The maximum number of vertices that can be enqueued before a
+ /// <see cref="Batcher{TDrawCallInfo}.Flush" /> is required. The default value is <code>8192</code>.
+ /// </param>
+ /// <param name="maximumIndicesCount">
+ /// The maximum number of indices that can be enqueued before a
+ /// <see cref="Batcher{TDrawCallInfo}.Flush" /> is required. The default value is <code>12288</code>.
+ /// </param>
+ /// <param name="maximumDrawCallsCount">
+ /// The maximum number of <see cref="DrawCallInfo" /> structs that can be enqueued before a
+ /// <see cref="Batcher{TDrawCallInfo}.Flush" /> is required. The default value is <code>2048</code>.
+ /// </param>
+ /// <exception cref="ArgumentNullException"><paramref name="graphicsDevice" />.</exception>
+ /// <exception cref="ArgumentOutOfRangeException">
+ /// <paramref name="maximumDrawCallsCount" /> is less than or equal
+ /// <code>0</code>, or <paramref name="maximumVerticesCount" /> is less than or equal to <code>0</code>, or,
+ /// <paramref name="maximumVerticesCount" /> is less than or equal to <code>0</code>.
+ /// </exception>
+ public Batcher2D(GraphicsDevice graphicsDevice,
+ ushort maximumVerticesCount = DefaultMaximumVerticesCount,
+ ushort maximumIndicesCount = DefaultMaximumIndicesCount,
+ int maximumDrawCallsCount = DefaultBatchMaximumDrawCallsCount)
+ : base(
+ graphicsDevice,
+ new DefaultEffect(graphicsDevice)
+ {
+ TextureEnabled = true,
+ VertexColorEnabled = true
+ }, maximumDrawCallsCount)
+
+ {
+ _vertices = new VertexPositionColorTexture[maximumVerticesCount];
+ _vertexBuffer = new DynamicVertexBuffer(graphicsDevice, VertexPositionColorTexture.VertexDeclaration, maximumVerticesCount, BufferUsage.WriteOnly);
+
+ _indices = new ushort[maximumIndicesCount];
+ _sortedIndices = new ushort[maximumIndicesCount];
+ _indexBuffer = new DynamicIndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, maximumIndicesCount, BufferUsage.WriteOnly);
+ _geometryBuilder = new GeometryBuilder2D(4, 6);
+ }
+
+ protected override void SortDrawCallsAndBindBuffers()
+ {
+ // Upload the vertices to the GPU and then select that vertex stream for drawing
+ _vertexBuffer.SetData(_vertices, 0, _vertexCount);
+ GraphicsDevice.SetVertexBuffer(_vertexBuffer);
+
+ Array.Sort(DrawCalls, 0, EnqueuedDrawCallCount);
+ BuildSortedIndices();
+
+ // Upload the indices to the GPU and then select that index stream for drawing
+ _indexBuffer.SetData(_sortedIndices, 0, _indexCount);
+ GraphicsDevice.Indices = _indexBuffer;
+
+ _indexCount = 0;
+ _vertexCount = 0;
+ }
+
+ private void BuildSortedIndices()
+ {
+ var newDrawCallsCount = 0;
+ DrawCalls[0].StartIndex = 0;
+ var currentDrawCall = DrawCalls[0];
+ DrawCalls[newDrawCallsCount++] = DrawCalls[0];
+
+ var drawCallIndexCount = currentDrawCall.PrimitiveCount * 3;
+ Array.Copy(_indices, currentDrawCall.StartIndex, _sortedIndices, 0, drawCallIndexCount);
+ var sortedIndexCount = drawCallIndexCount;
+
+ // iterate through sorted draw calls checking if any can now be merged to reduce expensive draw calls to the graphics API
+ // this might need to be changed for next-gen graphics API (Vulkan, Metal, DirectX 12) where the draw calls are not so expensive
+ for (var i = 1; i < EnqueuedDrawCallCount; i++)
+ {
+ currentDrawCall = DrawCalls[i];
+ drawCallIndexCount = currentDrawCall.PrimitiveCount * 3;
+ Array.Copy(_indices, currentDrawCall.StartIndex, _sortedIndices, sortedIndexCount, drawCallIndexCount);
+ sortedIndexCount += drawCallIndexCount;
+
+ if (currentDrawCall.TryMerge(ref DrawCalls[newDrawCallsCount - 1]))
+ continue;
+
+ currentDrawCall.StartIndex = sortedIndexCount;
+ DrawCalls[newDrawCallsCount++] = currentDrawCall;
+ }
+
+ EnqueuedDrawCallCount = newDrawCallsCount;
+ }
+
+ /// <summary>
+ /// Submits a draw operation to the <see cref="GraphicsDevice" /> using the specified <see cref="DrawCallInfo"/>.
+ /// </summary>
+ /// <param name="drawCall">The draw call information.</param>
+ protected override void InvokeDrawCall(ref DrawCallInfo drawCall)
+ {
+ GraphicsDevice.DrawIndexedPrimitives(drawCall.PrimitiveType, 0, drawCall.StartIndex, drawCall.PrimitiveCount);
+ }
+
+ /// <summary>
+ /// Draws a sprite using a specified <see cref="Texture" />, transform <see cref="Matrix2" />, source
+ /// <see cref="Rectangle" />, and an optional
+ /// <see cref="Color" />, origin <see cref="Vector2" />, <see cref="FlipFlags" />, and depth <see cref="float" />.
+ /// </summary>
+ /// <param name="texture">The <see cref="Texture" />.</param>
+ /// <param name="transformMatrix">The transform <see cref="Matrix2" />.</param>
+ /// <param name="sourceRectangle">
+ /// The texture region <see cref="Rectangle" /> of the <paramref name="texture" />. Use
+ /// <code>null</code> to use the entire <see cref="Texture2D" />.
+ /// </param>
+ /// <param name="color">The <see cref="Color" />. Use <code>null</code> to use the default <see cref="Color.White" />.</param>
+ /// <param name="flags">The <see cref="FlipFlags" />. The default value is <see cref="FlipFlags.None" />.</param>
+ /// <param name="depth">The depth <see cref="float" />. The default value is <code>0</code>.</param>
+ /// <exception cref="InvalidOperationException">The <see cref="Batcher{TDrawCallInfo}.Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> method has not been called.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="texture" /> is null.</exception>
+ public void DrawSprite(Texture2D texture, ref Matrix2 transformMatrix, ref Rectangle sourceRectangle,
+ Color? color = null, FlipFlags flags = FlipFlags.None, float depth = 0)
+ {
+ _geometryBuilder.BuildSprite(_vertexCount, ref transformMatrix, texture, ref sourceRectangle, color, flags, depth);
+ EnqueueBuiltGeometry(texture, depth);
+ }
+
+ /// <summary>
+ /// Draws a <see cref="Texture" /> using the specified transform <see cref="Matrix2" /> and an optional
+ /// <see cref="Color" />, origin <see cref="Vector2" />, <see cref="FlipFlags" />, and depth <see cref="float" />.
+ /// </summary>
+ /// <param name="texture">The <see cref="Texture" />.</param>
+ /// <param name="transformMatrix">The transform <see cref="Matrix2" />.</param>
+ /// <param name="color">The <see cref="Color" />. Use <code>null</code> to use the default <see cref="Color.White" />.</param>
+ /// <param name="flags">The <see cref="FlipFlags" />. The default value is <see cref="FlipFlags.None" />.</param>
+ /// <param name="depth">The depth <see cref="float" />. The default value is <code>0</code>.</param>
+ /// <exception cref="InvalidOperationException">The <see cref="Batcher{TDrawCallInfo}.Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> method has not been called.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="texture" /> is null.</exception>
+ public void DrawTexture(Texture2D texture, ref Matrix2 transformMatrix, Color? color = null,
+ FlipFlags flags = FlipFlags.None, float depth = 0)
+ {
+ var rectangle = default(Rectangle);
+ _geometryBuilder.BuildSprite(_vertexCount, ref transformMatrix, texture, ref rectangle, color, flags, depth);
+ EnqueueBuiltGeometry(texture, depth);
+ }
+
+ private void EnqueueBuiltGeometry(Texture2D texture, float depth)
+ {
+ if ((_vertexCount + _geometryBuilder.VertexCount > _vertices.Length) ||
+ (_indexCount + _geometryBuilder.IndexCount > _indices.Length))
+ Flush();
+ var drawCall = new DrawCallInfo(texture, _geometryBuilder.PrimitiveType, _indexCount,
+ _geometryBuilder.PrimitivesCount, depth);
+ Array.Copy(_geometryBuilder.Vertices, 0, _vertices, _vertexCount, _geometryBuilder.VertexCount);
+ _vertexCount += _geometryBuilder.VertexCount;
+ Array.Copy(_geometryBuilder.Indices, 0, _indices, _indexCount, _geometryBuilder.IndexCount);
+ _indexCount += _geometryBuilder.IndexCount;
+ Enqueue(ref drawCall);
+ }
+
+ /// <summary>
+ /// Draws unicode (UTF-16) characters as sprites using the specified <see cref="BitmapFont" />, text
+ /// <see cref="StringBuilder" />, transform <see cref="Matrix2" /> and optional <see cref="Color" />, origin
+ /// <see cref="Vector2" />, <see cref="FlipFlags" />, and depth <see cref="float" />.
+ /// </summary>
+ /// <param name="bitmapFont">The <see cref="BitmapFont" />.</param>
+ /// <param name="text">The text <see cref="StringBuilder" />.</param>
+ /// <param name="transformMatrix">The transform <see cref="Matrix2" />.</param>
+ /// <param name="color">
+ /// The <see cref="Color" />. Use <code>null</code> to use the default
+ /// <see cref="Color.White" />.
+ /// </param>
+ /// <param name="flags">The <see cref="FlipFlags" />. The default value is <see cref="FlipFlags.None" />.</param>
+ /// <param name="depth">The depth <see cref="float" />. The default value is <code>0f</code>.</param>
+ /// <exception cref="InvalidOperationException">The <see cref="Batcher{TDrawCallInfo}.Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> method has not been called.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="bitmapFont" /> is null or <paramref name="text" /> is null.</exception>
+ public void DrawString(BitmapFont bitmapFont, StringBuilder text, ref Matrix2 transformMatrix,
+ Color? color = null, FlipFlags flags = FlipFlags.None, float depth = 0f)
+ {
+ EnsureHasBegun();
+
+ if (bitmapFont == null)
+ throw new ArgumentNullException(nameof(bitmapFont));
+
+ if (text == null)
+ throw new ArgumentNullException(nameof(text));
+
+ var lineSpacing = bitmapFont.LineHeight;
+ var offset = new Vector2(0, 0);
+
+ BitmapFontRegion lastGlyph = null;
+ for (var i = 0; i < text.Length;)
+ {
+ int character;
+ if (char.IsLowSurrogate(text[i]))
+ {
+ character = char.ConvertToUtf32(text[i - 1], text[i]);
+ i += 2;
+ }
+ else if (char.IsHighSurrogate(text[i]))
+ {
+ character = char.ConvertToUtf32(text[i], text[i - 1]);
+ i += 2;
+ }
+ else
+ {
+ character = text[i];
+ i += 1;
+ }
+
+ // ReSharper disable once SwitchStatementMissingSomeCases
+ switch (character)
+ {
+ case '\r':
+ continue;
+ case '\n':
+ offset.X = 0;
+ offset.Y += lineSpacing;
+ lastGlyph = null;
+ continue;
+ }
+
+ var fontRegion = bitmapFont.GetCharacterRegion(character);
+ if (fontRegion == null)
+ continue;
+
+ var transform1Matrix = transformMatrix;
+ transform1Matrix.M31 += offset.X + fontRegion.XOffset;
+ transform1Matrix.M32 += offset.Y + fontRegion.YOffset;
+
+ var textureRegion = fontRegion.TextureRegion;
+ var bounds = textureRegion.Bounds;
+ DrawSprite(textureRegion.Texture, ref transform1Matrix, ref bounds, color, flags, depth);
+
+ var advance = fontRegion.XAdvance + bitmapFont.LetterSpacing;
+ if (BitmapFont.UseKernings && lastGlyph != null)
+ {
+ int amount;
+ if (lastGlyph.Kernings.TryGetValue(character, out amount))
+ {
+ advance += amount;
+ }
+ }
+
+ offset.X += i != text.Length - 1
+ ? advance
+ : fontRegion.XOffset + fontRegion.Width;
+
+ lastGlyph = fontRegion;
+ }
+ }
+
+ /// <summary>
+ /// Draws unicode (UTF-16) characters as sprites using the specified <see cref="BitmapFont" />, text
+ /// <see cref="StringBuilder" />, position <see cref="Vector2" /> and optional <see cref="Color" />, rotation
+ /// <see cref="float" />, origin <see cref="Vector2" />, scale <see cref="Vector2" /> <see cref="FlipFlags" />, and
+ /// depth <see cref="float" />.
+ /// </summary>
+ /// <param name="bitmapFont">The <see cref="BitmapFont" />.</param>
+ /// <param name="text">The text <see cref="string" />.</param>
+ /// <param name="position">The position <see cref="Vector2" />.</param>
+ /// <param name="color">
+ /// The <see cref="Color" />. Use <code>null</code> to use the default
+ /// <see cref="Color.White" />.
+ /// </param>
+ /// <param name="rotation">
+ /// The angle <see cref="float" /> (in radians) to rotate each sprite about its <paramref name="origin" />. The default
+ /// value is <code>0f</code>.
+ /// </param>
+ /// <param name="origin">
+ /// The origin <see cref="Vector2" />. Use <code>null</code> to use the default
+ /// <see cref="Vector2.Zero" />.
+ /// </param>
+ /// <param name="scale">
+ /// The scale <see cref="Vector2" />. Use <code>null</code> to use the default
+ /// <see cref="Vector2.One" />.
+ /// </param>
+ /// <param name="flags">The <see cref="FlipFlags" />. The default value is <see cref="FlipFlags.None" />.</param>
+ /// <param name="depth">The depth <see cref="float" />. The default value is <code>0f</code></param>
+ /// <exception cref="InvalidOperationException">The <see cref="Batcher{TDrawCallInfo}.Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> method has not been called.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="bitmapFont" /> is null or <paramref name="text" /> is null.</exception>
+ public void DrawString(BitmapFont bitmapFont, StringBuilder text, Vector2 position, Color? color = null,
+ float rotation = 0f, Vector2? origin = null, Vector2? scale = null,
+ FlipFlags flags = FlipFlags.None, float depth = 0f)
+ {
+ Matrix2 transformMatrix;
+ Matrix2.CreateFrom(position, rotation, scale, origin, out transformMatrix);
+ DrawString(bitmapFont, text, ref transformMatrix, color, flags, depth);
+ }
+
+ /// <summary>
+ /// Draws unicode (UTF-16) characters as sprites using the specified <see cref="BitmapFont" />, text
+ /// <see cref="string" />, transform <see cref="Matrix2" /> and optional <see cref="Color" />, origin
+ /// <see cref="Vector2" />, <see cref="FlipFlags" />, and depth <see cref="float" />.
+ /// </summary>
+ /// <param name="bitmapFont">The <see cref="BitmapFont" />.</param>
+ /// <param name="text">The text <see cref="string" />.</param>
+ /// <param name="transformMatrix">The transform <see cref="Matrix2" />.</param>
+ /// <param name="color">
+ /// The <see cref="Color" />. Use <code>null</code> to use the default
+ /// <see cref="Color.White" />.
+ /// </param>
+ /// <param name="flags">The <see cref="FlipFlags" />. The default value is <see cref="FlipFlags.None" />.</param>
+ /// <param name="depth">The depth <see cref="float" />. The default value is <code>0f</code></param>
+ /// <exception cref="InvalidOperationException">The <see cref="Batcher{TDrawCallInfo}.Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> method has not been called.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="bitmapFont" /> is null or <paramref name="text" /> is null.</exception>
+ public void DrawString(BitmapFont bitmapFont, string text, ref Matrix2 transformMatrix, Color? color = null,
+ FlipFlags flags = FlipFlags.None, float depth = 0f)
+ {
+ EnsureHasBegun();
+
+ if (bitmapFont == null)
+ throw new ArgumentNullException(nameof(bitmapFont));
+
+ if (text == null)
+ throw new ArgumentNullException(nameof(text));
+
+ var glyphs = bitmapFont.GetGlyphs(text);
+ foreach (var glyph in glyphs)
+ {
+ var transform1Matrix = transformMatrix;
+ transform1Matrix.M31 += glyph.Position.X;
+ transform1Matrix.M32 += glyph.Position.Y;
+
+ var texture = glyph.FontRegion.TextureRegion.Texture;
+ var bounds = texture.Bounds;
+ DrawSprite(texture, ref transform1Matrix, ref bounds, color, flags, depth);
+ }
+ }
+
+ /// <summary>
+ /// Draws unicode (UTF-16) characters as sprites using the specified <see cref="BitmapFont" />, text
+ /// <see cref="string" />, position <see cref="Vector2" /> and optional <see cref="Color" />, rotation
+ /// <see cref="float" />, origin <see cref="Vector2" />, scale <see cref="Vector2" /> <see cref="FlipFlags" />, and
+ /// depth <see cref="float" />.
+ /// </summary>
+ /// <param name="bitmapFont">The <see cref="BitmapFont" />.</param>
+ /// <param name="text">The text <see cref="string" />.</param>
+ /// <param name="position">The position <see cref="Vector2" />.</param>
+ /// <param name="color">
+ /// The <see cref="Color" />. Use <code>null</code> to use the default
+ /// <see cref="Color.White" />.
+ /// </param>
+ /// <param name="rotation">
+ /// The angle <see cref="float" /> (in radians) to rotate each sprite about its <paramref name="origin" />. The default
+ /// value is <code>0f</code>.
+ /// </param>
+ /// <param name="origin">
+ /// The origin <see cref="Vector2" />. Use <code>null</code> to use the default
+ /// <see cref="Vector2.Zero" />.
+ /// </param>
+ /// <param name="scale">
+ /// The scale <see cref="Vector2" />. Use <code>null</code> to use the default
+ /// <see cref="Vector2.One" />.
+ /// </param>
+ /// <param name="flags">The <see cref="FlipFlags" />. The default value is <see cref="FlipFlags.None" />.</param>
+ /// <param name="depth">The depth <see cref="float" />. The default value is <code>0f</code></param>
+ /// <exception cref="InvalidOperationException">The <see cref="Batcher{TDrawCallInfo}.Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> method has not been called.</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="bitmapFont" /> is null or <paramref name="text" /> is null.</exception>
+ public void DrawString(BitmapFont bitmapFont, string text, Vector2 position, Color? color = null,
+ float rotation = 0f, Vector2? origin = null, Vector2? scale = null,
+ FlipFlags flags = FlipFlags.None, float depth = 0f)
+ {
+ Matrix2 matrix;
+ Matrix2.CreateFrom(position, rotation, scale, origin, out matrix);
+ DrawString(bitmapFont, text, ref matrix, color, flags, depth);
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public struct DrawCallInfo : IBatchDrawCallInfo<DrawCallInfo>, IComparable<DrawCallInfo>
+ {
+ internal readonly PrimitiveType PrimitiveType;
+ internal int StartIndex;
+ internal int PrimitiveCount;
+ internal readonly Texture2D Texture;
+ internal readonly uint TextureKey;
+ internal readonly uint DepthKey;
+
+ internal unsafe DrawCallInfo(Texture2D texture, PrimitiveType primitiveType, int startIndex, int primitiveCount, float depth)
+ {
+ PrimitiveType = primitiveType;
+ StartIndex = startIndex;
+ PrimitiveCount = primitiveCount;
+ Texture = texture;
+ TextureKey = (uint)RuntimeHelpers.GetHashCode(texture);
+ DepthKey = *(uint*)&depth;
+ }
+
+ public void SetState(Effect effect)
+ {
+ var textureEffect = effect as ITextureEffect;
+ if (textureEffect != null)
+ textureEffect.Texture = Texture;
+ }
+
+ public bool TryMerge(ref DrawCallInfo drawCall)
+ {
+ if (PrimitiveType != drawCall.PrimitiveType || TextureKey != drawCall.TextureKey ||
+ DepthKey != drawCall.DepthKey)
+ return false;
+ drawCall.PrimitiveCount += PrimitiveCount;
+ return true;
+ }
+
+ [SuppressMessage("ReSharper", "ImpureMethodCallOnReadonlyValueField")]
+ public int CompareTo(DrawCallInfo other)
+ {
+ var result = TextureKey.CompareTo(other.TextureKey);;
+ if (result != 0)
+ return result;
+ result = DepthKey.CompareTo(other.DepthKey);
+ return result != 0 ? result : ((byte)PrimitiveType).CompareTo((byte)other.PrimitiveType);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/DefaultEffect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/DefaultEffect.cs
new file mode 100644
index 0000000..df71e47
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/DefaultEffect.cs
@@ -0,0 +1,203 @@
+using System.Collections.Specialized;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics.Effects
+{
+ /// <summary>
+ /// An <see cref="Effect" /> that allows objects, within a 3D context, to be represented on a 2D monitor.
+ /// </summary>
+ /// <seealso cref="MatrixChainEffect" />
+ /// <seealso cref="ITextureEffect" />
+ public class DefaultEffect : MatrixChainEffect, ITextureEffect
+ {
+ /// <summary>
+ /// The bitmask for use with <see cref="MatrixChainEffect.Flags" /> indicating wether <see cref="Texture" /> has
+ /// changed in the last frame.
+ /// </summary>
+ protected static int DirtyTextureBitMask = BitVector32.CreateMask(UseDefaultProjectionBitMask);
+
+ /// <summary>
+ /// The bitmask for use with <see cref="MatrixChainEffect.Flags" /> indicating wether the underlying vertex shader and
+ /// fragment (pixel) shaders have changed to one of the pre-defined shaders in the last frame.
+ /// </summary>
+ protected static int DirtyShaderIndexBitMask = BitVector32.CreateMask(DirtyTextureBitMask);
+
+ /// <summary>
+ /// The bitmask for use with <see cref="MatrixChainEffect.Flags" /> indicating wether the material color has changed in
+ /// the last frame.
+ /// </summary>
+ public static int DirtyMaterialColorBitMask = BitVector32.CreateMask(DirtyShaderIndexBitMask);
+
+ private Texture2D _texture;
+ private EffectParameter _textureParameter;
+
+ private float _alpha = 1;
+ private Color _diffuseColor = Color.White;
+ private EffectParameter _diffuseColorParameter;
+
+ private bool _textureEnabled;
+ private bool _vertexColorEnabled;
+
+ /// <summary>
+ /// Gets or sets the material <see cref="Texture2D" />.
+ /// </summary>
+ /// <value>
+ /// The material <see cref="Texture2D" />.
+ /// </value>
+ public Texture2D Texture
+ {
+ get { return _texture; }
+ set
+ {
+ _texture = value;
+ Flags[DirtyTextureBitMask] = true;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the material color alpha.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// The alpha channel uses the premultiplied (associated) representation. This means that the RGB components of a
+ /// color represent
+ /// the color of the object of pixel, adjusted for its opacity by multiplication of <see cref="Alpha" />.
+ /// </para>
+ /// </remarks>
+ public float Alpha
+ {
+ get { return _alpha; }
+
+ set
+ {
+ _alpha = value;
+ Flags[DirtyMaterialColorBitMask] = true;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets whether texturing is enabled.
+ /// </summary>
+ public bool TextureEnabled
+ {
+ get { return _textureEnabled; }
+
+ set
+ {
+ if (_textureEnabled == value)
+ return;
+ _textureEnabled = value;
+ Flags[DirtyShaderIndexBitMask] = true;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets whether vertex color is enabled.
+ /// </summary>
+ public bool VertexColorEnabled
+ {
+ get { return _vertexColorEnabled; }
+
+ set
+ {
+ if (_vertexColorEnabled == value)
+ return;
+ _vertexColorEnabled = value;
+ Flags[DirtyShaderIndexBitMask] = true;
+ }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DefaultEffect" /> class.
+ /// </summary>
+ /// <param name="graphicsDevice">The graphics device.</param>
+ public DefaultEffect(GraphicsDevice graphicsDevice)
+ : base(graphicsDevice, EffectResource.DefaultEffect.Bytecode)
+ {
+ Initialize();
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DefaultEffect" /> class.
+ /// </summary>
+ /// <param name="graphicsDevice">The graphics device.</param>
+ /// <param name="byteCode">The byte code of the shader program.</param>
+ public DefaultEffect(GraphicsDevice graphicsDevice, byte[] byteCode)
+ : base(graphicsDevice, byteCode)
+ {
+ Initialize();
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DefaultEffect" /> class.
+ /// </summary>
+ /// <param name="cloneSource">The clone source.</param>
+ public DefaultEffect(Effect cloneSource)
+ : base(cloneSource)
+ {
+ Initialize();
+ }
+
+ private void Initialize()
+ {
+ Flags[DirtyMaterialColorBitMask] = true;
+ _textureParameter = Parameters["Texture"];
+ _diffuseColorParameter = Parameters["DiffuseColor"];
+ }
+
+ /// <summary>
+ /// Computes derived parameter values immediately before applying the effect.
+ /// </summary>
+ protected override void OnApply()
+ {
+ base.OnApply();
+
+ if (Flags[DirtyTextureBitMask])
+ {
+ _textureParameter.SetValue(_texture);
+ Flags[DirtyTextureBitMask] = false;
+ }
+
+ // ReSharper disable once InvertIf
+ if (Flags[DirtyMaterialColorBitMask])
+ {
+ UpdateMaterialColor();
+ Flags[DirtyMaterialColorBitMask] = false;
+ }
+
+ // ReSharper disable once InvertIf
+ if (Flags[DirtyShaderIndexBitMask])
+ {
+ var shaderIndex = 0;
+
+ if (_textureEnabled)
+ shaderIndex += 1;
+
+ if (_vertexColorEnabled)
+ shaderIndex += 2;
+
+ Flags[DirtyShaderIndexBitMask] = false;
+ CurrentTechnique = Techniques[shaderIndex];
+ }
+ }
+
+ /// <summary>
+ /// Updates the material color parameters associated with this <see cref="DefaultEffect" />.
+ /// </summary>
+ protected virtual void UpdateMaterialColor()
+ {
+ var diffuseColorVector3 = _diffuseColor.ToVector3();
+
+ var diffuseColorVector4 = new Vector4()
+ {
+ X = diffuseColorVector3.X * Alpha,
+ Y = diffuseColorVector3.Y * Alpha,
+ Z = diffuseColorVector3.Z * Alpha,
+ W = Alpha,
+ };
+
+ _diffuseColorParameter.SetValue(diffuseColorVector4);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/EffectResource.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/EffectResource.cs
new file mode 100644
index 0000000..43bd535
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/EffectResource.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Reflection;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics.Effects
+{
+ /// <summary>
+ /// Reperesents the bytecode of an <see cref="Effect" /> that is encapsulated inside a compiled assembly.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// Files that are encapsulated inside a compiled assembly are commonly known as Manifiest or embedded resources.
+ /// Since embedded resources are added to the assembly at compiled time, they can not be accidentally deleted or
+ /// misplaced. However, if the file needs to be changed, the assembly will need to be re-compiled with the changed
+ /// file.
+ /// </para>
+ /// <para>
+ /// To add an embedded resource file to an assembly, first add it to the project and then change the Build Action
+ /// in the Properties of the file to <code>Embedded Resource</code>. The next time the project is compiled, the
+ /// compiler will add the file to the assembly as an embedded resource. The compiler adds namespace(s) to the
+ /// embedded resource so it matches with the path of where the file was added to the project.
+ /// </para>
+ /// </remarks>
+ public class EffectResource
+ {
+ private static EffectResource _defaultEffect;
+ private static string _shaderExtension;
+
+ /// <summary>
+ /// Gets the <see cref="Effects.DefaultEffect" /> embedded into the MonoGame.Extended.Graphics library.
+ /// </summary>
+ public static EffectResource DefaultEffect => _defaultEffect ?? (_defaultEffect = new EffectResource($"MonoGame.Extended.Graphics.Effects.Resources.DefaultEffect.{_shaderExtension}.mgfxo"));
+
+ static EffectResource()
+ {
+ DetermineShaderExtension();
+ }
+
+ private static void DetermineShaderExtension()
+ {
+ // use reflection to figure out if Shader.Profile is OpenGL (0) or DirectX (1),
+ // may need to be changed / fixed for future shader profiles
+
+ var assembly = typeof(Game).GetTypeInfo().Assembly;
+ Debug.Assert(assembly != null);
+
+ var shaderType = assembly.GetType("Microsoft.Xna.Framework.Graphics.Shader");
+ Debug.Assert(shaderType != null);
+ var shaderTypeInfo = shaderType.GetTypeInfo();
+ Debug.Assert(shaderTypeInfo != null);
+
+ // https://github.com/MonoGame/MonoGame/blob/develop/MonoGame.Framework/Graphics/Shader/Shader.cs#L47
+ var profileProperty = shaderTypeInfo.GetDeclaredProperty("Profile");
+ var value = (int)profileProperty.GetValue(null);
+
+ switch (value)
+ {
+ case 0:
+ // OpenGL
+ _shaderExtension = "ogl";
+ break;
+ case 1:
+ // DirectX
+ _shaderExtension = "dx11";
+ break;
+ default:
+ throw new InvalidOperationException("Unknown shader profile.");
+ }
+ }
+
+ private readonly string _resourceName;
+ private volatile byte[] _bytecode;
+ private readonly Assembly _assembly;
+
+ /// <summary>
+ /// Gets the bytecode of the <see cref="Effect" /> file.
+ /// </summary>
+ /// <value>
+ /// The bytecode of the <see cref="Effect" /> file.
+ /// </value>
+ public byte[] Bytecode
+ {
+ get
+ {
+ if (_bytecode != null)
+ return _bytecode;
+
+ lock (this)
+ {
+ if (_bytecode != null)
+ return _bytecode;
+
+ var stream = _assembly.GetManifestResourceStream(_resourceName);
+ using (var memoryStream = new MemoryStream())
+ {
+ stream.CopyTo(memoryStream);
+ _bytecode = memoryStream.ToArray();
+ }
+ }
+
+ return _bytecode;
+ }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EffectResource" /> class.
+ /// </summary>
+ /// <param name="resourceName">The name of the embedded resource. This must include the namespace(s).</param>
+ /// <param name="assembly">The assembly which the embedded resource is apart of.</param>
+ public EffectResource(string resourceName, Assembly assembly = null)
+ {
+ _resourceName = resourceName;
+ _assembly = assembly ?? typeof(EffectResource).GetTypeInfo().Assembly;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/ITextureEffect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/ITextureEffect.cs
new file mode 100644
index 0000000..ea2ef0b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/ITextureEffect.cs
@@ -0,0 +1,18 @@
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics.Effects
+{
+ /// <summary>
+ /// Defines an <see cref="Effect" /> that uses a <see cref="Texture2D" />.
+ /// </summary>
+ public interface ITextureEffect
+ {
+ /// <summary>
+ /// Gets or sets the <see cref="Texture2D" />.
+ /// </summary>
+ /// <value>
+ /// The <see cref="Texture2D" />.
+ /// </value>
+ Texture2D Texture { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/MatrixChainEffect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/MatrixChainEffect.cs
new file mode 100644
index 0000000..046479f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/MatrixChainEffect.cs
@@ -0,0 +1,155 @@
+using System.Collections.Specialized;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics.Effects
+{
+ /// <summary>
+ /// An <see cref="Effect" /> that uses the standard chain of matrix transformations to represent a 3D object on a 2D
+ /// monitor.
+ /// </summary>
+ /// <seealso cref="Effect" />
+ /// <seealso cref="IEffectMatrices" />
+ public abstract class MatrixChainEffect : Effect, IMatrixChainEffect
+ {
+ /// <summary>
+ /// The bitmask for use with <see cref="Flags"/> indicating wether <see cref="World"/>, <see cref="View"/>, or <see cref="Projection"/> has changed in the last frame.
+ /// </summary>
+ protected static int DirtyWorldViewProjectionBitMask = BitVector32.CreateMask();
+
+ /// <summary>
+ /// The bitmask for use with <see cref="Flags"/> indicating wether to use a default projection matrix or a custom projection matrix.
+ /// </summary>
+ protected static int UseDefaultProjectionBitMask = BitVector32.CreateMask(DirtyWorldViewProjectionBitMask);
+
+ /// <summary>
+ /// The dirty flags associated with this <see cref="MatrixChainEffect"/>.
+ /// </summary>
+ protected BitVector32 Flags;
+
+ private Matrix _projection = Matrix.Identity;
+ private Matrix _view = Matrix.Identity;
+ private Matrix _world = Matrix.Identity;
+ private EffectParameter _matrixParameter;
+
+ /// <summary>
+ /// Gets or sets the model-to-world <see cref="Matrix" />.
+ /// </summary>
+ /// <value>
+ /// The model-to-world <see cref="Matrix" />.
+ /// </value>
+ public Matrix World
+ {
+ get { return _world; }
+ set { SetWorld(ref value); }
+ }
+
+ /// <summary>
+ /// Gets or sets the world-to-view <see cref="Matrix" />.
+ /// </summary>
+ /// <value>
+ /// The world-to-view <see cref="Matrix" />.
+ /// </value>
+ public Matrix View
+ {
+ get { return _view; }
+ set { SetView(ref value); }
+ }
+
+ /// <summary>
+ /// Gets or sets the view-to-projection <see cref="Matrix" />.
+ /// </summary>
+ /// <value>
+ /// The view-to-projection <see cref="Matrix" />.
+ /// </value>
+ public Matrix Projection
+ {
+ get { return _projection; }
+ set { SetProjection(ref value); }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MatrixChainEffect" /> class.
+ /// </summary>
+ /// <param name="graphicsDevice">The graphics device.</param>
+ /// <param name="byteCode">The effect code.</param>
+ protected MatrixChainEffect(GraphicsDevice graphicsDevice, byte[] byteCode)
+ : base(graphicsDevice, byteCode)
+ {
+ Initialize();
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MatrixChainEffect" /> class.
+ /// </summary>
+ /// <param name="cloneSource">The clone source.</param>
+ protected MatrixChainEffect(Effect cloneSource)
+ : base(cloneSource)
+ {
+ Initialize();
+ }
+
+ private void Initialize()
+ {
+ Flags[UseDefaultProjectionBitMask] = true;
+
+ _matrixParameter = Parameters["WorldViewProjection"];
+ }
+
+ /// <summary>
+ /// Sets the model-to-world <see cref="Matrix" />.
+ /// </summary>
+ /// <param name="world">The model-to-world <see cref="Matrix" />.</param>
+ public void SetWorld(ref Matrix world)
+ {
+ _world = world;
+ Flags[DirtyWorldViewProjectionBitMask] = true;
+ }
+
+ /// <summary>
+ /// Sets the world-to-view <see cref="Matrix" />.
+ /// </summary>
+ /// <param name="view">The world-to-view <see cref="Matrix" />.</param>
+ public void SetView(ref Matrix view)
+ {
+ _view = view;
+ Flags[DirtyWorldViewProjectionBitMask] = true;
+ }
+
+ /// <summary>
+ /// Sets the view-to-projection <see cref="Matrix" />.
+ /// </summary>
+ /// <param name="projection">The view-to-projection <see cref="Matrix" />.</param>
+ public void SetProjection(ref Matrix projection)
+ {
+ _projection = projection;
+ Flags[DirtyWorldViewProjectionBitMask] = true;
+ Flags[UseDefaultProjectionBitMask] = false;
+ }
+
+ /// <summary>
+ /// Computes derived parameter values immediately before applying the effect.
+ /// </summary>
+ protected override void OnApply()
+ {
+ base.OnApply();
+
+ // ReSharper disable once InvertIf
+ if (Flags[DirtyWorldViewProjectionBitMask] || Flags[UseDefaultProjectionBitMask])
+ {
+ if (Flags[UseDefaultProjectionBitMask])
+ {
+ var viewport = GraphicsDevice.Viewport;
+ _projection = Matrix.CreateOrthographicOffCenter(0, viewport.Width, viewport.Height, 0, 0, -1);
+ }
+
+ Matrix worldViewProjection;
+ Matrix.Multiply(ref _world, ref _view, out worldViewProjection);
+ Matrix.Multiply(ref worldViewProjection, ref _projection, out worldViewProjection);
+ _matrixParameter.SetValue(worldViewProjection);
+
+ Flags[DirtyWorldViewProjectionBitMask] = false;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.dx11.mgfxo b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.dx11.mgfxo
new file mode 100644
index 0000000..b83b6ed
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.dx11.mgfxo
Binary files differ
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.fx b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.fx
new file mode 100644
index 0000000..30a772d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.fx
@@ -0,0 +1,72 @@
+#include "Macros.fxh"
+#include "Structures.fxh"
+
+DECLARE_TEXTURE(Texture, 0);
+
+BEGIN_CONSTANTS
+
+float4 DiffuseColor = float4(1, 1, 1, 1);
+
+MATRIX_CONSTANTS
+
+float4x4 WorldViewProjection _vs(c0) _cb(c0);
+
+END_CONSTANTS
+
+VertexShaderOutputPosition VertexShaderFunctionPosition(VertexShaderInputPosition input)
+{
+ VertexShaderOutputPosition output;
+ output.Position = mul(input.Position, WorldViewProjection);
+ return output;
+}
+
+float4 PixelShaderFunctionPosition(VertexShaderOutputPosition input) : SV_Target0
+{
+ return DiffuseColor;
+}
+
+VertexShaderOutputPositionTexture VertexShaderFunctionPositionTexture(VertexShaderInputPositionTexture input)
+{
+ VertexShaderOutputPositionTexture output;
+ output.Position = mul(input.Position, WorldViewProjection);
+ output.TextureCoordinate = input.TextureCoordinate;
+ return output;
+}
+
+float4 PixelShaderFunctionPositionTexture(VertexShaderOutputPositionTexture input) : SV_Target0
+{
+ return SAMPLE_TEXTURE(Texture, input.TextureCoordinate) * DiffuseColor;
+}
+
+VertexShaderOutputPositionColor VertexShaderFunctionPositionColor(VertexShaderInputPositionColor input)
+{
+ VertexShaderOutputPositionColor output;
+ output.Position = mul(input.Position, WorldViewProjection);
+ output.Color = input.Color;
+ return output;
+}
+
+float4 PixelShaderFunctionPositionColor(VertexShaderOutputPositionColor input) : SV_Target0
+{
+ return input.Color * DiffuseColor;
+}
+
+VertexShaderOutputPositionColorTexture VertexShaderFunctionPositionColorTexture(VertexShaderInputPositionColorTexture input)
+{
+ VertexShaderOutputPositionColorTexture output;
+ output.Position = mul(input.Position, WorldViewProjection);
+ output.Color = input.Color;
+ output.TextureCoordinate = input.TextureCoordinate;
+ return output;
+}
+
+float4 PixelShaderFunctionPositionColorTexture(VertexShaderOutputPositionColorTexture input) : SV_Target0
+{
+ float4 textureColor = SAMPLE_TEXTURE(Texture, input.TextureCoordinate);
+ return textureColor * input.Color * DiffuseColor;
+}
+
+TECHNIQUE(Position, VertexShaderFunctionPosition, PixelShaderFunctionPosition);
+TECHNIQUE(PositionTexture, VertexShaderFunctionPositionTexture, PixelShaderFunctionPositionTexture);
+TECHNIQUE(PositionColor, VertexShaderFunctionPositionColor, PixelShaderFunctionPositionColor);
+TECHNIQUE(PositionColorTexture, VertexShaderFunctionPositionColorTexture, PixelShaderFunctionPositionColorTexture); \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.ogl.mgfxo b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.ogl.mgfxo
new file mode 100644
index 0000000..4f51d2a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.ogl.mgfxo
Binary files differ
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Macros.fxh b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Macros.fxh
new file mode 100644
index 0000000..c253efe
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Macros.fxh
@@ -0,0 +1,60 @@
+//-----------------------------------------------------------------------------
+// Macros.fxh
+//
+// Microsoft XNA Community Game Platform
+// Copyright (C) Microsoft Corporation. All rights reserved.
+//-----------------------------------------------------------------------------
+
+#if SM4
+
+// Macros for targetting shader model 4.0 (DX11)
+
+#define TECHNIQUE(name, vsname, psname ) \
+ technique name { pass { VertexShader = compile vs_4_0_level_9_1 vsname (); PixelShader = compile ps_4_0_level_9_1 psname(); } }
+
+#define BEGIN_CONSTANTS cbuffer Parameters : register(b0) {
+#define MATRIX_CONSTANTS
+#define END_CONSTANTS };
+
+#define _vs(r)
+#define _ps(r)
+#define _cb(r)
+
+#define DECLARE_TEXTURE(Name, index) \
+ Texture2D<float4> Name : register(t##index); \
+ sampler Name##Sampler : register(s##index)
+
+#define DECLARE_CUBEMAP(Name, index) \
+ TextureCube<float4> Name : register(t##index); \
+ sampler Name##Sampler : register(s##index)
+
+#define SAMPLE_TEXTURE(Name, texCoord) Name.Sample(Name##Sampler, texCoord)
+#define SAMPLE_CUBEMAP(Name, texCoord) Name.Sample(Name##Sampler, texCoord)
+
+
+#else
+
+// Macros for targetting shader model 2.0 (DX9)
+
+#define TECHNIQUE(name, vsname, psname ) \
+ technique name { pass { VertexShader = compile vs_2_0 vsname (); PixelShader = compile ps_2_0 psname(); } }
+
+#define BEGIN_CONSTANTS
+#define MATRIX_CONSTANTS
+#define END_CONSTANTS
+
+#define _vs(r) : register(vs, r)
+#define _ps(r) : register(ps, r)
+#define _cb(r)
+
+#define DECLARE_TEXTURE(Name, index) \
+ sampler2D Name : register(s##index);
+
+#define DECLARE_CUBEMAP(Name, index) \
+ samplerCUBE Name : register(s##index);
+
+#define SAMPLE_TEXTURE(Name, texCoord) tex2D(Name, texCoord)
+#define SAMPLE_CUBEMAP(Name, texCoord) texCUBE(Name, texCoord)
+
+
+#endif \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/RebuildEffects.bat b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/RebuildEffects.bat
new file mode 100644
index 0000000..4fc7f21
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/RebuildEffects.bat
@@ -0,0 +1,15 @@
+setlocal
+
+SET TWOMGFX="mgfxc"
+
+@for /f %%f IN ('dir /b *.fx') do (
+
+ call %TWOMGFX% %%~nf.fx %%~nf.ogl.mgfxo /Profile:OpenGL
+
+ call %TWOMGFX% %%~nf.fx %%~nf.dx11.mgfxo /Profile:DirectX_11
+
+)
+
+endlocal
+
+pause \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Structures.fxh b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Structures.fxh
new file mode 100644
index 0000000..d3af6a0
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Structures.fxh
@@ -0,0 +1,51 @@
+// Vertex shader input structures.
+
+struct VertexShaderInputPosition
+{
+ float4 Position : SV_Position;
+};
+
+struct VertexShaderInputPositionColor
+{
+ float4 Position : SV_Position;
+ float4 Color : COLOR;
+};
+
+struct VertexShaderInputPositionTexture
+{
+ float4 Position : SV_Position;
+ float2 TextureCoordinate : TEXCOORD0;
+};
+
+struct VertexShaderInputPositionColorTexture
+{
+ float4 Position : SV_Position;
+ float4 Color : COLOR;
+ float2 TextureCoordinate : TEXCOORD0;
+};
+
+// Vertex shader output structures.
+
+struct VertexShaderOutputPosition
+{
+ float4 Position : SV_Position;
+};
+
+struct VertexShaderOutputPositionColor
+{
+ float4 Position : SV_Position;
+ float4 Color : COLOR0;
+};
+
+struct VertexShaderOutputPositionTexture
+{
+ float4 Position : SV_Position;
+ float2 TextureCoordinate : TEXCOORD0;
+};
+
+struct VertexShaderOutputPositionColorTexture
+{
+ float4 Position : SV_Position;
+ float4 Color : COLOR0;
+ float2 TextureCoordinate : TEXCOORD0;
+};
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/FlipFlags.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/FlipFlags.cs
new file mode 100644
index 0000000..6c19887
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/FlipFlags.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace MonoGame.Extended.Graphics
+{
+ [Flags]
+ public enum FlipFlags : byte
+ {
+ None = 0,
+ FlipDiagonally = 1 << 0,
+ FlipVertically = 1 << 1,
+ FlipHorizontally = 1 << 2
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder.cs
new file mode 100644
index 0000000..dad4f15
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder.cs
@@ -0,0 +1,24 @@
+using System;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics.Geometry
+{
+ public abstract class GeometryBuilder<TVertexType, TIndexType>
+ where TVertexType : struct, IVertexType
+ where TIndexType : struct
+ {
+ public PrimitiveType PrimitiveType { get; protected set; }
+ public int VertexCount { get; protected set; }
+ public int IndexCount { get; protected set; }
+ public int PrimitivesCount { get; protected set; }
+
+ public TVertexType[] Vertices { get; }
+ public TIndexType[] Indices { get; }
+
+ protected GeometryBuilder(int maximumVerticesCount, int maximumIndicesCount)
+ {
+ Vertices = new TVertexType[maximumVerticesCount];
+ Indices = new TIndexType[maximumIndicesCount];
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder2D.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder2D.cs
new file mode 100644
index 0000000..c113fbe
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder2D.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Runtime.CompilerServices;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics.Geometry
+{
+ public class GeometryBuilder2D : GeometryBuilder<VertexPositionColorTexture, ushort>
+ {
+ public GeometryBuilder2D(int maximumVerticesCount, int maximumIndicesCount)
+ : base(maximumVerticesCount, maximumIndicesCount)
+ {
+ }
+
+ public void BuildSprite(int indexOffset, ref Matrix2 transformMatrix, Texture2D texture,
+ ref Rectangle sourceRectangle,
+ Color? color = null, FlipFlags flags = FlipFlags.None, float depth = 0)
+ {
+ if (texture == null)
+ throw new ArgumentNullException(nameof(texture));
+
+ var texelLeft = 0f;
+ var texelTop = 0f;
+ var texelRight = 1f;
+ var texelBottom = 1f;
+
+ if (sourceRectangle.Width > 0)
+ {
+ texelLeft = (float)sourceRectangle.X / texture.Width;
+ texelTop = (float)sourceRectangle.Y / texture.Height;
+ texelRight = (sourceRectangle.X + sourceRectangle.Width) / (float)texture.Width;
+ texelBottom = (sourceRectangle.Y + sourceRectangle.Height) / (float)texture.Height;
+ }
+ else
+ {
+ sourceRectangle.Width = texture.Width;
+ sourceRectangle.Height = texture.Height;
+ }
+
+ var color1 = color ?? Color.White;
+
+ var vertices = Vertices;
+
+ transformMatrix.Transform(0, 0, ref vertices[0].Position);
+ vertices[0].Position.Z = depth;
+ vertices[0].Color = color1;
+ vertices[0].TextureCoordinate.X = texelLeft;
+ vertices[0].TextureCoordinate.Y = texelTop;
+
+ transformMatrix.Transform(sourceRectangle.Width, 0, ref vertices[1].Position);
+ vertices[1].Position.Z = depth;
+ vertices[1].Color = color1;
+ vertices[1].TextureCoordinate.X = texelRight;
+ vertices[1].TextureCoordinate.Y = texelTop;
+
+ transformMatrix.Transform(0, sourceRectangle.Height, ref vertices[2].Position);
+ vertices[2].Position.Z = depth;
+ vertices[2].Color = color1;
+ vertices[2].TextureCoordinate.X = texelLeft;
+ vertices[2].TextureCoordinate.Y = texelBottom;
+
+ transformMatrix.Transform(sourceRectangle.Width, sourceRectangle.Height, ref vertices[3].Position);
+ vertices[3].Position.Z = depth;
+ vertices[3].Color = color1;
+ vertices[3].TextureCoordinate.X = texelRight;
+ vertices[3].TextureCoordinate.Y = texelBottom;
+
+ var flipDiagonally = (flags & FlipFlags.FlipDiagonally) != 0;
+ var flipHorizontally = (flags & FlipFlags.FlipHorizontally) != 0;
+ var flipVertically = (flags & FlipFlags.FlipVertically) != 0;
+
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref vertices[1].TextureCoordinate.X, ref vertices[2].TextureCoordinate.X);
+ FloatHelper.Swap(ref vertices[1].TextureCoordinate.Y, ref vertices[2].TextureCoordinate.Y);
+ }
+
+ if (flipHorizontally)
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref vertices[0].TextureCoordinate.Y, ref vertices[1].TextureCoordinate.Y);
+ FloatHelper.Swap(ref vertices[2].TextureCoordinate.Y, ref vertices[3].TextureCoordinate.Y);
+ }
+ else
+ {
+ FloatHelper.Swap(ref vertices[0].TextureCoordinate.X, ref vertices[1].TextureCoordinate.X);
+ FloatHelper.Swap(ref vertices[2].TextureCoordinate.X, ref vertices[3].TextureCoordinate.X);
+ }
+
+ if (flipVertically)
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref vertices[0].TextureCoordinate.X, ref vertices[2].TextureCoordinate.X);
+ FloatHelper.Swap(ref vertices[1].TextureCoordinate.X, ref vertices[3].TextureCoordinate.X);
+ }
+ else
+ {
+ FloatHelper.Swap(ref vertices[0].TextureCoordinate.Y, ref vertices[2].TextureCoordinate.Y);
+ FloatHelper.Swap(ref vertices[1].TextureCoordinate.Y, ref vertices[3].TextureCoordinate.Y);
+ }
+
+ VertexCount = 4;
+ AddQuadrilateralIndices(indexOffset);
+ IndexCount = 6;
+ PrimitivesCount = 2;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void AddQuadrilateralIndices(int indexOffset)
+ {
+ Indices[0] = (ushort)(0 + indexOffset);
+ Indices[1] = (ushort)(1 + indexOffset);
+ Indices[2] = (ushort)(2 + indexOffset);
+ Indices[3] = (ushort)(1 + indexOffset);
+ Indices[4] = (ushort)(3 + indexOffset);
+ Indices[5] = (ushort)(2 + indexOffset);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IBatchDrawCallInfo.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IBatchDrawCallInfo.cs
new file mode 100644
index 0000000..67c4755
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IBatchDrawCallInfo.cs
@@ -0,0 +1,19 @@
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics
+{
+ /// <summary>
+ /// Defines the for a deferred draw call when batching.
+ /// </summary>
+ public interface IBatchDrawCallInfo<TDrawCallInfo> where TDrawCallInfo : IBatchDrawCallInfo<TDrawCallInfo>
+ {
+ /// <summary>
+ /// Applies any state from the <see cref="IBatchDrawCallInfo{TDrawCallInfo}" /> to the
+ /// <see cref="Effect" /> or <see cref="Effect.GraphicsDevice"/>.
+ /// </summary>
+ /// <param name="effect">The effect.</param>
+ void SetState(Effect effect);
+
+ bool TryMerge(ref TDrawCallInfo drawCall);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IMatrixChainEffect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IMatrixChainEffect.cs
new file mode 100644
index 0000000..da8ccb9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IMatrixChainEffect.cs
@@ -0,0 +1,30 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics
+{
+ /// <summary>
+ /// Defines an <see cref="Effect" /> that uses the standard chain of matrix transformations to represent a 3D object on
+ /// a 2D monitor.
+ /// </summary>
+ public interface IMatrixChainEffect : IEffectMatrices
+ {
+ /// <summary>
+ /// Sets the model-to-world <see cref="Matrix" />.
+ /// </summary>
+ /// <param name="world">The model-to-world <see cref="Matrix" />.</param>
+ void SetWorld(ref Matrix world);
+
+ /// <summary>
+ /// Sets the world-to-view <see cref="Matrix" />.
+ /// </summary>
+ /// <param name="view">The world-to-view <see cref="Matrix" />.</param>
+ void SetView(ref Matrix view);
+
+ /// <summary>
+ /// Sets the view-to-projection <see cref="Matrix" />.
+ /// </summary>
+ /// <param name="projection">The view-to-projection <see cref="Matrix" />.</param>
+ void SetProjection(ref Matrix projection);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/MonoGame.Extended.Graphics.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/MonoGame.Extended.Graphics.csproj
new file mode 100644
index 0000000..fd25850
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/MonoGame.Extended.Graphics.csproj
@@ -0,0 +1,28 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>Graphics makes MonoGame more awesome.</Description>
+ <PackageTags>monogame graphics batcher effects</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <None Remove="Effects\Resources\DefaultEffect.dx11.mgfxo" />
+ <None Remove="Effects\Resources\DefaultEffect.fx" />
+ <None Remove="Effects\Resources\DefaultEffect.ogl.mgfxo" />
+ <None Remove="Effects\Resources\Macros.fxh" />
+ <None Remove="Effects\Resources\Structures.fxh" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <EmbeddedResource Include="Effects\Resources\DefaultEffect.dx11.mgfxo" />
+ <EmbeddedResource Include="Effects\Resources\DefaultEffect.fx" />
+ <EmbeddedResource Include="Effects\Resources\DefaultEffect.ogl.mgfxo" />
+ <EmbeddedResource Include="Effects\Resources\Macros.fxh" />
+ <EmbeddedResource Include="Effects\Resources\Structures.fxh" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/PrimitiveTypeExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/PrimitiveTypeExtensions.cs
new file mode 100644
index 0000000..68a39ef
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/PrimitiveTypeExtensions.cs
@@ -0,0 +1,42 @@
+using System;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics
+{
+ public static class PrimitiveTypeExtensions
+ {
+ internal static int GetPrimitivesCount(this PrimitiveType primitiveType, int verticesCount)
+ {
+ switch (primitiveType)
+ {
+ case PrimitiveType.LineStrip:
+ return verticesCount - 1;
+ case PrimitiveType.LineList:
+ return verticesCount/2;
+ case PrimitiveType.TriangleStrip:
+ return verticesCount - 2;
+ case PrimitiveType.TriangleList:
+ return verticesCount/3;
+ default:
+ throw new ArgumentException("Invalid primitive type.");
+ }
+ }
+
+ internal static int GetVerticesCount(this PrimitiveType primitiveType, int primitivesCount)
+ {
+ switch (primitiveType)
+ {
+ case PrimitiveType.LineStrip:
+ return primitivesCount + 1;
+ case PrimitiveType.LineList:
+ return primitivesCount*2;
+ case PrimitiveType.TriangleStrip:
+ return primitivesCount + 2;
+ case PrimitiveType.TriangleList:
+ return primitivesCount*3;
+ default:
+ throw new ArgumentException("Invalid primitive type.");
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/RenderTarget2DExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/RenderTarget2DExtensions.cs
new file mode 100644
index 0000000..f090603
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/RenderTarget2DExtensions.cs
@@ -0,0 +1,41 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Graphics
+{
+ public static class RenderTarget2DExtensions
+ {
+ public static IDisposable BeginDraw(this RenderTarget2D renderTarget, GraphicsDevice graphicsDevice,
+ Color backgroundColor)
+ {
+ return new RenderTargetOperation(renderTarget, graphicsDevice, backgroundColor);
+ }
+
+ private class RenderTargetOperation : IDisposable
+ {
+ private readonly GraphicsDevice _graphicsDevice;
+ private readonly RenderTargetUsage _previousRenderTargetUsage;
+ private readonly Viewport _viewport;
+
+ public RenderTargetOperation(RenderTarget2D renderTarget, GraphicsDevice graphicsDevice,
+ Color backgroundColor)
+ {
+ _graphicsDevice = graphicsDevice;
+ _viewport = _graphicsDevice.Viewport;
+ _previousRenderTargetUsage = _graphicsDevice.PresentationParameters.RenderTargetUsage;
+
+ _graphicsDevice.PresentationParameters.RenderTargetUsage = RenderTargetUsage.PreserveContents;
+ _graphicsDevice.SetRenderTarget(renderTarget);
+ _graphicsDevice.Clear(backgroundColor);
+ }
+
+ public void Dispose()
+ {
+ _graphicsDevice.SetRenderTarget(null);
+ _graphicsDevice.PresentationParameters.RenderTargetUsage = _previousRenderTargetUsage;
+ _graphicsDevice.Viewport = _viewport;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ControlStyle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ControlStyle.cs
new file mode 100644
index 0000000..1593da3
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ControlStyle.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using MonoGame.Extended.Gui.Controls;
+
+namespace MonoGame.Extended.Gui
+{
+ public class ControlStyle : IDictionary<string, object>
+ {
+ private readonly Dictionary<Guid, Dictionary<string, object>> _previousStates = new Dictionary<Guid, Dictionary<string, object>>();
+
+ public ControlStyle()
+ : this(typeof(Element))
+ {
+ }
+
+ public ControlStyle(Type targetType)
+ : this(targetType.FullName, targetType)
+ {
+ }
+
+ public ControlStyle(string name, Type targetType)
+ {
+ Name = name;
+ TargetType = targetType;
+ _setters = new Dictionary<string, object>();
+ }
+
+ public string Name { get; }
+ public Type TargetType { get; set; }
+
+ private readonly Dictionary<string, object> _setters;
+
+ public void ApplyIf(Control control, bool predicate)
+ {
+ if (predicate)
+ Apply(control);
+ else
+ Revert(control);
+ }
+
+ public void Apply(Control control)
+ {
+ _previousStates[control.Id] = _setters
+ .ToDictionary(i => i.Key, i => TargetType.GetRuntimeProperty(i.Key)?.GetValue(control));
+
+ ChangePropertyValues(control, _setters);
+ }
+
+ public void Revert(Control control)
+ {
+ if (_previousStates.ContainsKey(control.Id) && _previousStates[control.Id] != null)
+ ChangePropertyValues(control, _previousStates[control.Id]);
+
+ _previousStates[control.Id] = null;
+ }
+
+ private static void ChangePropertyValues(Control control, Dictionary<string, object> setters)
+ {
+ var targetType = control.GetType();
+
+ foreach (var propertyName in setters.Keys)
+ {
+ var propertyInfo = targetType.GetRuntimeProperty(propertyName);
+ var value = setters[propertyName];
+
+ if (propertyInfo != null)
+ {
+ if(propertyInfo.CanWrite)
+ propertyInfo.SetValue(control, value);
+
+ // special case when we have a list of items as objects (like on a list box)
+ if (propertyInfo.PropertyType == typeof(List<object>))
+ {
+ var items = (List<object>)value;
+ var addMethod = propertyInfo.PropertyType.GetRuntimeMethod("Add", new[] { typeof(object) });
+
+ foreach (var item in items)
+ addMethod.Invoke(propertyInfo.GetValue(control), new[] {item});
+ }
+ }
+ }
+ }
+
+ public object this[string key]
+ {
+ get { return _setters[key]; }
+ set { _setters[key] = value; }
+ }
+
+ public ICollection<string> Keys => _setters.Keys;
+ public ICollection<object> Values => _setters.Values;
+ public int Count => _setters.Count;
+ public bool IsReadOnly => false;
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ public IEnumerator<KeyValuePair<string, object>> GetEnumerator() => _setters.GetEnumerator();
+ public void Add(string key, object value) => _setters.Add(key, value);
+ public void Add(KeyValuePair<string, object> item) => _setters.Add(item.Key, item.Value);
+ public bool Remove(string key) => _setters.Remove(key);
+ public bool Remove(KeyValuePair<string, object> item) => _setters.Remove(item.Key);
+ public void Clear() => _setters.Clear();
+ public bool Contains(KeyValuePair<string, object> item) => _setters.Contains(item);
+ public bool ContainsKey(string key) => _setters.ContainsKey(key);
+ public bool TryGetValue(string key, out object value) => _setters.TryGetValue(key, out value);
+
+ public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
+ {
+ throw new NotSupportedException();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Box.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Box.cs
new file mode 100644
index 0000000..1e5c06e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Box.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class Box : Control
+ {
+ public override IEnumerable<Control> Children { get; } = Enumerable.Empty<Control>();
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ return new Size(Width, Height);
+ }
+
+ public Color FillColor { get; set; } = Color.White;
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+ renderer.FillRectangle(ContentRectangle, FillColor);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Button.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Button.cs
new file mode 100644
index 0000000..310dc00
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Button.cs
@@ -0,0 +1,92 @@
+using System;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class Button : ContentControl
+ {
+ public Button()
+ {
+ }
+
+ public event EventHandler Clicked;
+ public event EventHandler PressedStateChanged;
+
+ private bool _isPressed;
+ public bool IsPressed
+ {
+ get => _isPressed;
+ set
+ {
+ if (_isPressed != value)
+ {
+ _isPressed = value;
+ PressedStyle?.ApplyIf(this, _isPressed);
+ PressedStateChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ private ControlStyle _pressedStyle;
+ public ControlStyle PressedStyle
+ {
+ get => _pressedStyle;
+ set
+ {
+ if (_pressedStyle != value)
+ {
+ _pressedStyle = value;
+ PressedStyle?.ApplyIf(this, _isPressed);
+ }
+ }
+ }
+
+ private bool _isPointerDown;
+
+ public override bool OnPointerDown(IGuiContext context, PointerEventArgs args)
+ {
+ if (IsEnabled)
+ {
+ _isPointerDown = true;
+ IsPressed = true;
+ }
+
+ return base.OnPointerDown(context, args);
+ }
+
+ public override bool OnPointerUp(IGuiContext context, PointerEventArgs args)
+ {
+ _isPointerDown = false;
+
+ if (IsPressed)
+ {
+ IsPressed = false;
+
+ if (BoundingRectangle.Contains(args.Position) && IsEnabled)
+ Click();
+ }
+
+ return base.OnPointerUp(context, args);
+ }
+
+ public override bool OnPointerEnter(IGuiContext context, PointerEventArgs args)
+ {
+ if (IsEnabled && _isPointerDown)
+ IsPressed = true;
+
+ return base.OnPointerEnter(context, args);
+ }
+
+ public override bool OnPointerLeave(IGuiContext context, PointerEventArgs args)
+ {
+ if (IsEnabled)
+ IsPressed = false;
+
+ return base.OnPointerLeave(context, args);
+ }
+
+ public void Click()
+ {
+ Clicked?.Invoke(this, EventArgs.Empty);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Canvas.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Canvas.cs
new file mode 100644
index 0000000..d667a73
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Canvas.cs
@@ -0,0 +1,25 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class Canvas : LayoutControl
+ {
+ public Canvas()
+ {
+ }
+
+ protected override void Layout(IGuiContext context, Rectangle rectangle)
+ {
+ foreach (var control in Items)
+ {
+ var actualSize = control.CalculateActualSize(context);
+ PlaceControl(context, control, control.Position.X, control.Position.Y, actualSize.Width, actualSize.Height);
+ }
+ }
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ return new Size();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CheckBox.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CheckBox.cs
new file mode 100644
index 0000000..c6c375f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CheckBox.cs
@@ -0,0 +1,65 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class CheckBox : CompositeControl
+ {
+ public CheckBox()
+ {
+ _contentLabel = new Label();
+ _checkLabel = new Box {Width = 20, Height = 20};
+
+ _toggleButton = new ToggleButton
+ {
+ Margin = 0,
+ Padding = 0,
+ BackgroundColor = Color.Transparent,
+ BorderThickness = 0,
+ HoverStyle = null,
+ CheckedStyle = null,
+ PressedStyle = null,
+ Content = new StackPanel
+ {
+ Margin = 0,
+ Orientation = Orientation.Horizontal,
+ Items =
+ {
+ _checkLabel,
+ _contentLabel
+ }
+ }
+ };
+
+ _toggleButton.CheckedStateChanged += (sender, args) => OnIsCheckedChanged();
+ Template = _toggleButton;
+ OnIsCheckedChanged();
+ }
+
+ private readonly Label _contentLabel;
+ private readonly ToggleButton _toggleButton;
+ private readonly Box _checkLabel;
+
+ protected override Control Template { get; }
+
+ public override object Content
+ {
+ get => _contentLabel.Content;
+ set => _contentLabel.Content = value;
+ }
+
+ public bool IsChecked
+ {
+ get => _toggleButton.IsChecked;
+ set
+ {
+ _toggleButton.IsChecked = value;
+ OnIsCheckedChanged();
+ }
+ }
+
+ private void OnIsCheckedChanged()
+ {
+ _checkLabel.FillColor = IsChecked ? Color.White : Color.Transparent;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ComboBox.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ComboBox.cs
new file mode 100644
index 0000000..f8867d2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ComboBox.cs
@@ -0,0 +1,102 @@
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGame.Extended.Input.InputListeners;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class ComboBox : SelectorControl
+ {
+ public ComboBox()
+ {
+ }
+
+ public bool IsOpen { get; set; }
+ public TextureRegion2D DropDownRegion { get; set; }
+ public Color DropDownColor { get; set; } = Color.White;
+
+ public override bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args)
+ {
+ if (args.Key == Keys.Enter)
+ IsOpen = false;
+
+ return base.OnKeyPressed(context, args);
+ }
+
+ public override bool OnPointerUp(IGuiContext context, PointerEventArgs args)
+ {
+ IsOpen = !IsOpen;
+ return base.OnPointerUp(context, args);
+ }
+
+ protected override Rectangle GetListAreaRectangle(IGuiContext context)
+ {
+ return GetDropDownRectangle(context);
+ }
+
+ public override bool Contains(IGuiContext context, Point point)
+ {
+ return base.Contains(context, point) || IsOpen && GetListAreaRectangle(context).Contains(point);
+ }
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var width = 0;
+ var height = 0;
+
+ foreach (var item in Items)
+ {
+ var itemSize = GetItemSize(context, item);
+
+ if (itemSize.Width > width)
+ width = itemSize.Width;
+
+ if (itemSize.Height > height)
+ height = itemSize.Height;
+ }
+
+ return new Size(width + ClipPadding.Width, height + ClipPadding.Height);
+ }
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+
+ if (IsOpen)
+ {
+ var dropDownRectangle = GetListAreaRectangle(context);
+
+ if (DropDownRegion != null)
+ {
+ renderer.DrawRegion(DropDownRegion, dropDownRectangle, DropDownColor);
+ }
+ else
+ {
+ renderer.FillRectangle(dropDownRectangle, DropDownColor);
+ renderer.DrawRectangle(dropDownRectangle, BorderColor);
+ }
+
+ DrawItemList(context, renderer);
+ }
+
+ var selectedTextInfo = GetItemTextInfo(context, ContentRectangle, SelectedItem);
+
+ if (!string.IsNullOrWhiteSpace(selectedTextInfo.Text))
+ renderer.DrawText(selectedTextInfo.Font, selectedTextInfo.Text, selectedTextInfo.Position + TextOffset, selectedTextInfo.Color, selectedTextInfo.ClippingRectangle);
+ }
+
+ private Rectangle GetDropDownRectangle(IGuiContext context)
+ {
+ var dropDownRectangle = BoundingRectangle;
+
+ dropDownRectangle.Y = dropDownRectangle.Y + dropDownRectangle.Height;
+ dropDownRectangle.Height = (int) Items
+ .Select(item => GetItemSize(context, item))
+ .Select(itemSize => itemSize.Height)
+ .Sum();
+
+ return dropDownRectangle;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CompositeControl.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CompositeControl.cs
new file mode 100644
index 0000000..8217c69
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CompositeControl.cs
@@ -0,0 +1,70 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public abstract class CompositeControl : Control
+ {
+ protected CompositeControl()
+ {
+ }
+
+ protected bool IsDirty { get; set; } = true;
+
+ public abstract object Content { get; set; }
+
+ protected abstract Control Template { get; }
+
+ public override IEnumerable<Control> Children
+ {
+ get
+ {
+ var control = Template;
+
+ if (control != null)
+ yield return control;
+ }
+ }
+
+ public override void InvalidateMeasure()
+ {
+ base.InvalidateMeasure();
+ IsDirty = true;
+ }
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var control = Template;
+
+ if (control != null)
+ return Template.CalculateActualSize(context);
+
+ return Size.Empty;
+ }
+
+ public override void Update(IGuiContext context, float deltaSeconds)
+ {
+ var control = Template;
+
+ if (control != null)
+ {
+ if (IsDirty)
+ {
+ control.Parent = this;
+ control.ActualSize = ContentRectangle.Size;
+ control.Position = new Point(Padding.Left, Padding.Top);
+ control.InvalidateMeasure();
+ IsDirty = false;
+ }
+ }
+ }
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+
+ var control = Template;
+ control?.Draw(context, renderer, deltaSeconds);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ContentControl.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ContentControl.cs
new file mode 100644
index 0000000..932f8e4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ContentControl.cs
@@ -0,0 +1,82 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class ContentControl : Control
+ {
+ private bool _contentChanged = true;
+
+ private object _content;
+ public object Content
+ {
+ get => _content;
+ set
+ {
+ if (_content != value)
+ {
+ _content = value;
+ _contentChanged = true;
+ }
+ }
+ }
+
+ public override IEnumerable<Control> Children
+ {
+ get
+ {
+ if (Content is Control control)
+ yield return control;
+ }
+ }
+
+ public bool HasContent => Content == null;
+
+ public override void InvalidateMeasure()
+ {
+ base.InvalidateMeasure();
+ _contentChanged = true;
+ }
+
+ public override void Update(IGuiContext context, float deltaSeconds)
+ {
+ if (_content is Control control && _contentChanged)
+ {
+ control.Parent = this;
+ control.ActualSize = ContentRectangle.Size;
+ control.Position = new Point(Padding.Left, Padding.Top);
+ control.InvalidateMeasure();
+ _contentChanged = false;
+ }
+ }
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+
+ if (Content is Control control)
+ {
+ control.Draw(context, renderer, deltaSeconds);
+ }
+ else
+ {
+ var text = Content?.ToString();
+ var textInfo = GetTextInfo(context, text, ContentRectangle, HorizontalTextAlignment, VerticalTextAlignment);
+
+ if (!string.IsNullOrWhiteSpace(textInfo.Text))
+ renderer.DrawText(textInfo.Font, textInfo.Text, textInfo.Position + TextOffset, textInfo.Color, textInfo.ClippingRectangle);
+ }
+ }
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ if (Content is Control control)
+ return control.CalculateActualSize(context);
+
+ var text = Content?.ToString();
+ var font = Font ?? context.DefaultFont;
+ return (Size)font.MeasureString(text ?? string.Empty);
+ }
+ }
+
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Control.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Control.cs
new file mode 100644
index 0000000..5440fdf
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Control.cs
@@ -0,0 +1,271 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.BitmapFonts;
+using MonoGame.Extended.Input.InputListeners;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public abstract class Control : Element<Control>, IRectangular
+ {
+ protected Control()
+ {
+ BackgroundColor = Color.White;
+ TextColor = Color.White;
+ IsEnabled = true;
+ IsVisible = true;
+ Origin = Point.Zero;
+ Skin = Skin.Default;
+ }
+
+ private Skin _skin;
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Skin Skin
+ {
+ get => _skin;
+ set
+ {
+ if (_skin != value)
+ {
+ _skin = value;
+ _skin?.Apply(this);
+ }
+ }
+ }
+
+ public abstract IEnumerable<Control> Children { get; }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Thickness Margin { get; set; }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Thickness Padding { get; set; }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Thickness ClipPadding { get; set; }
+
+ public bool IsLayoutRequired { get; set; }
+
+ public Rectangle ClippingRectangle
+ {
+ get
+ {
+ var r = BoundingRectangle;
+ return new Rectangle(r.Left + ClipPadding.Left, r.Top + ClipPadding.Top, r.Width - ClipPadding.Width, r.Height - ClipPadding.Height);
+ }
+ }
+
+ public Rectangle ContentRectangle
+ {
+ get
+ {
+ var r = BoundingRectangle;
+ return new Rectangle(r.Left + Padding.Left, r.Top + Padding.Top, r.Width - Padding.Width, r.Height - Padding.Height);
+ }
+ }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public bool IsFocused { get; set; }
+
+ private bool _isHovered;
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public bool IsHovered
+ {
+ get => _isHovered;
+ private set
+ {
+ if (_isHovered != value)
+ {
+ _isHovered = value;
+ HoverStyle?.ApplyIf(this, _isHovered);
+ }
+ }
+ }
+
+ [JsonIgnore]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Guid Id { get; set; } = Guid.NewGuid();
+
+ public Vector2 Offset { get; set; }
+ public BitmapFont Font { get; set; }
+ public Color TextColor { get; set; }
+ public Vector2 TextOffset { get; set; }
+ public HorizontalAlignment HorizontalAlignment { get; set; } = HorizontalAlignment.Stretch;
+ public VerticalAlignment VerticalAlignment { get; set; } = VerticalAlignment.Stretch;
+ public HorizontalAlignment HorizontalTextAlignment { get; set; } = HorizontalAlignment.Centre;
+ public VerticalAlignment VerticalTextAlignment { get; set; } = VerticalAlignment.Centre;
+
+ public abstract Size GetContentSize(IGuiContext context);
+
+ public virtual Size CalculateActualSize(IGuiContext context)
+ {
+ var fixedSize = Size;
+ var desiredSize = GetContentSize(context) + Margin.Size + Padding.Size;
+
+ if (desiredSize.Width < MinWidth)
+ desiredSize.Width = MinWidth;
+
+ if (desiredSize.Height < MinHeight)
+ desiredSize.Height = MinHeight;
+
+ if (desiredSize.Width > MaxWidth)
+ desiredSize.Width = MaxWidth;
+
+ if (desiredSize.Height > MaxWidth)
+ desiredSize.Height = MaxHeight;
+
+ var width = fixedSize.Width == 0 ? desiredSize.Width : fixedSize.Width;
+ var height = fixedSize.Height == 0 ? desiredSize.Height : fixedSize.Height;
+ return new Size(width, height);
+ }
+
+ public virtual void InvalidateMeasure() { }
+
+ private bool _isEnabled;
+ public bool IsEnabled
+ {
+ get => _isEnabled;
+ set
+ {
+ if (_isEnabled != value)
+ {
+ _isEnabled = value;
+ DisabledStyle?.ApplyIf(this, !_isEnabled);
+ }
+ }
+ }
+
+ public bool IsVisible { get; set; }
+
+ private ControlStyle _hoverStyle;
+ public ControlStyle HoverStyle
+ {
+ get => _hoverStyle;
+ set
+ {
+ if (_hoverStyle != value)
+ {
+ _hoverStyle = value;
+ HoverStyle?.ApplyIf(this, _isHovered);
+ }
+ }
+ }
+
+ private ControlStyle _disabledStyle;
+ public ControlStyle DisabledStyle
+ {
+ get => _disabledStyle;
+ set
+ {
+ _disabledStyle = value;
+ DisabledStyle?.ApplyIf(this, !_isEnabled);
+ }
+ }
+
+ public virtual void OnScrolled(int delta) { }
+
+ public virtual bool OnKeyTyped(IGuiContext context, KeyboardEventArgs args) { return true; }
+ public virtual bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args) { return true; }
+
+ public virtual bool OnFocus(IGuiContext context) { return true; }
+ public virtual bool OnUnfocus(IGuiContext context) { return true; }
+
+ public virtual bool OnPointerDown(IGuiContext context, PointerEventArgs args) { return true; }
+ public virtual bool OnPointerMove(IGuiContext context, PointerEventArgs args) { return true; }
+ public virtual bool OnPointerUp(IGuiContext context, PointerEventArgs args) { return true; }
+
+ public virtual bool OnPointerEnter(IGuiContext context, PointerEventArgs args)
+ {
+ if (IsEnabled && !IsHovered)
+ IsHovered = true;
+
+ return true;
+ }
+
+ public virtual bool OnPointerLeave(IGuiContext context, PointerEventArgs args)
+ {
+ if (IsEnabled && IsHovered)
+ IsHovered = false;
+
+ return true;
+ }
+
+ public virtual bool Contains(IGuiContext context, Point point)
+ {
+ return BoundingRectangle.Contains(point);
+ }
+
+ public virtual void Update(IGuiContext context, float deltaSeconds) { }
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ if (BackgroundRegion != null)
+ renderer.DrawRegion(BackgroundRegion, BoundingRectangle, BackgroundColor);
+ else if (BackgroundColor != Color.Transparent)
+ renderer.FillRectangle(BoundingRectangle, BackgroundColor);
+
+ if (BorderThickness != 0)
+ renderer.DrawRectangle(BoundingRectangle, BorderColor, BorderThickness);
+
+ // handy debug rectangles
+ //renderer.DrawRectangle(ContentRectangle, Color.Magenta);
+ //renderer.DrawRectangle(BoundingRectangle, Color.Lime);
+ }
+
+ public bool HasParent(Control control)
+ {
+ return Parent != null && (Parent == control || Parent.HasParent(control));
+ }
+
+ protected TextInfo GetTextInfo(IGuiContext context, string text, Rectangle targetRectangle, HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment)
+ {
+ var font = Font ?? context.DefaultFont;
+ var textSize = (Size)font.GetStringRectangle(text ?? string.Empty, Vector2.Zero).Size;
+ var destinationRectangle = LayoutHelper.AlignRectangle(horizontalAlignment, verticalAlignment, textSize, targetRectangle);
+ var textPosition = destinationRectangle.Location.ToVector2();
+ var textInfo = new TextInfo(text, font, textPosition, textSize, TextColor, targetRectangle);
+ return textInfo;
+ }
+
+ public struct TextInfo
+ {
+ public TextInfo(string text, BitmapFont font, Vector2 position, Size size, Color color, Rectangle? clippingRectangle)
+ {
+ Text = text ?? string.Empty;
+ Font = font;
+ Size = size;
+ Color = color;
+ ClippingRectangle = clippingRectangle;
+ Position = position;
+ }
+
+ public string Text;
+ public BitmapFont Font;
+ public Size Size;
+ public Color Color;
+ public Rectangle? ClippingRectangle;
+ public Vector2 Position;
+ }
+
+ public Dictionary<string, object> AttachedProperties { get; } = new Dictionary<string, object>();
+
+ public object GetAttachedProperty(string name)
+ {
+ return AttachedProperties.TryGetValue(name, out var value) ? value : null;
+ }
+
+ public void SetAttachedProperty(string name, object value)
+ {
+ AttachedProperties[name] = value;
+ }
+
+ public virtual Type GetAttachedPropertyType(string propertyName)
+ {
+ return null;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ControlCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ControlCollection.cs
new file mode 100644
index 0000000..6135b4c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ControlCollection.cs
@@ -0,0 +1,15 @@
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class ControlCollection : ElementCollection<Control, Control>
+ {
+ public ControlCollection()
+ : base(null)
+ {
+ }
+
+ public ControlCollection(Control parent)
+ : base(parent)
+ {
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Dialog.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Dialog.cs
new file mode 100644
index 0000000..26f0fca
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Dialog.cs
@@ -0,0 +1,41 @@
+using System.Linq;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ //public class Dialog : LayoutControl
+ //{
+ // public Dialog()
+ // {
+ // HorizontalAlignment = HorizontalAlignment.Centre;
+ // VerticalAlignment = VerticalAlignment.Centre;
+ // }
+
+ // public Thickness Padding { get; set; }
+ // public Screen Owner { get; private set; }
+
+ // public void Show(Screen owner)
+ // {
+ // Owner = owner;
+ // Owner.Controls.Add(this);
+ // }
+
+ // public void Hide()
+ // {
+ // Owner.Controls.Remove(this);
+ // }
+
+ // protected override Size2 CalculateDesiredSize(IGuiContext context, Size2 availableSize)
+ // {
+ // var sizes = Items.Select(control => LayoutHelper.GetSizeWithMargins(control, context, availableSize)).ToArray();
+ // var width = sizes.Max(s => s.Width);
+ // var height = sizes.Max(s => s.Height);
+ // return new Size2(width, height) + Padding.Size;
+ // }
+
+ // public override void Layout(IGuiContext context, RectangleF rectangle)
+ // {
+ // foreach (var control in Items)
+ // PlaceControl(context, control, Padding.Left, Padding.Top, Width - Padding.Size.Width, Height - Padding.Size.Height);
+ // }
+ //}
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/DockPanel.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/DockPanel.cs
new file mode 100644
index 0000000..7dfe1e6
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/DockPanel.cs
@@ -0,0 +1,104 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public enum Dock
+ {
+ Left, Right, Top, Bottom
+ }
+
+ public class DockPanel : LayoutControl
+ {
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var size = new Size();
+
+ for (var i = 0; i < Items.Count; i++)
+ {
+ var control = Items[i];
+ var actualSize = control.CalculateActualSize(context);
+
+ if (LastChildFill && i == Items.Count - 1)
+ {
+ size.Width += actualSize.Width;
+ size.Height += actualSize.Height;
+ }
+ else
+ {
+ var dock = control.GetAttachedProperty(DockProperty) as Dock? ?? Dock.Left;
+
+ switch (dock)
+ {
+ case Dock.Left:
+ case Dock.Right:
+ size.Width += actualSize.Width;
+ break;
+ case Dock.Top:
+ case Dock.Bottom:
+ size.Height += actualSize.Height;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+ }
+
+ return size;
+ }
+
+ protected override void Layout(IGuiContext context, Rectangle rectangle)
+ {
+ for (var i = 0; i < Items.Count; i++)
+ {
+ var control = Items[i];
+
+ if (LastChildFill && i == Items.Count - 1)
+ {
+ PlaceControl(context, control, rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
+ }
+ else
+ {
+ var actualSize = control.CalculateActualSize(context);
+ var dock = control.GetAttachedProperty(DockProperty) as Dock? ?? Dock.Left;
+
+ switch (dock)
+ {
+ case Dock.Left:
+ PlaceControl(context, control, rectangle.Left, rectangle.Top, actualSize.Width, rectangle.Height);
+ rectangle.X += actualSize.Width;
+ rectangle.Width -= actualSize.Width;
+ break;
+ case Dock.Right:
+ PlaceControl(context, control, rectangle.Right - actualSize.Width, rectangle.Top, actualSize.Width, rectangle.Height);
+ rectangle.Width -= actualSize.Width;
+ break;
+ case Dock.Top:
+ PlaceControl(context, control, rectangle.Left, rectangle.Top, rectangle.Width, actualSize.Height);
+ rectangle.Y += actualSize.Height;
+ rectangle.Height -= actualSize.Height;
+ break;
+ case Dock.Bottom:
+ PlaceControl(context, control, rectangle.Left, rectangle.Bottom - actualSize.Height, rectangle.Width, actualSize.Height);
+ rectangle.Height -= actualSize.Height;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+ }
+ }
+
+ public const string DockProperty = "Dock";
+
+ public override Type GetAttachedPropertyType(string propertyName)
+ {
+ if (string.Equals(DockProperty, propertyName, StringComparison.OrdinalIgnoreCase))
+ return typeof(Dock);
+
+ return base.GetAttachedPropertyType(propertyName);
+ }
+
+ public bool LastChildFill { get; set; } = true;
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Form.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Form.cs
new file mode 100644
index 0000000..97b984d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Form.cs
@@ -0,0 +1,42 @@
+using System.Linq;
+using MonoGame.Extended.Input.InputListeners;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ //public class Form : StackPanel
+ //{
+ // public Form()
+ // {
+ // }
+
+ // public override bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args)
+ // {
+ // if (args.Key == Keys.Tab)
+ // {
+ // var controls = FindControls<Control>();
+ // var index = controls.IndexOf(context.FocusedControl);
+ // if (index > -1)
+ // {
+ // index++;
+ // if (index >= controls.Count) index = 0;
+ // context.SetFocus(controls[index]);
+ // return true;
+ // }
+ // }
+
+ // if (args.Key == Keys.Enter)
+ // {
+ // var controls = FindControls<Submit>();
+ // if (controls.Count > 0)
+ // {
+ // var submit = controls.FirstOrDefault();
+ // submit.TriggerClicked();
+ // return true;
+ // }
+ // }
+
+ // return base.OnKeyPressed(context, args);
+ // }
+ //}
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ItemsControl.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ItemsControl.cs
new file mode 100644
index 0000000..f0babaf
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ItemsControl.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public abstract class ItemsControl : Control
+ {
+ protected ItemsControl()
+ {
+ Items = new ControlCollection(this);
+ //{
+ // ItemAdded = x => UpdateRootIsLayoutRequired(),
+ // ItemRemoved = x => UpdateRootIsLayoutRequired()
+ //};
+ }
+
+ public override IEnumerable<Control> Children => Items;
+
+ public ControlCollection Items { get; }
+
+ ///// <summary>
+ ///// Recursive Method to find the root element and update the IsLayoutRequired property. So that the screen knows that something in the controls
+ ///// have had a change to their layout. Also, it will reset the size of the element so that it can get a clean build so that the background patches
+ ///// can be rendered with the updates.
+ ///// </summary>
+ //private void UpdateRootIsLayoutRequired()
+ //{
+ // var parent = Parent as ItemsControl;
+
+ // if (parent == null)
+ // IsLayoutRequired = true;
+ // else
+ // parent.UpdateRootIsLayoutRequired();
+
+ // Size = Size2.Empty;
+ //}
+
+ //protected List<T> FindControls<T>()
+ // where T : Control
+ //{
+ // return FindControls<T>(Items);
+ //}
+
+ //protected List<T> FindControls<T>(ControlCollection controls)
+ // where T : Control
+ //{
+ // var results = new List<T>();
+ // foreach (var control in controls)
+ // {
+ // if (control is T)
+ // results.Add(control as T);
+
+ // var itemsControl = control as ItemsControl;
+
+ // if (itemsControl != null && itemsControl.Items.Any())
+ // results = results.Concat(FindControls<T>(itemsControl.Items)).ToList();
+ // }
+ // return results;
+ //}
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Label.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Label.cs
new file mode 100644
index 0000000..b949148
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Label.cs
@@ -0,0 +1,14 @@
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class Label : ContentControl
+ {
+ public Label()
+ {
+ }
+
+ public Label(string text = null)
+ {
+ Content = text ?? string.Empty;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/LayoutControl.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/LayoutControl.cs
new file mode 100644
index 0000000..3935baa
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/LayoutControl.cs
@@ -0,0 +1,40 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public abstract class LayoutControl : ItemsControl
+ {
+ protected LayoutControl()
+ {
+ HorizontalAlignment = HorizontalAlignment.Stretch;
+ VerticalAlignment = VerticalAlignment.Stretch;
+ BackgroundColor = Color.Transparent;
+ }
+
+ private bool _isLayoutValid;
+
+ public override void InvalidateMeasure()
+ {
+ base.InvalidateMeasure();
+ _isLayoutValid = false;
+ }
+
+ public override void Update(IGuiContext context, float deltaSeconds)
+ {
+ base.Update(context, deltaSeconds);
+
+ if (!_isLayoutValid)
+ {
+ Layout(context, new Rectangle(Padding.Left, Padding.Top, ContentRectangle.Width, ContentRectangle.Height));
+ _isLayoutValid = true;
+ }
+ }
+
+ protected abstract void Layout(IGuiContext context, Rectangle rectangle);
+
+ protected static void PlaceControl(IGuiContext context, Control control, float x, float y, float width, float height)
+ {
+ LayoutHelper.PlaceControl(context, control, x, y, width, height);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ListBox.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ListBox.cs
new file mode 100644
index 0000000..d456e07
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ListBox.cs
@@ -0,0 +1,42 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class ListBox : SelectorControl
+ {
+ public ListBox()
+ {
+ }
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var width = 0;
+ var height = 0;
+
+ foreach (var item in Items)
+ {
+ var itemSize = GetItemSize(context, item);
+
+ if (itemSize.Width > width)
+ width = itemSize.Width;
+
+ height += itemSize.Height;
+ }
+
+ return new Size(width + ClipPadding.Width, height + ClipPadding.Height);
+ }
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+
+ ScrollIntoView(context);
+ DrawItemList(context, renderer);
+ }
+
+ protected override Rectangle GetListAreaRectangle(IGuiContext context)
+ {
+ return ContentRectangle;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ProgressBar.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ProgressBar.cs
new file mode 100644
index 0000000..6983d09
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ProgressBar.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class ProgressBar : Control
+ {
+ public ProgressBar()
+ {
+ }
+
+ private float _progress = 1.0f;
+ public float Progress
+ {
+ get { return _progress; }
+ set
+ {
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if(_progress != value)
+ {
+ _progress = value;
+ ProgressChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ public TextureRegion2D BarRegion { get; set; }
+ public Color BarColor { get; set; } = Color.White;
+
+ public event EventHandler ProgressChanged;
+
+ public override IEnumerable<Control> Children { get; } = Enumerable.Empty<Control>();
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ return new Size(5, 5);
+ }
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+
+ var boundingRectangle = ContentRectangle;
+ var clippingRectangle = new Rectangle(boundingRectangle.X, boundingRectangle.Y, (int)(boundingRectangle.Width * Progress), boundingRectangle.Height);
+
+ if (BarRegion != null)
+ renderer.DrawRegion(BarRegion, BoundingRectangle, BarColor, clippingRectangle);
+ else
+ renderer.FillRectangle(BoundingRectangle, BarColor, clippingRectangle);
+ }
+
+ //protected override void DrawBackground(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ //{
+ // base.DrawBackground(context, renderer, deltaSeconds);
+
+
+ //}
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/SelectorControl.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/SelectorControl.cs
new file mode 100644
index 0000000..49b39ba
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/SelectorControl.cs
@@ -0,0 +1,173 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGame.Extended.Input.InputListeners;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public abstract class SelectorControl : Control
+ {
+ protected SelectorControl()
+ {
+ }
+
+ private int _selectedIndex = -1;
+ public virtual int SelectedIndex
+ {
+ get { return _selectedIndex; }
+ set
+ {
+ if (_selectedIndex != value)
+ {
+ _selectedIndex = value;
+ SelectedIndexChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ public override IEnumerable<Control> Children => Items.OfType<Control>();
+
+ public virtual List<object> Items { get; } = new List<object>();
+ public virtual Color SelectedTextColor { get; set; } = Color.White;
+ public virtual Color SelectedItemColor { get; set; } = Color.CornflowerBlue;
+ public virtual Thickness ItemPadding { get; set; } = new Thickness(4, 2);
+ public virtual string NameProperty { get; set; }
+
+ public event EventHandler SelectedIndexChanged;
+
+ protected int FirstIndex;
+
+ public object SelectedItem
+ {
+ get { return SelectedIndex >= 0 && SelectedIndex <= Items.Count - 1 ? Items[SelectedIndex] : null; }
+ set { SelectedIndex = Items.IndexOf(value); }
+ }
+
+ public override bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args)
+ {
+ if (args.Key == Keys.Down) ScrollDown();
+ if (args.Key == Keys.Up) ScrollUp();
+
+ return base.OnKeyPressed(context, args);
+ }
+
+ public override void OnScrolled(int delta)
+ {
+ base.OnScrolled(delta);
+
+ if (delta < 0) ScrollDown();
+ if (delta > 0) ScrollUp();
+ }
+
+ private void ScrollDown()
+ {
+ if (SelectedIndex < Items.Count - 1)
+ SelectedIndex++;
+ }
+
+ private void ScrollUp()
+ {
+ if (SelectedIndex > 0)
+ SelectedIndex--;
+ }
+
+ public override bool OnPointerDown(IGuiContext context, PointerEventArgs args)
+ {
+ var contentRectangle = GetListAreaRectangle(context);
+
+ for (var i = FirstIndex; i < Items.Count; i++)
+ {
+ var itemRectangle = GetItemRectangle(context, i - FirstIndex, contentRectangle);
+
+ if (itemRectangle.Contains(args.Position))
+ {
+ SelectedIndex = i;
+ OnItemClicked(context, args);
+ break;
+ }
+ }
+
+ return base.OnPointerDown(context, args);
+ }
+
+ protected virtual void OnItemClicked(IGuiContext context, PointerEventArgs args) { }
+
+ protected TextInfo GetItemTextInfo(IGuiContext context, Rectangle itemRectangle, object item)
+ {
+ var textRectangle = new Rectangle(itemRectangle.X + ItemPadding.Left, itemRectangle.Y + ItemPadding.Top,
+ itemRectangle.Width - ItemPadding.Right, itemRectangle.Height - ItemPadding.Bottom);
+ var itemTextInfo = GetTextInfo(context, GetItemName(item), textRectangle, HorizontalTextAlignment, VerticalTextAlignment);
+ return itemTextInfo;
+ }
+
+ private string GetItemName(object item)
+ {
+ if (item != null)
+ {
+ if (NameProperty != null)
+ {
+ return item.GetType()
+ .GetRuntimeProperty(NameProperty)
+ .GetValue(item)
+ ?.ToString() ?? string.Empty;
+ }
+
+ return item.ToString();
+ }
+
+ return string.Empty;
+ }
+
+ protected Rectangle GetItemRectangle(IGuiContext context, int index, Rectangle contentRectangle)
+ {
+ var font = Font ?? context.DefaultFont;
+ var itemHeight = font.LineHeight + ItemPadding.Top + ItemPadding.Bottom;
+ return new Rectangle(contentRectangle.X, contentRectangle.Y + itemHeight * index, contentRectangle.Width, itemHeight);
+ }
+
+ protected void ScrollIntoView(IGuiContext context)
+ {
+ var contentRectangle = GetListAreaRectangle(context);
+ var selectedItemRectangle = GetItemRectangle(context, SelectedIndex - FirstIndex, contentRectangle);
+
+ if (selectedItemRectangle.Bottom > ClippingRectangle.Bottom)
+ FirstIndex++;
+
+ if (selectedItemRectangle.Top < ClippingRectangle.Top && FirstIndex > 0)
+ FirstIndex--;
+ }
+
+ protected Size GetItemSize(IGuiContext context, object item)
+ {
+ var text = GetItemName(item);
+ var font = Font ?? context.DefaultFont;
+ var textSize = (Size)font.MeasureString(text ?? string.Empty);
+ var itemWidth = textSize.Width + ItemPadding.Width;
+ var itemHeight = textSize.Height + ItemPadding.Height;
+ return new Size(itemWidth, itemHeight);
+ }
+
+ protected abstract Rectangle GetListAreaRectangle(IGuiContext context);
+
+ protected void DrawItemList(IGuiContext context, IGuiRenderer renderer)
+ {
+ var listRectangle = GetListAreaRectangle(context);
+
+ for (var i = FirstIndex; i < Items.Count; i++)
+ {
+ var item = Items[i];
+ var itemRectangle = GetItemRectangle(context, i - FirstIndex, listRectangle);
+ var itemTextInfo = GetItemTextInfo(context, itemRectangle, item);
+ var textColor = i == SelectedIndex ? SelectedTextColor : itemTextInfo.Color;
+
+ if (SelectedIndex == i)
+ renderer.FillRectangle(itemRectangle, SelectedItemColor, listRectangle);
+
+ renderer.DrawText(itemTextInfo.Font, itemTextInfo.Text, itemTextInfo.Position + TextOffset, textColor, itemTextInfo.ClippingRectangle);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/StackPanel.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/StackPanel.cs
new file mode 100644
index 0000000..5d8926d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/StackPanel.cs
@@ -0,0 +1,69 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class StackPanel : LayoutControl
+ {
+ public StackPanel()
+ {
+ }
+
+ public Orientation Orientation { get; set; } = Orientation.Vertical;
+ public int Spacing { get; set; }
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var width = 0;
+ var height = 0;
+
+ foreach (var control in Items)
+ {
+ var actualSize = control.CalculateActualSize(context);
+
+ switch (Orientation)
+ {
+ case Orientation.Horizontal:
+ width += actualSize.Width;
+ height = actualSize.Height > height ? actualSize.Height : height;
+ break;
+ case Orientation.Vertical:
+ width = actualSize.Width > width ? actualSize.Width : width;
+ height += actualSize.Height;
+ break;
+ default:
+ throw new InvalidOperationException($"Unexpected orientation {Orientation}");
+ }
+ }
+
+ width += Orientation == Orientation.Horizontal ? (Items.Count - 1) * Spacing : 0;
+ height += Orientation == Orientation.Vertical ? (Items.Count - 1) * Spacing : 0;
+
+ return new Size(width, height);
+ }
+
+ protected override void Layout(IGuiContext context, Rectangle rectangle)
+ {
+ foreach (var control in Items)
+ {
+ var actualSize = control.CalculateActualSize(context);
+
+ switch (Orientation)
+ {
+ case Orientation.Vertical:
+ PlaceControl(context, control, rectangle.X, rectangle.Y, rectangle.Width, actualSize.Height);
+ rectangle.Y += actualSize.Height + Spacing;
+ rectangle.Height -= actualSize.Height;
+ break;
+ case Orientation.Horizontal:
+ PlaceControl(context, control, rectangle.X, rectangle.Y, actualSize.Width, rectangle.Height);
+ rectangle.X += actualSize.Width + Spacing;
+ rectangle.Width -= actualSize.Width;
+ break;
+ default:
+ throw new InvalidOperationException($"Unexpected orientation {Orientation}");
+ }
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox.cs
new file mode 100644
index 0000000..75b6940
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox.cs
@@ -0,0 +1,331 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGame.Extended.Input.InputListeners;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public sealed class TextBox : Control
+ {
+ public TextBox(string text = null)
+ {
+ Text = text ?? string.Empty;
+ HorizontalTextAlignment = HorizontalAlignment.Left;
+ }
+
+ public TextBox()
+ : this(null)
+ {
+ }
+
+ public int SelectionStart { get; set; }
+ public char? PasswordCharacter { get; set; }
+
+ private string _text;
+
+ public string Text
+ {
+ get { return _text; }
+ set
+ {
+ if (_text != value)
+ {
+ _text = value;
+ OnTextChanged();
+ }
+ }
+ }
+
+ private void OnTextChanged()
+ {
+ if (!string.IsNullOrEmpty(Text) && SelectionStart > Text.Length)
+ SelectionStart = Text.Length;
+
+ TextChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ public event EventHandler TextChanged;
+
+ public override IEnumerable<Control> Children { get; } = Enumerable.Empty<Control>();
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var font = Font ?? context.DefaultFont;
+ var stringSize = (Size) font.MeasureString(Text ?? string.Empty);
+ return new Size(stringSize.Width,
+ stringSize.Height < font.LineHeight ? font.LineHeight : stringSize.Height);
+ }
+
+ //protected override Size2 CalculateDesiredSize(IGuiContext context, Size2 availableSize)
+ //{
+ // var font = Font ?? context.DefaultFont;
+ // return new Size2(Width + Padding.Left + Padding.Right, (Height <= 0.0f ? font.LineHeight + 2 : Height) + Padding.Top + Padding.Bottom);
+ //}
+
+ public override bool OnPointerDown(IGuiContext context, PointerEventArgs args)
+ {
+ SelectionStart = FindNearestGlyphIndex(context, args.Position);
+ _isCaretVisible = true;
+
+ //_selectionIndexes.Clear();
+ //_selectionIndexes.Push(SelectionStart);
+ //_startSelectionBox = Text.Length > 0;
+
+ return base.OnPointerDown(context, args);
+ }
+
+ //public override bool OnPointerMove(IGuiContext context, PointerEventArgs args)
+ //{
+ // if (_startSelectionBox)
+ // {
+ // var selection = FindNearestGlyphIndex(context, args.Position);
+ // if (selection != _selectionIndexes.Peek())
+ // {
+ // if (_selectionIndexes.Count == 1)
+ // {
+ // _selectionIndexes.Push(selection);
+ // }
+ // else if (_selectionIndexes.Last() < _selectionIndexes.Peek())
+ // {
+ // if (selection > _selectionIndexes.Peek()) _selectionIndexes.Pop();
+ // else _selectionIndexes.Push(selection);
+ // }
+ // else
+ // {
+ // if (selection < _selectionIndexes.Peek()) _selectionIndexes.Pop();
+ // else _selectionIndexes.Push(selection);
+ // }
+ // SelectionStart = selection;
+ // }
+ // }
+
+ // return base.OnPointerMove(context, args);
+ //}
+
+ //public override bool OnPointerLeave(IGuiContext context, PointerEventArgs args)
+ //{
+ // _startSelectionBox = false;
+
+ // return base.OnPointerLeave(context, args);
+ //}
+
+ //public override bool OnPointerUp(IGuiContext context, PointerEventArgs args)
+ //{
+ // _startSelectionBox = false;
+
+ // return base.OnPointerUp(context, args);
+ //}
+
+ private int FindNearestGlyphIndex(IGuiContext context, Point position)
+ {
+ var font = Font ?? context.DefaultFont;
+ var textInfo = GetTextInfo(context, Text, ContentRectangle, HorizontalTextAlignment, VerticalTextAlignment);
+ var i = 0;
+
+ foreach (var glyph in font.GetGlyphs(textInfo.Text, textInfo.Position))
+ {
+ var fontRegionWidth = glyph.FontRegion?.Width ?? 0;
+ var glyphMiddle = (int) (glyph.Position.X + fontRegionWidth * 0.5f);
+
+ if (position.X >= glyphMiddle)
+ {
+ i++;
+ continue;
+ }
+
+ return i;
+ }
+
+ return i;
+ }
+
+ public override bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args)
+ {
+ switch (args.Key)
+ {
+ case Keys.Tab:
+ case Keys.Enter:
+ return true;
+ case Keys.Back:
+ if (Text.Length > 0)
+ {
+ if (SelectionStart > 0) // && _selectionIndexes.Count <= 1)
+ {
+ SelectionStart--;
+ Text = Text.Remove(SelectionStart, 1);
+ }
+ //else
+ //{
+ // var start = MathHelper.Min(_selectionIndexes.Last(), _selectionIndexes.Peek());
+ // var end = MathHelper.Max(_selectionIndexes.Last(), _selectionIndexes.Peek());
+ // Text = Text.Remove(start, end - start);
+
+ // _selectionIndexes.Clear();
+ //}
+ }
+
+ break;
+ case Keys.Delete:
+ if (SelectionStart < Text.Length) // && _selectionIndexes.Count <= 1)
+ {
+ Text = Text.Remove(SelectionStart, 1);
+ }
+ //else if (_selectionIndexes.Count > 1)
+ //{
+ // var start = MathHelper.Min(_selectionIndexes.Last(), _selectionIndexes.Peek());
+ // var end = MathHelper.Max(_selectionIndexes.Last(), _selectionIndexes.Peek());
+ // Text = Text.Remove(start, end - start);
+ // SelectionStart = 0; // yeah, nah.
+
+ // _selectionIndexes.Clear();
+ //}
+ break;
+ case Keys.Left:
+ if (SelectionStart > 0)
+ {
+ //if (_selectionIndexes.Count > 1)
+ //{
+ // if (_selectionIndexes.Last() < SelectionStart) SelectionStart = _selectionIndexes.Last();
+ // _selectionIndexes.Clear();
+ //}
+ //else
+ {
+ SelectionStart--;
+ }
+ }
+
+ break;
+ case Keys.Right:
+ if (SelectionStart < Text.Length)
+ {
+ //if (_selectionIndexes.Count > 1)
+ //{
+ // if (_selectionIndexes.Last() > SelectionStart) SelectionStart = _selectionIndexes.Last();
+ // _selectionIndexes.Clear();
+ //}
+ //else
+ {
+ SelectionStart++;
+ }
+ }
+
+ break;
+ case Keys.Home:
+ SelectionStart = 0;
+ //_selectionIndexes.Clear();
+ break;
+ case Keys.End:
+ SelectionStart = Text.Length;
+ //_selectionIndexes.Clear();
+ break;
+ default:
+ if (args.Character != null)
+ {
+ //if (_selectionIndexes.Count > 1)
+ //{
+ // var start = MathHelper.Min(_selectionIndexes.Last(), _selectionIndexes.Peek());
+ // var end = MathHelper.Max(_selectionIndexes.Last(), _selectionIndexes.Peek());
+ // Text = Text.Remove(start, end - start);
+
+ // _selectionIndexes.Clear();
+ //}
+
+ Text = Text.Insert(SelectionStart, args.Character.ToString());
+ SelectionStart++;
+ }
+
+ break;
+ }
+
+ _isCaretVisible = true;
+ return base.OnKeyPressed(context, args);
+ }
+
+ private const float _caretBlinkRate = 0.53f;
+ private float _nextCaretBlink = _caretBlinkRate;
+ private bool _isCaretVisible = true;
+
+ //private bool _startSelectionBox = false;
+ //private Stack<int> _selectionIndexes = new Stack<int>();
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+
+ var text = PasswordCharacter.HasValue ? new string(PasswordCharacter.Value, Text.Length) : Text;
+ var textInfo = GetTextInfo(context, text, ContentRectangle, HorizontalTextAlignment, VerticalTextAlignment);
+
+ if (!string.IsNullOrWhiteSpace(textInfo.Text))
+ renderer.DrawText(textInfo.Font, textInfo.Text, textInfo.Position + TextOffset, textInfo.Color,
+ textInfo.ClippingRectangle);
+
+ if (IsFocused)
+ {
+ var caretRectangle = GetCaretRectangle(textInfo, SelectionStart);
+
+ if (_isCaretVisible)
+ renderer.DrawRectangle(caretRectangle, TextColor);
+
+ _nextCaretBlink -= deltaSeconds;
+
+ if (_nextCaretBlink <= 0)
+ {
+ _isCaretVisible = !_isCaretVisible;
+ _nextCaretBlink = _caretBlinkRate;
+ }
+
+ //if (_selectionIndexes.Count > 1)
+ //{
+ // var start = 0;
+ // var end = 0;
+ // var point = Point2.Zero;
+ // if (_selectionIndexes.Last() > _selectionIndexes.Peek())
+ // {
+ // start = _selectionIndexes.Peek();
+ // end = _selectionIndexes.Last();
+ // point = caretRectangle.Position;
+ // }
+ // else
+ // {
+ // start = _selectionIndexes.Last();
+ // end = _selectionIndexes.Peek();
+ // point = GetCaretRectangle(textInfo, start).Position;
+ // }
+ // var selectionRectangle = textInfo.Font.GetStringRectangle(textInfo.Text.Substring(start, end - start), point);
+
+ // renderer.FillRectangle((Rectangle)selectionRectangle, Color.Black * 0.25f);
+ //}
+ }
+ }
+
+
+ //protected override string CreateBoxText(string text, BitmapFont font, float width)
+ //{
+ // return text;
+ //}
+
+ private Rectangle GetCaretRectangle(TextInfo textInfo, int index)
+ {
+ var caretRectangle = textInfo.Font.GetStringRectangle(textInfo.Text.Substring(0, index), textInfo.Position);
+
+ // TODO: Finish the caret position stuff when it's outside the clipping rectangle
+ if (caretRectangle.Right > ClippingRectangle.Right)
+ textInfo.Position.X -= caretRectangle.Right - ClippingRectangle.Right;
+
+ caretRectangle.X = caretRectangle.Right < ClippingRectangle.Right
+ ? caretRectangle.Right
+ : ClippingRectangle.Right;
+ caretRectangle.Width = 1;
+
+ if (caretRectangle.Left < ClippingRectangle.Left)
+ {
+ textInfo.Position.X += ClippingRectangle.Left - caretRectangle.Left;
+ caretRectangle.X = ClippingRectangle.Left;
+ }
+
+ return (Rectangle) caretRectangle;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox2.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox2.cs
new file mode 100644
index 0000000..e20b621
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox2.cs
@@ -0,0 +1,328 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGame.Extended.Input.InputListeners;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class TextBox2 : Control
+ {
+ public TextBox2()
+ : this(null)
+ {
+ }
+
+ public TextBox2(string text)
+ {
+ Text = text ?? string.Empty;
+ HorizontalTextAlignment = HorizontalAlignment.Left;
+ VerticalTextAlignment = VerticalAlignment.Top;
+ }
+
+
+ private const float _caretBlinkRate = 0.53f;
+ private float _nextCaretBlink = _caretBlinkRate;
+ private bool _isCaretVisible = true;
+
+ private readonly List<StringBuilder> _lines = new List<StringBuilder>();
+ public string Text
+ {
+ get => string.Concat(_lines.SelectMany(s => $"{s}\n"));
+ set
+ {
+ _lines.Clear();
+
+ var line = new StringBuilder();
+
+ for (var i = 0; i < value.Length; i++)
+ {
+ var c = value[i];
+
+ if (c == '\n')
+ {
+ _lines.Add(line);
+ line = new StringBuilder();
+ }
+ else if(c != '\r')
+ {
+ line.Append(c);
+ }
+ }
+
+ _lines.Add(line);
+ }
+ }
+
+ public int CaretIndex => ColumnIndex * LineIndex + ColumnIndex;
+ public int LineIndex { get; set; }
+ public int ColumnIndex { get; set; }
+ public int TabStops { get; set; } = 4;
+
+ public override IEnumerable<Control> Children => Enumerable.Empty<Control>();
+
+ public string GetLineText(int lineIndex) => _lines[lineIndex].ToString();
+ public int GetLineLength(int lineIndex) => _lines[lineIndex].Length;
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var font = Font ?? context.DefaultFont;
+ var stringSize = (Size)font.MeasureString(Text ?? string.Empty);
+ return new Size(stringSize.Width, stringSize.Height < font.LineHeight ? font.LineHeight : stringSize.Height);
+ }
+
+ public override bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args)
+ {
+ switch (args.Key)
+ {
+ case Keys.Tab:
+ Tab();
+ break;
+ case Keys.Back:
+ Backspace();
+ break;
+ case Keys.Delete:
+ Delete();
+ break;
+ case Keys.Left:
+ Left();
+ break;
+ case Keys.Right:
+ Right();
+ break;
+ case Keys.Up:
+ Up();
+ break;
+ case Keys.Down:
+ Down();
+ break;
+ case Keys.Home:
+ Home();
+ break;
+ case Keys.End:
+ End();
+ break;
+ case Keys.Enter:
+ Type('\n');
+ return true;
+ default:
+ if (args.Character.HasValue)
+ Type(args.Character.Value);
+
+ break;
+ }
+
+ _isCaretVisible = true;
+ return base.OnKeyPressed(context, args);
+ }
+
+ public void Type(char c)
+ {
+ switch (c)
+ {
+ case '\n':
+ var lineText = GetLineText(LineIndex);
+ var left = lineText.Substring(0, ColumnIndex);
+ var right = lineText.Substring(ColumnIndex);
+ _lines.Insert(LineIndex + 1, new StringBuilder(right));
+ _lines[LineIndex] = new StringBuilder(left);
+ LineIndex++;
+ Home();
+ break;
+ case '\t':
+ Tab();
+ break;
+ default:
+ _lines[LineIndex].Insert(ColumnIndex, c);
+ ColumnIndex++;
+ break;
+ }
+ }
+
+ public void Backspace()
+ {
+ if (ColumnIndex == 0 && LineIndex > 0)
+ {
+ var topLineLength = GetLineLength(LineIndex - 1);
+
+ if (RemoveLineBreak(LineIndex - 1))
+ {
+ LineIndex--;
+ ColumnIndex = topLineLength;
+ }
+ }
+ else if (Left())
+ {
+ RemoveCharacter(LineIndex, ColumnIndex);
+ }
+ }
+
+ public void Delete()
+ {
+ var lineLength = GetLineLength(LineIndex);
+
+ if (ColumnIndex == lineLength)
+ RemoveLineBreak(LineIndex);
+ else
+ RemoveCharacter(LineIndex, ColumnIndex);
+ }
+
+ public void RemoveCharacter(int lineIndex, int columnIndex)
+ {
+ _lines[lineIndex].Remove(columnIndex, 1);
+ }
+
+ public bool RemoveLineBreak(int lineIndex)
+ {
+ if (lineIndex < _lines.Count - 1)
+ {
+ var topLine = _lines[lineIndex];
+ var bottomLine = _lines[lineIndex + 1];
+ _lines.RemoveAt(lineIndex + 1);
+ topLine.Append(bottomLine);
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool Home()
+ {
+ ColumnIndex = 0;
+ return true;
+ }
+
+ public bool End()
+ {
+ ColumnIndex = GetLineLength(LineIndex);
+ return true;
+ }
+
+ public bool Up()
+ {
+ if (LineIndex > 0)
+ {
+ LineIndex--;
+
+ if (ColumnIndex > GetLineLength(LineIndex))
+ ColumnIndex = GetLineLength(LineIndex);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool Down()
+ {
+ if (LineIndex < _lines.Count - 1)
+ {
+ LineIndex++;
+
+ if (ColumnIndex > GetLineLength(LineIndex))
+ ColumnIndex = GetLineLength(LineIndex);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool Left()
+ {
+ if (ColumnIndex == 0)
+ {
+ if (LineIndex == 0)
+ return false;
+
+ LineIndex--;
+ ColumnIndex = GetLineLength(LineIndex);
+ }
+ else
+ {
+ ColumnIndex--;
+ }
+
+ return true;
+ }
+
+ public bool Right()
+ {
+ if (ColumnIndex == _lines[LineIndex].Length)
+ {
+ if (LineIndex == _lines.Count - 1)
+ return false;
+
+ LineIndex++;
+ ColumnIndex = 0;
+ }
+ else
+ {
+ ColumnIndex++;
+ }
+
+ return true;
+ }
+
+ public bool Tab()
+ {
+ var spaces = TabStops - ColumnIndex % TabStops;
+
+ for (var s = 0; s < spaces; s++)
+ Type(' ');
+
+ return spaces > 0;
+ }
+
+ public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ {
+ base.Draw(context, renderer, deltaSeconds);
+
+ var text = Text;
+ var textInfo = GetTextInfo(context, text, ContentRectangle, HorizontalTextAlignment, VerticalTextAlignment);
+
+ if (!string.IsNullOrWhiteSpace(textInfo.Text))
+ renderer.DrawText(textInfo.Font, textInfo.Text, textInfo.Position + TextOffset, textInfo.Color, textInfo.ClippingRectangle);
+
+ if (IsFocused)
+ {
+ var caretRectangle = GetCaretRectangle(textInfo);
+
+ if (_isCaretVisible)
+ renderer.DrawRectangle(caretRectangle, TextColor);
+
+ _nextCaretBlink -= deltaSeconds;
+
+ if (_nextCaretBlink <= 0)
+ {
+ _isCaretVisible = !_isCaretVisible;
+ _nextCaretBlink = _caretBlinkRate;
+ }
+ }
+ }
+
+ private Rectangle GetCaretRectangle(TextInfo textInfo)
+ {
+ var font = textInfo.Font;
+ var text = GetLineText(LineIndex);
+ var offset = new Vector2(0, font.LineHeight * LineIndex);
+ var caretRectangle = font.GetStringRectangle(text.Substring(0, ColumnIndex), textInfo.Position + offset);
+
+ // TODO: Finish the caret position stuff when it's outside the clipping rectangle
+ if (caretRectangle.Right > ClippingRectangle.Right)
+ textInfo.Position.X -= caretRectangle.Right - ClippingRectangle.Right;
+
+ caretRectangle.X = caretRectangle.Right < ClippingRectangle.Right ? caretRectangle.Right : ClippingRectangle.Right;
+ caretRectangle.Width = 1;
+
+ if (caretRectangle.Left < ClippingRectangle.Left)
+ {
+ textInfo.Position.X += ClippingRectangle.Left - caretRectangle.Left;
+ caretRectangle.X = ClippingRectangle.Left;
+ }
+
+ return (Rectangle)caretRectangle;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ToggleButton.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ToggleButton.cs
new file mode 100644
index 0000000..5858894
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ToggleButton.cs
@@ -0,0 +1,98 @@
+using System;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class ToggleButton : Button
+ {
+ public ToggleButton()
+ {
+ }
+
+ public event EventHandler CheckedStateChanged;
+
+ private bool _isChecked;
+ public bool IsChecked
+ {
+ get { return _isChecked; }
+ set
+ {
+ if (_isChecked != value)
+ {
+ _isChecked = value;
+ CheckedStyle?.ApplyIf(this, _isChecked);
+ CheckedStateChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ private ControlStyle _checkedStyle;
+ public ControlStyle CheckedStyle
+ {
+ get { return _checkedStyle; }
+ set
+ {
+ if (_checkedStyle != value)
+ {
+ _checkedStyle = value;
+ CheckedStyle?.ApplyIf(this, _isChecked);
+ }
+ }
+ }
+
+ private ControlStyle _checkedHoverStyle;
+ public ControlStyle CheckedHoverStyle
+ {
+ get { return _checkedHoverStyle; }
+ set
+ {
+ if (_checkedHoverStyle != value)
+ {
+ _checkedHoverStyle = value;
+ CheckedHoverStyle?.ApplyIf(this, _isChecked && IsHovered);
+ }
+ }
+ }
+
+ public override bool OnPointerUp(IGuiContext context, PointerEventArgs args)
+ {
+ base.OnPointerUp(context, args);
+
+ if (BoundingRectangle.Contains(args.Position))
+ {
+ HoverStyle?.Revert(this);
+ CheckedHoverStyle?.Revert(this);
+
+ IsChecked = !IsChecked;
+
+ if (IsChecked)
+ CheckedHoverStyle?.Apply(this);
+ else
+ HoverStyle?.Apply(this);
+ }
+
+ return true;
+ }
+
+ public override bool OnPointerEnter(IGuiContext context, PointerEventArgs args)
+ {
+ if (IsChecked)
+ {
+ CheckedHoverStyle?.Apply(this);
+ return true;
+ }
+
+ return base.OnPointerEnter(context, args);
+ }
+
+ public override bool OnPointerLeave(IGuiContext context, PointerEventArgs args)
+ {
+ if (IsChecked)
+ {
+ CheckedHoverStyle?.Revert(this);
+ return true;
+ }
+
+ return base.OnPointerLeave(context, args);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/UniformGrid.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/UniformGrid.cs
new file mode 100644
index 0000000..829969d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/UniformGrid.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Linq;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Gui.Controls
+{
+ public class UniformGrid : LayoutControl
+ {
+ public UniformGrid()
+ {
+ }
+
+ public int Columns { get; set; }
+ public int Rows { get; set; }
+
+ public override Size GetContentSize(IGuiContext context)
+ {
+ var columns = Columns == 0 ? (int)Math.Ceiling(Math.Sqrt(Items.Count)) : Columns;
+ var rows = Rows == 0 ? (int)Math.Ceiling((float)Items.Count / columns) : Rows;
+ var sizes = Items
+ .Select(control => control.CalculateActualSize(context))
+ .ToArray();
+ var minCellWidth = sizes.Max(s => s.Width);
+ var minCellHeight = sizes.Max(s => s.Height);
+ return new Size(minCellWidth * columns, minCellHeight * rows);
+ }
+
+ protected override void Layout(IGuiContext context, Rectangle rectangle)
+ {
+ var gridInfo = CalculateGridInfo(context, rectangle.Size);
+ var columnIndex = 0;
+ var rowIndex = 0;
+ var cellWidth = HorizontalAlignment == HorizontalAlignment.Stretch ? gridInfo.MaxCellWidth : gridInfo.MinCellWidth;
+ var cellHeight = VerticalAlignment == VerticalAlignment.Stretch ? gridInfo.MaxCellHeight : gridInfo.MinCellHeight;
+
+ foreach (var control in Items)
+ {
+ var x = columnIndex * cellWidth + rectangle.X;
+ var y = rowIndex * cellHeight + rectangle.Y;
+
+ PlaceControl(context, control, x, y, cellWidth, cellHeight);
+ columnIndex++;
+
+ if (columnIndex > gridInfo.Columns - 1)
+ {
+ columnIndex = 0;
+ rowIndex++;
+ }
+ }
+ }
+
+ private struct GridInfo
+ {
+ public float MinCellWidth;
+ public float MinCellHeight;
+ public float MaxCellWidth;
+ public float MaxCellHeight;
+ public float Columns;
+ public float Rows;
+ public Size2 MinCellSize => new Size2(MinCellWidth * Columns, MinCellHeight * Rows);
+ }
+
+ private GridInfo CalculateGridInfo(IGuiContext context, Size2 availableSize)
+ {
+ var columns = Columns == 0 ? (int)Math.Ceiling(Math.Sqrt(Items.Count)) : Columns;
+ var rows = Rows == 0 ? (int)Math.Ceiling((float)Items.Count / columns) : Rows;
+ var maxCellWidth = availableSize.Width / columns;
+ var maxCellHeight = availableSize.Height / rows;
+ var sizes = Items
+ .Select(control => control.CalculateActualSize(context)) // LayoutHelper.GetSizeWithMargins(control, context, new Size2(maxCellWidth, maxCellHeight)))
+ .ToArray();
+ var maxControlWidth = sizes.Length == 0 ? 0 : sizes.Max(s => s.Width);
+ var maxControlHeight = sizes.Length == 0 ? 0 : sizes.Max(s => s.Height);
+
+ return new GridInfo
+ {
+ Columns = columns,
+ Rows = rows,
+ MinCellWidth = Math.Min(maxControlWidth, maxCellWidth),
+ MinCellHeight = Math.Min(maxControlHeight, maxCellHeight),
+ MaxCellWidth = maxCellWidth,
+ MaxCellHeight = maxCellHeight
+ };
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Cursor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Cursor.cs
new file mode 100644
index 0000000..638baec
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Cursor.cs
@@ -0,0 +1,11 @@
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Gui
+{
+ public class Cursor
+ {
+ public TextureRegion2D TextureRegion { get; set; }
+ public Color Color { get; set; } = Color.White;
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Element.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Element.cs
new file mode 100644
index 0000000..0c364a9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Element.cs
@@ -0,0 +1,125 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Reflection;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Gui
+{
+ public class Binding
+ {
+ public Binding(object viewModel, string viewModelProperty, string viewProperty)
+ {
+ ViewModel = viewModel;
+ ViewModelProperty = viewModelProperty;
+ ViewProperty = viewProperty;
+ }
+
+ public object ViewModel { get; }
+ public string ViewModelProperty { get; }
+ public string ViewProperty { get; }
+ }
+
+ public abstract class Element
+ {
+ public string Name { get; set; }
+ public Point Position { get; set; }
+ public Point Origin { get; set; }
+ public Color BackgroundColor { get; set; }
+ public Color BorderColor { get; set; } = Color.White;
+ public int BorderThickness { get; set; } = 0;
+
+ private TextureRegion2D _backgroundRegion;
+ public TextureRegion2D BackgroundRegion
+ {
+ get => _backgroundRegion;
+ set
+ {
+ _backgroundRegion = value;
+
+ if (_backgroundRegion != null && BackgroundColor == Color.Transparent)
+ BackgroundColor = Color.White;
+ }
+ }
+
+ public List<Binding> Bindings { get; } = new List<Binding>();
+
+ protected void OnPropertyChanged(string propertyName)
+ {
+ foreach (var binding in Bindings)
+ {
+ if (binding.ViewProperty == propertyName)
+ {
+ var value = GetType()
+ .GetTypeInfo()
+ .GetDeclaredProperty(binding.ViewProperty)
+ .GetValue(this);
+
+ binding.ViewModel
+ .GetType()
+ .GetTypeInfo()
+ .GetDeclaredProperty(binding.ViewModelProperty)
+ .SetValue(binding.ViewModel, value);
+ }
+ }
+ }
+
+ private Size _size;
+ public Size Size
+ {
+ get => _size;
+ set
+ {
+ _size = value;
+ OnSizeChanged();
+ }
+ }
+
+ protected virtual void OnSizeChanged() { }
+
+ public int MinWidth { get; set; }
+ public int MinHeight { get; set; }
+ public int MaxWidth { get; set; } = int.MaxValue;
+ public int MaxHeight { get; set; } = int.MaxValue;
+
+ public int Width
+ {
+ get => Size.Width;
+ set => Size = new Size(value, Size.Height);
+ }
+
+ public int Height
+ {
+ get => Size.Height;
+ set => Size = new Size(Size.Width, value);
+ }
+
+ public Size ActualSize { get; internal set; }
+
+ public abstract void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds);
+ }
+
+ public abstract class Element<TParent> : Element, IRectangular
+ where TParent : IRectangular
+ {
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [JsonIgnore]
+ public TParent Parent { get; internal set; }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [JsonIgnore]
+ public Rectangle BoundingRectangle
+ {
+ get
+ {
+ var offset = Point.Zero;
+
+ if (Parent != null)
+ offset = Parent.BoundingRectangle.Location;
+
+ return new Rectangle(offset + Position - ActualSize * Origin, ActualSize);
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ElementCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ElementCollection.cs
new file mode 100644
index 0000000..009161d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ElementCollection.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using MonoGame.Extended.Gui.Controls;
+
+namespace MonoGame.Extended.Gui
+{
+ public abstract class ElementCollection<TChild, TParent> : IList<TChild>
+ where TParent : class, IRectangular
+ where TChild : Element<TParent>
+ {
+ private readonly TParent _parent;
+ private readonly List<TChild> _list = new List<TChild>();
+
+ public Action<TChild> ItemAdded { get; set; }
+ public Action<TChild> ItemRemoved { get; set; }
+
+ protected ElementCollection(TParent parent)
+ {
+ _parent = parent;
+ }
+
+ public IEnumerator<TChild> GetEnumerator()
+ {
+ return _list.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable)_list).GetEnumerator();
+ }
+
+ public void Add(TChild item)
+ {
+ item.Parent = _parent;
+ _list.Add(item);
+ ItemAdded?.Invoke(item);
+ }
+
+ public void Clear()
+ {
+ foreach (var child in _list)
+ {
+ child.Parent = null;
+ ItemRemoved?.Invoke(child);
+ }
+
+ _list.Clear();
+ }
+
+ public bool Contains(TChild item)
+ {
+ return _list.Contains(item);
+ }
+
+ public void CopyTo(TChild[] array, int arrayIndex)
+ {
+ _list.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(TChild item)
+ {
+ item.Parent = null;
+ ItemRemoved?.Invoke(item);
+ return _list.Remove(item);
+ }
+
+ public int Count => _list.Count;
+
+ public bool IsReadOnly => ((ICollection<Control>)_list).IsReadOnly;
+
+ public int IndexOf(TChild item)
+ {
+ return _list.IndexOf(item);
+ }
+
+ public void Insert(int index, TChild item)
+ {
+ item.Parent = _parent;
+ _list.Insert(index, item);
+ ItemAdded?.Invoke(item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ var child = _list[index];
+ child.Parent = null;
+ _list.RemoveAt(index);
+ ItemRemoved?.Invoke(child);
+ }
+
+ public TChild this[int index]
+ {
+ get { return _list[index]; }
+ set { _list[index] = value; }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSpriteBatchRenderer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSpriteBatchRenderer.cs
new file mode 100644
index 0000000..6d1e8ce
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSpriteBatchRenderer.cs
@@ -0,0 +1,81 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.BitmapFonts;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Gui
+{
+ public interface IGuiRenderer
+ {
+ void Begin();
+ void DrawRegion(TextureRegion2D textureRegion, Rectangle rectangle, Color color, Rectangle? clippingRectangle = null);
+ void DrawRegion(TextureRegion2D textureRegion, Vector2 position, Color color, Rectangle? clippingRectangle = null);
+ void DrawText(BitmapFont font, string text, Vector2 position, Color color, Rectangle? clippingRectangle = null);
+ void DrawRectangle(Rectangle rectangle, Color color, float thickness = 1f, Rectangle? clippingRectangle = null);
+ void FillRectangle(Rectangle rectangle, Color color, Rectangle? clippingRectangle = null);
+ void End();
+ }
+
+ public class GuiSpriteBatchRenderer : IGuiRenderer
+ {
+ private readonly Func<Matrix> _getTransformMatrix;
+ private readonly SpriteBatch _spriteBatch;
+
+ public GuiSpriteBatchRenderer(GraphicsDevice graphicsDevice, Func<Matrix> getTransformMatrix)
+ {
+ _getTransformMatrix = getTransformMatrix;
+ _spriteBatch = new SpriteBatch(graphicsDevice);
+ }
+
+ public SpriteSortMode SortMode { get; set; }
+ public BlendState BlendState { get; set; } = BlendState.AlphaBlend;
+ public SamplerState SamplerState { get; set; } = SamplerState.PointClamp;
+ public DepthStencilState DepthStencilState { get; set; } = DepthStencilState.Default;
+ public RasterizerState RasterizerState { get; set; } = RasterizerState.CullNone;
+ public Effect Effect { get; set; }
+
+ public void Begin()
+ {
+ _spriteBatch.Begin(SortMode, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect, _getTransformMatrix());
+ }
+
+ public void End()
+ {
+ _spriteBatch.End();
+ }
+
+ public void DrawRegion(TextureRegion2D textureRegion, Rectangle rectangle, Color color, Rectangle? clippingRectangle = null)
+ {
+ if (textureRegion != null)
+ _spriteBatch.Draw(textureRegion, rectangle, color, clippingRectangle);
+ }
+
+ public void DrawRegion(TextureRegion2D textureRegion, Vector2 position, Color color, Rectangle? clippingRectangle = null)
+ {
+ if (textureRegion != null)
+ _spriteBatch.Draw(textureRegion, position, color, clippingRectangle);
+ }
+
+ public void DrawText(BitmapFont font, string text, Vector2 position, Color color, Rectangle? clippingRectangle = null)
+ {
+ _spriteBatch.DrawString(font, text, position, color, clippingRectangle);
+ }
+
+ public void DrawRectangle(Rectangle rectangle, Color color, float thickness = 1f, Rectangle? clippingRectangle = null)
+ {
+ if (clippingRectangle.HasValue)
+ rectangle = rectangle.Clip(clippingRectangle.Value);
+
+ _spriteBatch.DrawRectangle(rectangle, color, thickness);
+ }
+
+ public void FillRectangle(Rectangle rectangle, Color color, Rectangle? clippingRectangle = null)
+ {
+ if (clippingRectangle.HasValue)
+ rectangle = rectangle.Clip(clippingRectangle.Value);
+
+ _spriteBatch.FillRectangle(rectangle, color);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSystem.cs
new file mode 100644
index 0000000..8b522fb
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSystem.cs
@@ -0,0 +1,265 @@
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.BitmapFonts;
+using MonoGame.Extended.Gui.Controls;
+using MonoGame.Extended.Input.InputListeners;
+using MonoGame.Extended.ViewportAdapters;
+using System;
+using System.Linq;
+
+namespace MonoGame.Extended.Gui
+{
+ public interface IGuiContext
+ {
+ BitmapFont DefaultFont { get; }
+ Vector2 CursorPosition { get; }
+ Control FocusedControl { get; }
+
+ void SetFocus(Control focusedControl);
+ }
+
+ public class GuiSystem : IGuiContext, IRectangular
+ {
+ private readonly ViewportAdapter _viewportAdapter;
+ private readonly IGuiRenderer _renderer;
+ private readonly MouseListener _mouseListener;
+ private readonly TouchListener _touchListener;
+ private readonly KeyboardListener _keyboardListener;
+
+ private Control _preFocusedControl;
+
+ public GuiSystem(ViewportAdapter viewportAdapter, IGuiRenderer renderer)
+ {
+ _viewportAdapter = viewportAdapter;
+ _renderer = renderer;
+
+ _mouseListener = new MouseListener(viewportAdapter);
+ _mouseListener.MouseDown += (s, e) => OnPointerDown(PointerEventArgs.FromMouseArgs(e));
+ _mouseListener.MouseMoved += (s, e) => OnPointerMoved(PointerEventArgs.FromMouseArgs(e));
+ _mouseListener.MouseUp += (s, e) => OnPointerUp(PointerEventArgs.FromMouseArgs(e));
+ _mouseListener.MouseWheelMoved += (s, e) => FocusedControl?.OnScrolled(e.ScrollWheelDelta);
+
+ _touchListener = new TouchListener(viewportAdapter);
+ _touchListener.TouchStarted += (s, e) => OnPointerDown(PointerEventArgs.FromTouchArgs(e));
+ _touchListener.TouchMoved += (s, e) => OnPointerMoved(PointerEventArgs.FromTouchArgs(e));
+ _touchListener.TouchEnded += (s, e) => OnPointerUp(PointerEventArgs.FromTouchArgs(e));
+
+ _keyboardListener = new KeyboardListener();
+ _keyboardListener.KeyTyped += (sender, args) => PropagateDown(FocusedControl, x => x.OnKeyTyped(this, args));
+ _keyboardListener.KeyPressed += (sender, args) => PropagateDown(FocusedControl, x => x.OnKeyPressed(this, args));
+ }
+
+ public Control FocusedControl { get; private set; }
+ public Control HoveredControl { get; private set; }
+
+ private Screen _activeScreen;
+ public Screen ActiveScreen
+ {
+ get => _activeScreen;
+ set
+ {
+ if (_activeScreen != value)
+ {
+ _activeScreen = value;
+
+ if(_activeScreen != null)
+ InitializeScreen(_activeScreen);
+ }
+ }
+ }
+
+ public Rectangle BoundingRectangle => _viewportAdapter.BoundingRectangle;
+
+ public Vector2 CursorPosition { get; set; }
+
+ public BitmapFont DefaultFont => Skin.Default?.DefaultFont;
+
+ private void InitializeScreen(Screen screen)
+ {
+ screen.Layout(this, BoundingRectangle);
+ }
+
+ public void ClientSizeChanged()
+ {
+ //ActiveScreen?.Content?.InvalidateMeasure();
+ ActiveScreen?.Layout(this, BoundingRectangle);
+ }
+
+ public void Update(GameTime gameTime)
+ {
+ if(ActiveScreen == null)
+ return;
+
+ _touchListener.Update(gameTime);
+ _mouseListener.Update(gameTime);
+ _keyboardListener.Update(gameTime);
+
+ var deltaSeconds = gameTime.GetElapsedSeconds();
+
+ if (ActiveScreen != null && ActiveScreen.IsVisible)
+ UpdateControl(ActiveScreen.Content, deltaSeconds);
+
+ //if (ActiveScreen.IsLayoutRequired)
+ // ActiveScreen.Layout(this, BoundingRectangle);
+
+ ActiveScreen.Update(gameTime);
+ }
+
+ public void Draw(GameTime gameTime)
+ {
+ var deltaSeconds = gameTime.GetElapsedSeconds();
+
+ _renderer.Begin();
+
+ if (ActiveScreen != null && ActiveScreen.IsVisible)
+ {
+ DrawControl(ActiveScreen.Content, deltaSeconds);
+ //DrawWindows(ActiveScreen.Windows, deltaSeconds);
+ }
+
+ var cursor = Skin.Default?.Cursor;
+
+ if (cursor != null)
+ _renderer.DrawRegion(cursor.TextureRegion, CursorPosition, cursor.Color);
+
+ _renderer.End();
+ }
+
+ //private void DrawWindows(WindowCollection windows, float deltaSeconds)
+ //{
+ // foreach (var window in windows)
+ // {
+ // window.Draw(this, _renderer, deltaSeconds);
+ // DrawChildren(window.Controls, deltaSeconds);
+ // }
+ //}
+
+ public void UpdateControl(Control control, float deltaSeconds)
+ {
+ if (control.IsVisible)
+ {
+ control.Update(this, deltaSeconds);
+
+ foreach (var childControl in control.Children)
+ UpdateControl(childControl, deltaSeconds);
+ }
+ }
+
+ private void DrawControl(Control control, float deltaSeconds)
+ {
+ if (control.IsVisible)
+ {
+ control.Draw(this, _renderer, deltaSeconds);
+
+ foreach (var childControl in control.Children)
+ DrawControl(childControl, deltaSeconds);
+ }
+ }
+
+ private void OnPointerDown(PointerEventArgs args)
+ {
+ if (ActiveScreen == null || !ActiveScreen.IsVisible)
+ return;
+
+ _preFocusedControl = FindControlAtPoint(args.Position);
+ PropagateDown(HoveredControl, x => x.OnPointerDown(this, args));
+ }
+
+ private void OnPointerUp(PointerEventArgs args)
+ {
+ if (ActiveScreen == null || !ActiveScreen.IsVisible)
+ return;
+
+ var postFocusedControl = FindControlAtPoint(args.Position);
+
+ if (_preFocusedControl == postFocusedControl)
+ {
+ SetFocus(postFocusedControl);
+ }
+
+ _preFocusedControl = null;
+ PropagateDown(HoveredControl, x => x.OnPointerUp(this, args));
+ }
+
+ private void OnPointerMoved(PointerEventArgs args)
+ {
+ CursorPosition = args.Position.ToVector2();
+
+ if (ActiveScreen == null || !ActiveScreen.IsVisible)
+ return;
+
+ var hoveredControl = FindControlAtPoint(args.Position);
+
+ if (HoveredControl != hoveredControl)
+ {
+ if (HoveredControl != null && (hoveredControl == null || !hoveredControl.HasParent(HoveredControl)))
+ PropagateDown(HoveredControl, x => x.OnPointerLeave(this, args));
+
+ HoveredControl = hoveredControl;
+ PropagateDown(HoveredControl, x => x.OnPointerEnter(this, args));
+ }
+ else
+ {
+ PropagateDown(HoveredControl, x => x.OnPointerMove(this, args));
+ }
+ }
+
+ public void SetFocus(Control focusedControl)
+ {
+ if (FocusedControl != focusedControl)
+ {
+ if (FocusedControl != null)
+ {
+ FocusedControl.IsFocused = false;
+ PropagateDown(FocusedControl, x => x.OnUnfocus(this));
+ }
+
+ FocusedControl = focusedControl;
+
+ if (FocusedControl != null)
+ {
+ FocusedControl.IsFocused = true;
+ PropagateDown(FocusedControl, x => x.OnFocus(this));
+ }
+ }
+ }
+
+ /// <summary>
+ /// Method is meant to loop down the parents control to find a suitable event control. If the predicate returns false
+ /// it will continue down the control tree.
+ /// </summary>
+ /// <param name="control">The control we want to check against</param>
+ /// <param name="predicate">A function to check if the propagation should resume, if returns false it will continue down the tree.</param>
+ private static void PropagateDown(Control control, Func<Control, bool> predicate)
+ {
+ while(control != null && predicate(control))
+ {
+ control = control.Parent;
+ }
+ }
+
+ private Control FindControlAtPoint(Point point)
+ {
+ if (ActiveScreen == null || !ActiveScreen.IsVisible)
+ return null;
+
+ return FindControlAtPoint(ActiveScreen.Content, point);
+ }
+
+ private Control FindControlAtPoint(Control control, Point point)
+ {
+ foreach (var controlChild in control.Children.Reverse())
+ {
+ var c = FindControlAtPoint(controlChild, point);
+
+ if (c != null)
+ return c;
+ }
+
+
+ if (control.IsVisible && control.Contains(this, point))
+ return control;
+
+ return null;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/LayoutHelper.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/LayoutHelper.cs
new file mode 100644
index 0000000..1268bff
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/LayoutHelper.cs
@@ -0,0 +1,65 @@
+using System;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Gui.Controls;
+
+namespace MonoGame.Extended.Gui
+{
+ public enum HorizontalAlignment { Left, Right, Centre, Stretch }
+ public enum VerticalAlignment { Top, Bottom, Centre, Stretch }
+
+ public static class LayoutHelper
+ {
+ public static void PlaceControl(IGuiContext context, Control control, float x, float y, float width, float height)
+ {
+ var rectangle = new Rectangle((int)x, (int)y, (int)width, (int)height);
+ var desiredSize = control.CalculateActualSize(context);
+ var alignedRectangle = AlignRectangle(control.HorizontalAlignment, control.VerticalAlignment, desiredSize, rectangle);
+
+ control.Position = new Point(control.Margin.Left + alignedRectangle.X, control.Margin.Top + alignedRectangle.Y);
+ control.ActualSize = (Size)alignedRectangle.Size - control.Margin.Size;
+ control.InvalidateMeasure();
+ }
+
+ public static Rectangle AlignRectangle(HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment, Size size, Rectangle targetRectangle)
+ {
+ var x = GetHorizontalPosition(horizontalAlignment, size, targetRectangle);
+ var y = GetVerticalPosition(verticalAlignment, size, targetRectangle);
+ var width = horizontalAlignment == HorizontalAlignment.Stretch ? targetRectangle.Width : size.Width;
+ var height = verticalAlignment == VerticalAlignment.Stretch ? targetRectangle.Height : size.Height;
+
+ return new Rectangle(x, y, width, height);
+ }
+
+ public static int GetHorizontalPosition(HorizontalAlignment horizontalAlignment, Size size, Rectangle targetRectangle)
+ {
+ switch (horizontalAlignment)
+ {
+ case HorizontalAlignment.Stretch:
+ case HorizontalAlignment.Left:
+ return targetRectangle.X;
+ case HorizontalAlignment.Right:
+ return targetRectangle.Right - size.Width;
+ case HorizontalAlignment.Centre:
+ return targetRectangle.X + targetRectangle.Width / 2 - size.Width / 2;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(horizontalAlignment), horizontalAlignment, $"{horizontalAlignment} is not supported");
+ }
+ }
+
+ public static int GetVerticalPosition(VerticalAlignment verticalAlignment, Size size, Rectangle targetRectangle)
+ {
+ switch (verticalAlignment)
+ {
+ case VerticalAlignment.Stretch:
+ case VerticalAlignment.Top:
+ return targetRectangle.Y;
+ case VerticalAlignment.Bottom:
+ return targetRectangle.Bottom - size.Height;
+ case VerticalAlignment.Centre:
+ return targetRectangle.Y + targetRectangle.Height / 2 - size.Height / 2;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(verticalAlignment), verticalAlignment, $"{verticalAlignment} is not supported");
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Markup/MarkupParser.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Markup/MarkupParser.cs
new file mode 100644
index 0000000..c2e8776
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Markup/MarkupParser.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Xml;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Gui.Controls;
+
+namespace MonoGame.Extended.Gui.Markup
+{
+ public class MarkupParser
+ {
+ public MarkupParser()
+ {
+ }
+
+ private static readonly Dictionary<string, Type> _controlTypes =
+ typeof(Control).Assembly
+ .ExportedTypes
+ .Where(t => t.IsSubclassOf(typeof(Control)))
+ .ToDictionary(t => t.Name, StringComparer.OrdinalIgnoreCase);
+
+ private static readonly Dictionary<Type, Func<string, object>> _converters =
+ new Dictionary<Type, Func<string, object>>
+ {
+ {typeof(object), s => s},
+ {typeof(string), s => s},
+ {typeof(bool), s => bool.Parse(s)},
+ {typeof(int), s => int.Parse(s)},
+ {typeof(Color), s => s.StartsWith("#") ? ColorHelper.FromHex(s) : ColorHelper.FromName(s)}
+ };
+
+ private static object ConvertValue(Type propertyType, string input, object dataContext)
+ {
+ var value = ParseBinding(input, dataContext);
+
+ if (_converters.TryGetValue(propertyType, out var converter))
+ return converter(value); //property.SetValue(control, converter(value));
+
+ if (propertyType.IsEnum)
+ return
+ Enum.Parse(propertyType, value,
+ true); // property.SetValue(control, Enum.Parse(propertyType, value, true));
+
+ throw new InvalidOperationException($"Converter not found for {propertyType}");
+ }
+
+ private static object ParseChildNode(XmlNode node, Control parent, object dataContext)
+ {
+ if (node is XmlText)
+ return node.InnerText.Trim();
+
+ if (_controlTypes.TryGetValue(node.Name, out var type))
+ {
+ var typeInfo = type.GetTypeInfo();
+ var control = (Control) Activator.CreateInstance(type);
+
+ // ReSharper disable once AssignNullToNotNullAttribute
+ foreach (var attribute in node.Attributes.Cast<XmlAttribute>())
+ {
+ var property = typeInfo.GetProperty(attribute.Name);
+
+ if (property != null)
+ {
+ var value = ConvertValue(property.PropertyType, attribute.Value, dataContext);
+ property.SetValue(control, value);
+ }
+ else
+ {
+ var parts = attribute.Name.Split('.');
+ var parentType = parts[0];
+ var propertyName = parts[1];
+ var propertyType = parent.GetAttachedPropertyType(propertyName);
+ var propertyValue = ConvertValue(propertyType, attribute.Value, dataContext);
+
+ if (!string.Equals(parent.GetType().Name, parentType, StringComparison.OrdinalIgnoreCase))
+ throw new InvalidOperationException(
+ $"Attached properties are only supported on the immediate parent type {parentType}");
+
+ control.SetAttachedProperty(propertyName, propertyValue);
+ }
+ }
+
+
+ if (node.HasChildNodes)
+ {
+ switch (control)
+ {
+ case ContentControl contentControl:
+ if (node.ChildNodes.Count > 1)
+ throw new InvalidOperationException("A content control can only have one child");
+
+ contentControl.Content = ParseChildNode(node.ChildNodes[0], control, dataContext);
+ break;
+ case LayoutControl layoutControl:
+ foreach (var childControl in ParseChildNodes(node.ChildNodes, control, dataContext))
+ layoutControl.Items.Add(childControl as Control);
+ break;
+ }
+ }
+
+ return control;
+ }
+
+ throw new InvalidOperationException($"Unknown control type {node.Name}");
+ }
+
+ private static string ParseBinding(string expression, object dataContext)
+ {
+ if (dataContext != null && expression.StartsWith("{{") && expression.EndsWith("}}"))
+ {
+ var binding = expression.Substring(2, expression.Length - 4);
+ var bindingValue = dataContext
+ .GetType()
+ .GetProperty(binding)
+ ?.GetValue(dataContext);
+
+ return $"{bindingValue}";
+ }
+
+ return expression;
+ }
+
+ private static IEnumerable<object> ParseChildNodes(XmlNodeList nodes, Control parent, object dataContext)
+ {
+ foreach (var node in nodes.Cast<XmlNode>())
+ {
+ if (node.Name == "xml")
+ {
+ // TODO: Validate header
+ }
+ else
+ {
+ yield return ParseChildNode(node, parent, dataContext);
+ }
+ }
+ }
+
+ public Control Parse(string filePath, object dataContext)
+ {
+ var d = new XmlDocument();
+ d.Load(filePath);
+ return ParseChildNodes(d.ChildNodes, null, dataContext)
+ .LastOrDefault() as Control;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/MonoGame.Extended.Gui.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/MonoGame.Extended.Gui.csproj
new file mode 100644
index 0000000..d319cb4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/MonoGame.Extended.Gui.csproj
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>A GUI system for MonoGame written from the ground up to make MonoGame more awesome.</Description>
+ <PackageTags>monogame gui</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended.Input\MonoGame.Extended.Input.csproj" />
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Orientation.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Orientation.cs
new file mode 100644
index 0000000..a12599f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Orientation.cs
@@ -0,0 +1,4 @@
+namespace MonoGame.Extended.Gui
+{
+ public enum Orientation { Horizontal, Vertical }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/PointerEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/PointerEventArgs.cs
new file mode 100644
index 0000000..18cffac
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/PointerEventArgs.cs
@@ -0,0 +1,42 @@
+using System;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Input;
+using MonoGame.Extended.Input.InputListeners;
+
+namespace MonoGame.Extended.Gui
+{
+ public class PointerEventArgs : EventArgs
+ {
+ private PointerEventArgs()
+ {
+ }
+
+ public Point Position { get; private set; }
+ public MouseButton Button { get; private set; }
+ public int ScrollWheelDelta { get; private set; }
+ public int ScrollWheelValue { get; private set; }
+ public TimeSpan Time { get; private set; }
+
+ public static PointerEventArgs FromMouseArgs(MouseEventArgs args)
+ {
+ return new PointerEventArgs
+ {
+ Position = args.Position,
+ Button = args.Button,
+ ScrollWheelDelta = args.ScrollWheelDelta,
+ ScrollWheelValue = args.ScrollWheelValue,
+ Time = args.Time
+ };
+ }
+
+ public static PointerEventArgs FromTouchArgs(TouchEventArgs args)
+ {
+ return new PointerEventArgs
+ {
+ Position = args.Position,
+ Button = MouseButton.Left,
+ Time = args.Time
+ };
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Screen.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Screen.cs
new file mode 100644
index 0000000..821a1e2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Screen.cs
@@ -0,0 +1,140 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using MonoGame.Extended.Gui.Controls;
+using MonoGame.Extended.Gui.Serialization;
+
+namespace MonoGame.Extended.Gui
+{
+ public class Screen //: Element<GuiSystem>, IDisposable
+ {
+ public Screen()
+ {
+ //Windows = new WindowCollection(this) { ItemAdded = w => _isLayoutRequired = true };
+ }
+
+ public virtual void Dispose()
+ {
+ }
+
+ private Control _content;
+ [JsonPropertyOrder(1)]
+ public Control Content
+ {
+ get { return _content; }
+ set
+ {
+ if (_content != value)
+ {
+ _content = value;
+ _isLayoutRequired = true;
+ }
+ }
+ }
+
+ //[JsonIgnore]
+ //public WindowCollection Windows { get; }
+
+ public float Width { get; private set; }
+ public float Height { get; private set; }
+ public Size2 Size => new Size2(Width, Height);
+ public bool IsVisible { get; set; } = true;
+
+ private bool _isLayoutRequired;
+ [JsonIgnore]
+ public bool IsLayoutRequired => _isLayoutRequired || Content.IsLayoutRequired;
+
+ public virtual void Update(GameTime gameTime)
+ {
+
+ }
+
+ public void Show()
+ {
+ IsVisible = true;
+ }
+
+ public void Hide()
+ {
+ IsVisible = false;
+ }
+
+ public T FindControl<T>(string name)
+ where T : Control
+ {
+ return FindControl<T>(Content, name);
+ }
+
+ private static T FindControl<T>(Control rootControl, string name)
+ where T : Control
+ {
+ if (rootControl.Name == name)
+ return rootControl as T;
+
+ foreach (var childControl in rootControl.Children)
+ {
+ var control = FindControl<T>(childControl, name);
+
+ if (control != null)
+ return control;
+ }
+
+ return null;
+ }
+
+ public void Layout(IGuiContext context, Rectangle rectangle)
+ {
+ Width = rectangle.Width;
+ Height = rectangle.Height;
+
+ LayoutHelper.PlaceControl(context, Content, rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
+
+ //foreach (var window in Windows)
+ // LayoutHelper.PlaceWindow(context, window, rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
+
+ _isLayoutRequired = false;
+ Content.IsLayoutRequired = false;
+ }
+
+ //public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ //{
+ // renderer.DrawRectangle(BoundingRectangle, Color.Green);
+ //}
+
+ public static Screen FromStream(ContentManager contentManager, Stream stream, params Type[] customControlTypes)
+ {
+ return FromStream<Screen>(contentManager, stream, customControlTypes);
+ }
+
+ public static TScreen FromStream<TScreen>(ContentManager contentManager, Stream stream, params Type[] customControlTypes)
+ where TScreen : Screen
+ {
+ var skinService = new SkinService();
+ var options = GuiJsonSerializerOptionsProvider.GetOptions(contentManager, customControlTypes);
+ options.Converters.Add(new SkinJsonConverter(contentManager, skinService, customControlTypes));
+ options.Converters.Add(new ControlJsonConverter(skinService, customControlTypes));
+ return JsonSerializer.Deserialize<TScreen>(stream, options);
+ }
+
+ public static Screen FromFile(ContentManager contentManager, string path, params Type[] customControlTypes)
+ {
+ using (var stream = TitleContainer.OpenStream(path))
+ {
+ return FromStream<Screen>(contentManager, stream, customControlTypes);
+ }
+ }
+
+ public static TScreen FromFile<TScreen>(ContentManager contentManager, string path, params Type[] customControlTypes)
+ where TScreen : Screen
+ {
+ using (var stream = TitleContainer.OpenStream(path))
+ {
+ return FromStream<TScreen>(contentManager, stream, customControlTypes);
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ScreenCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ScreenCollection.cs
new file mode 100644
index 0000000..0572728
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ScreenCollection.cs
@@ -0,0 +1,10 @@
+namespace MonoGame.Extended.Gui
+{
+ //public class ScreenCollection : ElementCollection<Screen, GuiSystem>
+ //{
+ // public ScreenCollection(GuiSystem parent)
+ // : base(parent)
+ // {
+ // }
+ //}
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlJsonConverter.cs
new file mode 100644
index 0000000..a9c71f8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlJsonConverter.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using MonoGame.Extended.Gui.Controls;
+
+namespace MonoGame.Extended.Gui.Serialization
+{
+ public class ControlJsonConverter : JsonConverter<Control>
+ {
+ private readonly IGuiSkinService _guiSkinService;
+ private readonly ControlStyleJsonConverter _styleConverter;
+ private const string _styleProperty = "Style";
+
+ public ControlJsonConverter(IGuiSkinService guiSkinService, params Type[] customControlTypes)
+ {
+ _guiSkinService = guiSkinService;
+ _styleConverter = new ControlStyleJsonConverter(customControlTypes);
+ }
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Control);
+
+ /// <inheritdoc />
+ public override Control Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var style = _styleConverter.Read(ref reader, typeToConvert, options);
+ var template = GetControlTemplate(style);
+ var skin = _guiSkinService.Skin;
+ var control = skin.Create(style.TargetType, template);
+
+ var itemsControl = control as ItemsControl;
+ if (itemsControl != null)
+ {
+ object childControls;
+
+ if (style.TryGetValue(nameof(ItemsControl.Items), out childControls))
+ {
+ var controlCollection = childControls as ControlCollection;
+
+ if (controlCollection != null)
+ {
+ foreach (var child in controlCollection)
+ itemsControl.Items.Add(child);
+ }
+ }
+ }
+
+ style.Apply(control);
+ return control;
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, Control value, JsonSerializerOptions options) { }
+
+
+
+ private static string GetControlTemplate(ControlStyle style)
+ {
+ object template;
+
+ if (style.TryGetValue(_styleProperty, out template))
+ return template as string;
+
+ return null;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlStyleJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlStyleJsonConverter.cs
new file mode 100644
index 0000000..c31549b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlStyleJsonConverter.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using MonoGame.Extended.Collections;
+using MonoGame.Extended.Gui.Controls;
+
+namespace MonoGame.Extended.Gui.Serialization
+{
+ public class ControlStyleJsonConverter : JsonConverter<ControlStyle>
+ {
+ private readonly Dictionary<string, Type> _controlTypes;
+ private const string _typeProperty = "Type";
+ private const string _nameProperty = "Name";
+
+ public ControlStyleJsonConverter(params Type[] customControlTypes)
+ {
+ _controlTypes = typeof(Control)
+ .GetTypeInfo()
+ .Assembly
+ .ExportedTypes
+ .Concat(customControlTypes)
+ .Where(t => t.GetTypeInfo().IsSubclassOf(typeof(Control)))
+ .ToDictionary(t => t.Name);
+ }
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(ControlStyle);
+
+ /// <inheritdoc />
+ public override ControlStyle Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var dictionary = JsonSerializer.Deserialize<Dictionary<string, object>>(ref reader, options);
+ var name = dictionary.GetValueOrDefault(_nameProperty) as string;
+ var typeName = dictionary.GetValueOrDefault(_typeProperty) as string;
+
+ if (!_controlTypes.TryGetValue(typeName, out Type controlType))
+ throw new FormatException("invalid control type: " + typeName);
+
+ var targetType = typeName != null ? controlType : typeof(Control);
+ var properties = targetType
+ .GetRuntimeProperties()
+ .ToDictionary(p => p.Name);
+ var style = new ControlStyle(name, targetType);
+
+ foreach (var keyValuePair in dictionary.Where(i => i.Key != _typeProperty))
+ {
+ var propertyName = keyValuePair.Key;
+ var rawValue = keyValuePair.Value;
+
+ PropertyInfo propertyInfo;
+ var value = properties.TryGetValue(propertyName, out propertyInfo)
+ ? DeserializeValueAs(rawValue, propertyInfo.PropertyType)
+ : DeserializeValueAs(rawValue, typeof(object));
+
+ style.Add(propertyName, value);
+ }
+
+ return style;
+ }
+
+ private static object DeserializeValueAs(object value, Type type)
+ {
+ var json = JsonSerializer.Serialize(value, type);
+ return JsonSerializer.Deserialize(json, type);
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, ControlStyle value, JsonSerializerOptions options)
+ {
+ var style = (ControlStyle)value;
+ var dictionary = new Dictionary<string, object> { [_typeProperty] = style.TargetType.Name };
+
+ foreach (var keyValuePair in style)
+ dictionary.Add(keyValuePair.Key, keyValuePair.Value);
+
+ JsonSerializer.Serialize(writer, dictionary);
+ }
+
+
+
+
+
+
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiJsonSerializerOptionsProvider.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiJsonSerializerOptionsProvider.cs
new file mode 100644
index 0000000..5ebc880
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiJsonSerializerOptionsProvider.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework.Content;
+using MonoGame.Extended.BitmapFonts;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Gui.Serialization;
+
+public static class GuiJsonSerializerOptionsProvider
+{
+ public static JsonSerializerOptions GetOptions(ContentManager contentManager, params Type[] customControlTypes)
+ {
+ var options = new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ var textureRegionService = new GuiTextureRegionService();
+
+ options.Converters.Add(new Vector2JsonConverter());
+ options.Converters.Add(new SizeJsonConverter());
+ options.Converters.Add(new Size2JsonConverter());
+ options.Converters.Add(new ColorJsonConverter());
+ options.Converters.Add(new ThicknessJsonConverter());
+ options.Converters.Add(new ContentManagerJsonConverter<BitmapFont>(contentManager, font => font.Name));
+ options.Converters.Add(new ControlStyleJsonConverter(customControlTypes));
+ options.Converters.Add(new GuiTextureAtlasJsonConverter(contentManager, textureRegionService));
+ options.Converters.Add(new GuiNinePatchRegion2DJsonConverter(textureRegionService));
+ options.Converters.Add(new TextureRegion2DJsonConverter(textureRegionService));
+ options.Converters.Add(new VerticalAlignmentConverter());
+ options.Converters.Add(new HorizontalAlignmentConverter());
+
+ return options;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiNinePatchRegion2DJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiNinePatchRegion2DJsonConverter.cs
new file mode 100644
index 0000000..9be58e5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiNinePatchRegion2DJsonConverter.cs
@@ -0,0 +1,15 @@
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Gui.Serialization
+{
+ public class GuiNinePatchRegion2DJsonConverter : NinePatchRegion2DJsonConverter
+ {
+ private readonly IGuiTextureRegionService _textureRegionService;
+
+ public GuiNinePatchRegion2DJsonConverter(IGuiTextureRegionService textureRegionService)
+ : base(textureRegionService)
+ {
+ _textureRegionService = textureRegionService;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureAtlasJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureAtlasJsonConverter.cs
new file mode 100644
index 0000000..a0617cf
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureAtlasJsonConverter.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Text.Json;
+using Microsoft.Xna.Framework.Content;
+using MonoGame.Extended.Serialization;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Gui.Serialization
+{
+ public class GuiTextureAtlasJsonConverter : ContentManagerJsonConverter<TextureAtlas>
+ {
+ private readonly IGuiTextureRegionService _textureRegionService;
+
+ public GuiTextureAtlasJsonConverter(ContentManager contentManager, IGuiTextureRegionService textureRegionService)
+ : base(contentManager, atlas => atlas.Name)
+ {
+ _textureRegionService = textureRegionService;
+ }
+
+ /// <inheritdoc />
+ public override TextureAtlas Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var textureAtlas = base.Read(ref reader, typeToConvert, options);
+ if (textureAtlas is not null)
+ {
+ _textureRegionService.TextureAtlases.Add(textureAtlas);
+ }
+
+ return textureAtlas;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureRegionService.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureRegionService.cs
new file mode 100644
index 0000000..cf9ab9e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureRegionService.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+using MonoGame.Extended.Serialization;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Gui.Serialization
+{
+ public interface IGuiTextureRegionService : ITextureRegionService
+ {
+ IList<TextureAtlas> TextureAtlases { get; }
+ IList<NinePatchRegion2D> NinePatches { get; }
+ }
+
+ public class GuiTextureRegionService : TextureRegionService, IGuiTextureRegionService
+ {
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/HorizontalAlignmentConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/HorizontalAlignmentConverter.cs
new file mode 100644
index 0000000..a696528
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/HorizontalAlignmentConverter.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Gui.Serialization;
+
+public class HorizontalAlignmentConverter : JsonConverter<HorizontalAlignment>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(HorizontalAlignment);
+
+ /// <inheritdoc />
+ public override HorizontalAlignment Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var value = reader.GetString();
+
+ if (value.Equals("Center", StringComparison.OrdinalIgnoreCase) || value.Equals("Centre", StringComparison.OrdinalIgnoreCase))
+ {
+ return HorizontalAlignment.Centre;
+ }
+
+ if (Enum.TryParse<HorizontalAlignment>(value, true, out var alignment))
+ {
+ return alignment;
+ }
+
+ throw new InvalidOperationException($"Invalid value for '{nameof(HorizontalAlignment)}'");
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, HorizontalAlignment value, JsonSerializerOptions options) { }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/SkinJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/SkinJsonConverter.cs
new file mode 100644
index 0000000..016ea6d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/SkinJsonConverter.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+
+namespace MonoGame.Extended.Gui.Serialization;
+
+public interface IGuiSkinService
+{
+ Skin Skin { get; set; }
+}
+
+public class SkinService : IGuiSkinService
+{
+ public Skin Skin { get; set; }
+}
+
+public class SkinJsonConverter : JsonConverter<Skin>
+{
+ private readonly ContentManager _contentManager;
+ private readonly IGuiSkinService _skinService;
+ private readonly Type[] _customControlTypes;
+
+ public SkinJsonConverter(ContentManager contentManager, IGuiSkinService skinService, params Type[] customControlTypes)
+ {
+ _contentManager = contentManager;
+ _skinService = skinService;
+ _customControlTypes = customControlTypes;
+ }
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Skin);
+
+ /// <inheritdoc />
+ public override Skin Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ var assetName = reader.GetString();
+
+ // TODO: Load this using the ContentManager instead.
+ using (var stream = TitleContainer.OpenStream(assetName))
+ {
+ var skin = Skin.FromStream(_contentManager, stream, _customControlTypes);
+ _skinService.Skin = skin;
+ return skin;
+ }
+
+ }
+
+ throw new InvalidOperationException($"{nameof(SkinJsonConverter)} can only convert from a string");
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, Skin value, JsonSerializerOptions options) { }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/VerticalAlignmentConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/VerticalAlignmentConverter.cs
new file mode 100644
index 0000000..bc55cda
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/VerticalAlignmentConverter.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Gui.Serialization;
+
+public class VerticalAlignmentConverter : JsonConverter<VerticalAlignment>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(VerticalAlignment);
+
+ /// <inheritdoc />
+ public override VerticalAlignment Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var value = reader.GetString();
+
+ if (value.Equals("Center", StringComparison.OrdinalIgnoreCase) || value.Equals("Centre", StringComparison.OrdinalIgnoreCase))
+ {
+ return VerticalAlignment.Centre;
+ }
+
+ if (Enum.TryParse<VerticalAlignment>(value, true, out var alignment))
+ {
+ return alignment;
+ }
+
+ throw new InvalidOperationException($"Invalid value for '{nameof(VerticalAlignment)}'");
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, VerticalAlignment value, JsonSerializerOptions options) { }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Skin.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Skin.cs
new file mode 100644
index 0000000..77476be
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Skin.cs
@@ -0,0 +1,221 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using MonoGame.Extended.BitmapFonts;
+using System.Linq;
+using System.Reflection;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using MonoGame.Extended.Collections;
+using MonoGame.Extended.Gui.Controls;
+using MonoGame.Extended.Gui.Serialization;
+using MonoGame.Extended.TextureAtlases;
+using System.Text.Json.Serialization;
+using System.Text.Json;
+
+namespace MonoGame.Extended.Gui
+{
+ public class Skin
+ {
+ public Skin()
+ {
+ TextureAtlases = new List<TextureAtlas>();
+ Fonts = new List<BitmapFont>();
+ NinePatches = new List<NinePatchRegion2D>();
+ Styles = new KeyedCollection<string, ControlStyle>(s => s.Name ?? s.TargetType.Name);
+ }
+
+ [JsonPropertyOrder(0)]
+ public string Name { get; set; }
+
+ [JsonPropertyOrder(1)]
+ public IList<TextureAtlas> TextureAtlases { get; set; }
+
+ [JsonPropertyOrder(2)]
+ public IList<BitmapFont> Fonts { get; set; }
+
+ [JsonPropertyOrder(3)]
+ public IList<NinePatchRegion2D> NinePatches { get; set; }
+
+ [JsonPropertyOrder(4)]
+ public BitmapFont DefaultFont => Fonts.FirstOrDefault();
+
+ [JsonPropertyOrder(5)]
+ public Cursor Cursor { get; set; }
+
+ [JsonPropertyOrder(6)]
+ public KeyedCollection<string, ControlStyle> Styles { get; private set; }
+
+ public ControlStyle GetStyle(string name)
+ {
+ if (Styles.TryGetValue(name, out var controlStyle))
+ return controlStyle;
+
+ return null;
+ }
+
+ public ControlStyle GetStyle(Type controlType)
+ {
+ return GetStyle(controlType.FullName);
+ }
+
+ public void Apply(Control control)
+ {
+ // TODO: This allocates memory on each apply because it needs to apply styles in reverse
+ var types = new List<Type>();
+ var controlType = control.GetType();
+
+ while (controlType != null)
+ {
+ types.Add(controlType);
+ controlType = controlType.GetTypeInfo().BaseType;
+ }
+
+ for (var i = types.Count - 1; i >= 0; i--)
+ {
+ var style = GetStyle(types[i]);
+ style?.Apply(control);
+ }
+ }
+
+ public static Skin FromFile(ContentManager contentManager, string path, params Type[] customControlTypes)
+ {
+ using (var stream = TitleContainer.OpenStream(path))
+ {
+ return FromStream(contentManager, stream, customControlTypes);
+ }
+ }
+
+ public static Skin FromStream(ContentManager contentManager, Stream stream, params Type[] customControlTypes)
+ {
+ var options = GuiJsonSerializerOptionsProvider.GetOptions(contentManager, customControlTypes);
+ return JsonSerializer.Deserialize<Skin>(stream, options);
+ }
+
+
+ public T Create<T>(string template, Action<T> onCreate)
+ where T : Control, new()
+ {
+ var control = new T();
+ GetStyle(template).Apply(control);
+ onCreate(control);
+ return control;
+ }
+
+ public Control Create(Type type, string template)
+ {
+ var control = (Control)Activator.CreateInstance(type);
+
+ if (template != null)
+ {
+ var style = GetStyle(template);
+ if (style != null)
+ style.Apply(control);
+ else
+ throw new FormatException($"invalid style {template} for control {type.Name}");
+ }
+
+ return control;
+ }
+
+ public static Skin Default { get; set; }
+
+ public static Skin CreateDefault(BitmapFont font)
+ {
+ Default = new Skin
+ {
+ Fonts = { font },
+ Styles =
+ {
+ new ControlStyle(typeof(Control)) {
+ {nameof(Control.BackgroundColor), new Color(51, 51, 55)},
+ {nameof(Control.BorderColor), new Color(67, 67, 70)},
+ {nameof(Control.BorderThickness), 1},
+ {nameof(Control.TextColor), new Color(241, 241, 241)},
+ {nameof(Control.Padding), new Thickness(5)},
+ {nameof(Control.DisabledStyle), new ControlStyle(typeof(Control)) {
+ { nameof(Control.TextColor), new Color(78,78,80) }
+ }
+ }
+ },
+ new ControlStyle(typeof(LayoutControl)) {
+ {nameof(Control.BackgroundColor), Color.Transparent},
+ {nameof(Control.BorderColor), Color.Transparent },
+ {nameof(Control.BorderThickness), 0},
+ {nameof(Control.Padding), new Thickness(0)},
+ {nameof(Control.Margin), new Thickness(0)},
+ },
+ new ControlStyle(typeof(ComboBox)) {
+ {nameof(ComboBox.DropDownColor), new Color(71, 71, 75)},
+ {nameof(ComboBox.SelectedItemColor), new Color(0, 122, 204)},
+ {nameof(ComboBox.HorizontalTextAlignment), HorizontalAlignment.Left }
+ },
+ new ControlStyle(typeof(CheckBox))
+ {
+ {nameof(CheckBox.HorizontalTextAlignment), HorizontalAlignment.Left },
+ {nameof(CheckBox.BorderThickness), 0},
+ {nameof(CheckBox.BackgroundColor), Color.Transparent},
+ },
+ new ControlStyle(typeof(ListBox))
+ {
+ {nameof(ListBox.SelectedItemColor), new Color(0, 122, 204)},
+ {nameof(ListBox.HorizontalTextAlignment), HorizontalAlignment.Left }
+ },
+ new ControlStyle(typeof(Label)) {
+ {nameof(Label.BackgroundColor), Color.Transparent},
+ {nameof(Label.TextColor), Color.White},
+ {nameof(Label.BorderColor), Color.Transparent},
+ {nameof(Label.BorderThickness), 0},
+ {nameof(Label.HorizontalTextAlignment), HorizontalAlignment.Left},
+ {nameof(Label.VerticalTextAlignment), VerticalAlignment.Bottom},
+ {nameof(Control.Margin), new Thickness(5,0)},
+ {nameof(Control.Padding), new Thickness(0)},
+ },
+ new ControlStyle(typeof(TextBox)) {
+ {nameof(Control.BackgroundColor), Color.DarkGray},
+ {nameof(Control.TextColor), Color.Black},
+ {nameof(Control.BorderColor), new Color(67, 67, 70)},
+ {nameof(Control.BorderThickness), 2},
+ },
+ new ControlStyle(typeof(TextBox2)) {
+ {nameof(Control.BackgroundColor), Color.DarkGray},
+ {nameof(Control.TextColor), Color.Black},
+ {nameof(Control.BorderColor), new Color(67, 67, 70)},
+ {nameof(Control.BorderThickness), 2},
+ },
+ new ControlStyle(typeof(Button)) {
+ {
+ nameof(Button.HoverStyle), new ControlStyle {
+ {nameof(Button.BackgroundColor), new Color(62, 62, 64)},
+ {nameof(Button.BorderColor), Color.WhiteSmoke }
+ }
+ },
+ {
+ nameof(Button.PressedStyle), new ControlStyle {
+ {nameof(Button.BackgroundColor), new Color(0, 122, 204)}
+ }
+ }
+ },
+ new ControlStyle(typeof(ToggleButton)) {
+ {
+ nameof(ToggleButton.CheckedStyle), new ControlStyle {
+ {nameof(Button.BackgroundColor), new Color(0, 122, 204)}
+ }
+ },
+ {
+ nameof(ToggleButton.CheckedHoverStyle), new ControlStyle {
+ {nameof(Button.BorderColor), Color.WhiteSmoke}
+ }
+ }
+ },
+ new ControlStyle(typeof(ProgressBar)) {
+ {nameof(ProgressBar.BarColor), new Color(0, 122, 204) },
+ {nameof(ProgressBar.Height), 32 },
+ {nameof(ProgressBar.Padding), new Thickness(5, 4)},
+ }
+ }
+ };
+ return Default;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Window.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Window.cs
new file mode 100644
index 0000000..12f4dcf
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Window.cs
@@ -0,0 +1,42 @@
+using System;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Gui.Controls;
+
+namespace MonoGame.Extended.Gui
+{
+ //public class Window : Element<Screen>
+ //{
+ // public Window(Screen parent)
+ // {
+ // Parent = parent;
+ // }
+
+ // public ControlCollection Controls { get; } = new ControlCollection();
+
+ // public void Show()
+ // {
+ // Parent.Windows.Add(this);
+ // }
+
+ // public void Hide()
+ // {
+ // Parent.Windows.Remove(this);
+ // }
+
+ // public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds)
+ // {
+ // renderer.FillRectangle(BoundingRectangle, Color.Magenta);
+ // }
+
+ // public Size2 GetDesiredSize(IGuiContext context, Size2 availableSize)
+ // {
+ // return new Size2(Width, Height);
+ // }
+
+ // public void Layout(IGuiContext context, RectangleF rectangle)
+ // {
+ // foreach (var control in Controls)
+ // LayoutHelper.PlaceControl(context, control, rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
+ // }
+ //}
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/WindowCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/WindowCollection.cs
new file mode 100644
index 0000000..d22f274
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/WindowCollection.cs
@@ -0,0 +1,10 @@
+namespace MonoGame.Extended.Gui
+{
+ //public class WindowCollection : ElementCollection<Window, Screen>
+ //{
+ // public WindowCollection(Screen parent)
+ // : base(parent)
+ // {
+ // }
+ //}
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/ExtendedPlayerIndex.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/ExtendedPlayerIndex.cs
new file mode 100644
index 0000000..a153f02
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/ExtendedPlayerIndex.cs
@@ -0,0 +1,32 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Input
+{
+ /// <summary>Player index enumeration with slots for 8 players</summary>
+ public enum ExtendedPlayerIndex
+ {
+ /// <summary>First player</summary>
+ One = PlayerIndex.One,
+
+ /// <summary>Second player</summary>
+ Two = PlayerIndex.Two,
+
+ /// <summary>Third player</summary>
+ Three = PlayerIndex.Three,
+
+ /// <summary>Fourth player</summary>
+ Four = PlayerIndex.Four,
+
+ /// <summary>Fifth player</summary>
+ Five,
+
+ /// <summary>Sixth player</summary>
+ Six,
+
+ /// <summary>Seventh player</summary>
+ Seven,
+
+ /// <summary>Eigth player</summary>
+ Eight
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadEventArgs.cs
new file mode 100644
index 0000000..9b113c6
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadEventArgs.cs
@@ -0,0 +1,62 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ /// <summary>
+ /// This class contains all information resulting from events fired by
+ /// <see cref="GamePadListener" />.
+ /// </summary>
+ public class GamePadEventArgs : EventArgs
+ {
+ public GamePadEventArgs(GamePadState previousState, GamePadState currentState,
+ TimeSpan elapsedTime, PlayerIndex playerIndex, Buttons? button = null,
+ float triggerState = 0, Vector2? thumbStickState = null)
+ {
+ PlayerIndex = playerIndex;
+ PreviousState = previousState;
+ CurrentState = currentState;
+ ElapsedTime = elapsedTime;
+ if (button != null)
+ Button = button.Value;
+ TriggerState = triggerState;
+ ThumbStickState = thumbStickState ?? Vector2.Zero;
+ }
+
+ /// <summary>
+ /// The index of the controller.
+ /// </summary>
+ public PlayerIndex PlayerIndex { get; private set; }
+
+ /// <summary>
+ /// The state of the controller in the previous update.
+ /// </summary>
+ public GamePadState PreviousState { get; private set; }
+
+ /// <summary>
+ /// The state of the controller in this update.
+ /// </summary>
+ public GamePadState CurrentState { get; private set; }
+
+ /// <summary>
+ /// The button that triggered this event, if appliable.
+ /// </summary>
+ public Buttons Button { get; private set; }
+
+ /// <summary>
+ /// The time elapsed since last event.
+ /// </summary>
+ public TimeSpan ElapsedTime { get; private set; }
+
+ /// <summary>
+ /// If a TriggerMoved event, displays the responsible trigger's position.
+ /// </summary>
+ public float TriggerState { get; private set; }
+
+ /// <summary>
+ /// If a ThumbStickMoved event, displays the responsible stick's position.
+ /// </summary>
+ public Vector2 ThumbStickState { get; private set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListener.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListener.cs
new file mode 100644
index 0000000..b7ea79b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListener.cs
@@ -0,0 +1,529 @@
+using System;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ /// <summary>
+ /// This is a listener that exposes several events for easier handling of gamepads.
+ /// </summary>
+ public class GamePadListener : InputListener
+ {
+ private static readonly bool[] _gamePadConnections = new bool[4];
+
+ // These buttons are not to be evaluated normally, but with the debounce filter
+ // in their respective methods.
+ private readonly Buttons[] _excludedButtons =
+ {
+ Buttons.LeftTrigger, Buttons.RightTrigger,
+ Buttons.LeftThumbstickDown, Buttons.LeftThumbstickUp, Buttons.LeftThumbstickRight,
+ Buttons.LeftThumbstickLeft,
+ Buttons.RightThumbstickLeft, Buttons.RightThumbstickRight, Buttons.RightThumbstickUp,
+ Buttons.RightThumbstickDown
+ };
+
+ private GamePadState _currentState;
+ //private int _lastPacketNumber;
+ // Implementation doesn't work, see explanation in CheckAllButtons().
+ private GameTime _gameTime;
+ private Buttons _lastButton;
+ private Buttons _lastLeftStickDirection;
+ private Buttons _lastRightStickDirection;
+ private GamePadState _lastThumbStickState;
+
+ private GamePadState _lastTriggerState;
+
+ private float _leftCurVibrationStrength;
+ private bool _leftStickDown;
+ private bool _leftTriggerDown;
+ private bool _leftVibrating;
+ private GameTime _previousGameTime;
+ private GamePadState _previousState;
+ private int _repeatedButtonTimer;
+ private float _rightCurVibrationStrength;
+ private bool _rightStickDown;
+ private bool _rightTriggerDown;
+ private bool _rightVibrating;
+ private TimeSpan _vibrationDurationLeft;
+ private TimeSpan _vibrationDurationRight;
+ private TimeSpan _vibrationStart;
+
+ private float _vibrationStrengthLeft;
+ private float _vibrationStrengthRight;
+
+ public GamePadListener()
+ : this(new GamePadListenerSettings())
+ {
+ }
+
+ public GamePadListener(GamePadListenerSettings settings)
+ {
+ PlayerIndex = settings.PlayerIndex;
+ VibrationEnabled = settings.VibrationEnabled;
+ VibrationStrengthLeft = settings.VibrationStrengthLeft;
+ VibrationStrengthRight = settings.VibrationStrengthRight;
+ ThumbStickDeltaTreshold = settings.ThumbStickDeltaTreshold;
+ ThumbstickDownTreshold = settings.ThumbstickDownTreshold;
+ TriggerDeltaTreshold = settings.TriggerDeltaTreshold;
+ TriggerDownTreshold = settings.TriggerDownTreshold;
+ RepeatInitialDelay = settings.RepeatInitialDelay;
+ RepeatDelay = settings.RepeatDelay;
+
+ _previousGameTime = new GameTime();
+ _previousState = GamePadState.Default;
+ }
+
+ /// <summary>
+ /// If set to true, the static event <see cref="ControllerConnectionChanged" />
+ /// will fire when any controller changes in connectivity status.
+ /// <para>
+ /// This functionality requires that you have one actively updating
+ /// <see cref="InputListenerManager" />.
+ /// </para>
+ /// </summary>
+ public static bool CheckControllerConnections { get; set; }
+
+ /// <summary>
+ /// The index of the controller.
+ /// </summary>
+ public PlayerIndex PlayerIndex { get; }
+
+ /// <summary>
+ /// When a button is held down, the interval in which
+ /// ButtonRepeated fires. Value in milliseconds.
+ /// </summary>
+ public int RepeatDelay { get; }
+
+ /// <summary>
+ /// The amount of time a button has to be held down
+ /// in order to fire ButtonRepeated the first time.
+ /// Value in milliseconds.
+ /// </summary>
+ public int RepeatInitialDelay { get; }
+
+ /// <summary>
+ /// Whether vibration is enabled for this controller.
+ /// </summary>
+ public bool VibrationEnabled { get; set; }
+
+ /// <summary>
+ /// General setting for the strength of the left motor.
+ /// This motor has a slow, deep, powerful rumble.
+ /// <para>
+ /// This setting will modify all future vibrations
+ /// through this listener.
+ /// </para>
+ /// </summary>
+ public float VibrationStrengthLeft
+ {
+ get { return _vibrationStrengthLeft; }
+ // Clamp the value, just to be sure.
+ set { _vibrationStrengthLeft = MathHelper.Clamp(value, 0, 1); }
+ }
+
+ /// <summary>
+ /// General setting for the strength of the right motor.
+ /// This motor has a snappy, quick, high-pitched rumble.
+ /// <para>
+ /// This setting will modify all future vibrations
+ /// through this listener.
+ /// </para>
+ /// </summary>
+ public float VibrationStrengthRight
+ {
+ get { return _vibrationStrengthRight; }
+ // Clamp the value, just to be sure.
+ set { _vibrationStrengthRight = MathHelper.Clamp(value, 0, 1); }
+ }
+
+ /// <summary>
+ /// The treshold of movement that has to be met in order
+ /// for the listener to fire an event with the trigger's
+ /// updated position.
+ /// <para>
+ /// In essence this defines the event's
+ /// resolution.
+ /// </para>
+ /// At a value of 0 this will fire every time
+ /// the trigger's position is not 0f.
+ /// </summary>
+ public float TriggerDeltaTreshold { get; }
+
+ /// <summary>
+ /// The treshold of movement that has to be met in order
+ /// for the listener to fire an event with the thumbstick's
+ /// updated position.
+ /// <para>
+ /// In essence this defines the event's
+ /// resolution.
+ /// </para>
+ /// At a value of 0 this will fire every time
+ /// the thumbstick's position is not {x:0, y:0}.
+ /// </summary>
+ public float ThumbStickDeltaTreshold { get; }
+
+ /// <summary>
+ /// How deep the triggers have to be depressed in order to
+ /// register as a ButtonDown event.
+ /// </summary>
+ public float TriggerDownTreshold { get; }
+
+ /// <summary>
+ /// How deep the triggers have to be depressed in order to
+ /// register as a ButtonDown event.
+ /// </summary>
+ public float ThumbstickDownTreshold { get; }
+
+ /// <summary>
+ /// This event fires whenever a controller connects or disconnects.
+ /// <para>
+ /// In order
+ /// for it to work, the <see cref="CheckControllerConnections" /> property must
+ /// be set to true.
+ /// </para>
+ /// </summary>
+ public static event EventHandler<GamePadEventArgs> ControllerConnectionChanged;
+
+ /// <summary>
+ /// This event fires whenever a button changes from the Up
+ /// to the Down state.
+ /// </summary>
+ public event EventHandler<GamePadEventArgs> ButtonDown;
+
+ /// <summary>
+ /// This event fires whenever a button changes from the Down
+ /// to the Up state.
+ /// </summary>
+ public event EventHandler<GamePadEventArgs> ButtonUp;
+
+ /// <summary>
+ /// This event fires repeatedly whenever a button is held sufficiently
+ /// long. Use this for things like menu navigation.
+ /// </summary>
+ public event EventHandler<GamePadEventArgs> ButtonRepeated;
+
+ /// <summary>
+ /// This event fires whenever a thumbstick changes position.
+ /// <para>
+ /// The parameter governing the sensitivity of this functionality
+ /// is <see cref="GamePadListenerSettings.ThumbStickDeltaTreshold" />.
+ /// </para>
+ /// </summary>
+ public event EventHandler<GamePadEventArgs> ThumbStickMoved;
+
+ /// <summary>
+ /// This event fires whenever a trigger changes position.
+ /// <para>
+ /// The parameter governing the sensitivity of this functionality
+ /// is <see cref="GamePadListenerSettings.TriggerDeltaTreshold" />.
+ /// </para>
+ /// </summary>
+ public event EventHandler<GamePadEventArgs> TriggerMoved;
+
+
+ /// <summary>
+ /// Send a vibration command to the controller.
+ /// Returns true if the operation succeeded.
+ /// <para>
+ /// Motor values that are unset preserve
+ /// their current vibration strength and duration.
+ /// </para>
+ /// Note: Vibration currently only works on select platforms,
+ /// like Monogame.Windows.
+ /// </summary>
+ /// <param name="durationMs">Duration of the vibration in milliseconds.</param>
+ /// <param name="leftStrength">
+ /// The strength of the left motor.
+ /// This motor has a slow, deep, powerful rumble.
+ /// </param>
+ /// <param name="rightStrength">
+ /// The strength of the right motor.
+ /// This motor has a snappy, quick, high-pitched rumble.
+ /// </param>
+ /// <returns>Returns true if the operation succeeded.</returns>
+ public bool Vibrate(int durationMs, float leftStrength = float.NegativeInfinity,
+ float rightStrength = float.NegativeInfinity)
+ {
+ if (!VibrationEnabled)
+ return false;
+
+ var lstrength = MathHelper.Clamp(leftStrength, 0, 1);
+ var rstrength = MathHelper.Clamp(rightStrength, 0, 1);
+
+ if (float.IsNegativeInfinity(leftStrength))
+ lstrength = _leftCurVibrationStrength;
+ if (float.IsNegativeInfinity(rightStrength))
+ rstrength = _rightCurVibrationStrength;
+
+ var success = GamePad.SetVibration(PlayerIndex, lstrength*VibrationStrengthLeft,
+ rstrength*VibrationStrengthRight);
+ if (success)
+ {
+ _leftVibrating = true;
+ _rightVibrating = true;
+
+ if (leftStrength > 0)
+ _vibrationDurationLeft = new TimeSpan(0, 0, 0, 0, durationMs);
+ else
+ {
+ if (lstrength > 0)
+ _vibrationDurationLeft -= _gameTime.TotalGameTime - _vibrationStart;
+ else
+ _leftVibrating = false;
+ }
+
+ if (rightStrength > 0)
+ _vibrationDurationRight = new TimeSpan(0, 0, 0, 0, durationMs);
+ else
+ {
+ if (rstrength > 0)
+ _vibrationDurationRight -= _gameTime.TotalGameTime - _vibrationStart;
+ else
+ _rightVibrating = false;
+ }
+
+ _vibrationStart = _gameTime.TotalGameTime;
+
+ _leftCurVibrationStrength = lstrength;
+ _rightCurVibrationStrength = rstrength;
+ }
+ return success;
+ }
+
+ private void CheckAllButtons()
+ {
+ // PacketNumber only and always changes if there is a difference between GamePadStates.
+ // ...At least, that's the theory. It doesn't seem to be implemented. Disabled for now.
+ //if (_lastPacketNumber == _currentState.PacketNumber)
+ // return;
+ foreach (Buttons button in Enum.GetValues(typeof(Buttons)))
+ {
+ if (_excludedButtons.Contains(button))
+ break;
+ if (_currentState.IsButtonDown(button) && _previousState.IsButtonUp(button))
+ RaiseButtonDown(button);
+ if (_currentState.IsButtonUp(button) && _previousState.IsButtonDown(button))
+ RaiseButtonUp(button);
+ }
+
+ // Checks triggers as buttons and floats
+ CheckTriggers(s => s.Triggers.Left, Buttons.LeftTrigger);
+ CheckTriggers(s => s.Triggers.Right, Buttons.RightTrigger);
+
+ // Checks thumbsticks as vector2s
+ CheckThumbSticks(s => s.ThumbSticks.Right, Buttons.RightStick);
+ CheckThumbSticks(s => s.ThumbSticks.Left, Buttons.LeftStick);
+ }
+
+ private void CheckTriggers(Func<GamePadState, float> getButtonState, Buttons button)
+ {
+ var debounce = 0.05f; // Value used to qualify a trigger as coming Up from a Down state
+ var curstate = getButtonState(_currentState);
+ var curdown = curstate > TriggerDownTreshold;
+ var prevdown = button == Buttons.RightTrigger ? _rightTriggerDown : _leftTriggerDown;
+
+ if (!prevdown && curdown)
+ {
+ RaiseButtonDown(button);
+ if (button == Buttons.RightTrigger)
+ _rightTriggerDown = true;
+ else
+ _leftTriggerDown = true;
+ }
+ else
+ {
+ if (prevdown && (curstate < debounce))
+ {
+ RaiseButtonUp(button);
+ if (button == Buttons.RightTrigger)
+ _rightTriggerDown = false;
+ else
+ _leftTriggerDown = false;
+ }
+ }
+
+ var prevstate = getButtonState(_lastTriggerState);
+ if (curstate > TriggerDeltaTreshold)
+ {
+ if (Math.Abs(prevstate - curstate) >= TriggerDeltaTreshold)
+ {
+ TriggerMoved?.Invoke(this, MakeArgs(button, curstate));
+ _lastTriggerState = _currentState;
+ }
+ }
+ else
+ {
+ if (prevstate > TriggerDeltaTreshold)
+ {
+ TriggerMoved?.Invoke(this, MakeArgs(button, curstate));
+ _lastTriggerState = _currentState;
+ }
+ }
+ }
+
+ private void CheckThumbSticks(Func<GamePadState, Vector2> getButtonState, Buttons button)
+ {
+ const float debounce = 0.15f;
+ var curVector = getButtonState(_currentState);
+ var curdown = curVector.Length() > ThumbstickDownTreshold;
+ var right = button == Buttons.RightStick;
+ var prevdown = right ? _rightStickDown : _leftStickDown;
+
+ var prevdir = button == Buttons.RightStick ? _lastRightStickDirection : _lastLeftStickDirection;
+ Buttons curdir;
+ if (curVector.Y > curVector.X)
+ {
+ if (curVector.Y > -curVector.X)
+ curdir = right ? Buttons.RightThumbstickUp : Buttons.LeftThumbstickUp;
+ else
+ curdir = right ? Buttons.RightThumbstickLeft : Buttons.LeftThumbstickLeft;
+ }
+ else
+ {
+ if (curVector.Y < -curVector.X)
+ curdir = right ? Buttons.RightThumbstickDown : Buttons.LeftThumbstickDown;
+ else
+ curdir = right ? Buttons.RightThumbstickRight : Buttons.LeftThumbstickRight;
+ }
+
+ if (!prevdown && curdown)
+ {
+ if (right)
+ _lastRightStickDirection = curdir;
+ else
+ _lastLeftStickDirection = curdir;
+
+ RaiseButtonDown(curdir);
+ if (button == Buttons.RightStick)
+ _rightStickDown = true;
+ else
+ _leftStickDown = true;
+ }
+ else
+ {
+ if (prevdown && (curVector.Length() < debounce))
+ {
+ RaiseButtonUp(prevdir);
+ if (button == Buttons.RightStick)
+ _rightStickDown = false;
+ else
+ _leftStickDown = false;
+ }
+ else
+ {
+ if (prevdown && curdown && (curdir != prevdir))
+ {
+ RaiseButtonUp(prevdir);
+ if (right)
+ _lastRightStickDirection = curdir;
+ else
+ _lastLeftStickDirection = curdir;
+ RaiseButtonDown(curdir);
+ }
+ }
+ }
+
+ var prevVector = getButtonState(_lastThumbStickState);
+ if (curVector.Length() > ThumbStickDeltaTreshold)
+ {
+ if (Vector2.Distance(curVector, prevVector) >= ThumbStickDeltaTreshold)
+ {
+ ThumbStickMoved?.Invoke(this, MakeArgs(button, thumbStickState: curVector));
+ _lastThumbStickState = _currentState;
+ }
+ }
+ else
+ {
+ if (prevVector.Length() > ThumbStickDeltaTreshold)
+ {
+ ThumbStickMoved?.Invoke(this, MakeArgs(button, thumbStickState: curVector));
+ _lastThumbStickState = _currentState;
+ }
+ }
+ }
+
+ internal static void CheckConnections()
+ {
+ if (!CheckControllerConnections)
+ return;
+
+ foreach (PlayerIndex index in Enum.GetValues(typeof(PlayerIndex)))
+ {
+ if (GamePad.GetState(index).IsConnected ^ _gamePadConnections[(int) index])
+ // We need more XORs in this world
+ {
+ _gamePadConnections[(int) index] = !_gamePadConnections[(int) index];
+ ControllerConnectionChanged?.Invoke(null,
+ new GamePadEventArgs(GamePadState.Default, GamePad.GetState(index), TimeSpan.Zero, index));
+ }
+ }
+ }
+
+ private void CheckVibrate()
+ {
+ if (_leftVibrating && (_vibrationStart + _vibrationDurationLeft < _gameTime.TotalGameTime))
+ Vibrate(0, 0);
+ if (_rightVibrating && (_vibrationStart + _vibrationDurationRight < _gameTime.TotalGameTime))
+ Vibrate(0, rightStrength: 0);
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ _gameTime = gameTime;
+ _currentState = GamePad.GetState(PlayerIndex);
+ CheckVibrate();
+ if (!_currentState.IsConnected)
+ return;
+ CheckAllButtons();
+ CheckRepeatButton();
+ //_lastPacketNumber = _currentState.PacketNumber;
+ _previousGameTime = gameTime;
+ _previousState = _currentState;
+ }
+
+ private GamePadEventArgs MakeArgs(Buttons? button,
+ float triggerstate = 0, Vector2? thumbStickState = null)
+ {
+ var elapsedTime = _gameTime.TotalGameTime - _previousGameTime.TotalGameTime;
+ return new GamePadEventArgs(_previousState, _currentState,
+ elapsedTime, PlayerIndex, button, triggerstate, thumbStickState);
+ }
+
+ private void RaiseButtonDown(Buttons button)
+ {
+ ButtonDown?.Invoke(this, MakeArgs(button));
+ ButtonRepeated?.Invoke(this, MakeArgs(button));
+ _lastButton = button;
+ _repeatedButtonTimer = 0;
+ }
+
+ private void RaiseButtonUp(Buttons button)
+ {
+ ButtonUp?.Invoke(this, MakeArgs(button));
+ _lastButton = 0;
+ }
+
+ private void CheckRepeatButton()
+ {
+ _repeatedButtonTimer += _gameTime.ElapsedGameTime.Milliseconds;
+
+ if ((_repeatedButtonTimer < RepeatInitialDelay) || (_lastButton == 0))
+ return;
+
+ if (_repeatedButtonTimer < RepeatInitialDelay + RepeatDelay)
+ {
+ ButtonRepeated?.Invoke(this, MakeArgs(_lastButton));
+ _repeatedButtonTimer = RepeatDelay + RepeatInitialDelay;
+ }
+ else
+ {
+ if (_repeatedButtonTimer > RepeatInitialDelay + RepeatDelay*2)
+ {
+ ButtonRepeated?.Invoke(this, MakeArgs(_lastButton));
+ _repeatedButtonTimer = RepeatDelay + RepeatInitialDelay;
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListenerSettings.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListenerSettings.cs
new file mode 100644
index 0000000..8c36e4c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListenerSettings.cs
@@ -0,0 +1,134 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ /// <summary>
+ /// This is a class that contains settings to be used to initialise a <see cref="GamePadListener" />.
+ /// </summary>
+ /// <seealso cref="InputListenerManager" />
+ public class GamePadListenerSettings : InputListenerSettings<GamePadListener>
+ {
+ public GamePadListenerSettings()
+ : this(PlayerIndex.One)
+ {
+ }
+
+ /// <summary>
+ /// This is a class that contains settings to be used to initialise a <see cref="GamePadListener" />.
+ /// <para>Note: There are a number of extra settings that are settable properties.</para>
+ /// </summary>
+ /// <param name="playerIndex">The index of the controller the listener will be tied to.</param>
+ /// <param name="vibrationEnabled">Whether vibration is enabled on the controller.</param>
+ /// <param name="vibrationStrengthLeft">
+ /// General setting for the strength of the left motor.
+ /// This motor has a slow, deep, powerful rumble.
+ /// This setting will modify all future vibrations
+ /// through this listener.
+ /// </param>
+ /// <param name="vibrationStrengthRight">
+ /// General setting for the strength of the right motor.
+ /// This motor has a snappy, quick, high-pitched rumble.
+ /// This setting will modify all future vibrations
+ /// through this listener.
+ /// </param>
+ public GamePadListenerSettings(PlayerIndex playerIndex, bool vibrationEnabled = true,
+ float vibrationStrengthLeft = 1.0f, float vibrationStrengthRight = 1.0f)
+ {
+ PlayerIndex = playerIndex;
+ VibrationEnabled = vibrationEnabled;
+ VibrationStrengthLeft = vibrationStrengthLeft;
+ VibrationStrengthRight = vibrationStrengthRight;
+ TriggerDownTreshold = 0.15f;
+ ThumbstickDownTreshold = 0.5f;
+ RepeatInitialDelay = 500;
+ RepeatDelay = 50;
+ }
+
+ /// <summary>
+ /// The index of the controller.
+ /// </summary>
+ public PlayerIndex PlayerIndex { get; set; }
+
+ /// <summary>
+ /// When a button is held down, the interval in which
+ /// ButtonRepeated fires. Value in milliseconds.
+ /// </summary>
+ public int RepeatDelay { get; set; }
+
+ /// <summary>
+ /// The amount of time a button has to be held down
+ /// in order to fire ButtonRepeated the first time.
+ /// Value in milliseconds.
+ /// </summary>
+ public int RepeatInitialDelay { get; set; }
+
+
+ /// <summary>
+ /// Whether vibration is enabled for this controller.
+ /// </summary>
+ public bool VibrationEnabled { get; set; }
+
+ /// <summary>
+ /// General setting for the strength of the left motor.
+ /// This motor has a slow, deep, powerful rumble.
+ /// <para>
+ /// This setting will modify all future vibrations
+ /// through this listener.
+ /// </para>
+ /// </summary>
+ public float VibrationStrengthLeft { get; set; }
+
+ /// <summary>
+ /// General setting for the strength of the right motor.
+ /// This motor has a snappy, quick, high-pitched rumble.
+ /// <para>
+ /// This setting will modify all future vibrations
+ /// through this listener.
+ /// </para>
+ /// </summary>
+ public float VibrationStrengthRight { get; set; }
+
+ /// <summary>
+ /// The treshold of movement that has to be met in order
+ /// for the listener to fire an event with the trigger's
+ /// updated position.
+ /// <para>
+ /// In essence this defines the event's
+ /// resolution.
+ /// </para>
+ /// At a value of 0 this will fire every time
+ /// the trigger's position is not 0f.
+ /// </summary>
+ public float TriggerDeltaTreshold { get; set; }
+
+ /// <summary>
+ /// The treshold of movement that has to be met in order
+ /// for the listener to fire an event with the thumbstick's
+ /// updated position.
+ /// <para>
+ /// In essence this defines the event's
+ /// resolution.
+ /// </para>
+ /// At a value of 0 this will fire every time
+ /// the thumbstick's position is not {x:0, y:0}.
+ /// </summary>
+ public float ThumbStickDeltaTreshold { get; set; }
+
+ /// <summary>
+ /// How deep the triggers have to be depressed in order to
+ /// register as a ButtonDown event.
+ /// </summary>
+ public float TriggerDownTreshold { get; set; }
+
+ /// <summary>
+ /// How deep the triggers have to be depressed in order to
+ /// register as a ButtonDown event.
+ /// </summary>
+ public float ThumbstickDownTreshold { get; private set; }
+
+ public override GamePadListener CreateListener()
+ {
+ return new GamePadListener(this);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/IInputService.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/IInputService.cs
new file mode 100644
index 0000000..46198ec
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/IInputService.cs
@@ -0,0 +1,13 @@
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public interface IInputService
+ {
+ KeyboardListener GuiKeyboardListener { get; }
+
+ MouseListener GuiMouseListener { get; }
+
+ GamePadListener GuiGamePadListener { get; }
+
+ TouchListener GuiTouchListener { get; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListener.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListener.cs
new file mode 100644
index 0000000..6323295
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListener.cs
@@ -0,0 +1,13 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public abstract class InputListener
+ {
+ protected InputListener()
+ {
+ }
+
+ public abstract void Update(GameTime gameTime);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerComponent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerComponent.cs
new file mode 100644
index 0000000..302f6b5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerComponent.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public class InputListenerComponent : GameComponent, IUpdate
+ {
+ private readonly List<InputListener> _listeners;
+
+ public InputListenerComponent(Game game)
+ : base(game)
+ {
+ _listeners = new List<InputListener>();
+ }
+
+ public InputListenerComponent(Game game, params InputListener[] listeners)
+ : base(game)
+ {
+ _listeners = new List<InputListener>(listeners);
+ }
+
+ public IList<InputListener> Listeners => _listeners;
+
+ public override void Update(GameTime gameTime)
+ {
+ base.Update(gameTime);
+
+ if (Game.IsActive)
+ {
+ foreach (var listener in _listeners)
+ listener.Update(gameTime);
+ }
+
+ GamePadListener.CheckConnections();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerSettings.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerSettings.cs
new file mode 100644
index 0000000..468a30b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerSettings.cs
@@ -0,0 +1,8 @@
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public abstract class InputListenerSettings<T>
+ where T : InputListener
+ {
+ public abstract T CreateListener();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardEventArgs.cs
new file mode 100644
index 0000000..d6d01ab
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardEventArgs.cs
@@ -0,0 +1,119 @@
+using System;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public class KeyboardEventArgs : EventArgs
+ {
+ public KeyboardEventArgs(Keys key, KeyboardState keyboardState)
+ {
+ Key = key;
+
+ Modifiers = KeyboardModifiers.None;
+
+ if (keyboardState.IsKeyDown(Keys.LeftControl) || keyboardState.IsKeyDown(Keys.RightControl))
+ Modifiers |= KeyboardModifiers.Control;
+
+ if (keyboardState.IsKeyDown(Keys.LeftShift) || keyboardState.IsKeyDown(Keys.RightShift))
+ Modifiers |= KeyboardModifiers.Shift;
+
+ if (keyboardState.IsKeyDown(Keys.LeftAlt) || keyboardState.IsKeyDown(Keys.RightAlt))
+ Modifiers |= KeyboardModifiers.Alt;
+ }
+
+ public Keys Key { get; }
+ public KeyboardModifiers Modifiers { get; }
+
+ public char? Character => ToChar(Key, Modifiers);
+
+ private static char? ToChar(Keys key, KeyboardModifiers modifiers = KeyboardModifiers.None)
+ {
+ var isShiftDown = (modifiers & KeyboardModifiers.Shift) == KeyboardModifiers.Shift;
+
+ if (key == Keys.A) return isShiftDown ? 'A' : 'a';
+ if (key == Keys.B) return isShiftDown ? 'B' : 'b';
+ if (key == Keys.C) return isShiftDown ? 'C' : 'c';
+ if (key == Keys.D) return isShiftDown ? 'D' : 'd';
+ if (key == Keys.E) return isShiftDown ? 'E' : 'e';
+ if (key == Keys.F) return isShiftDown ? 'F' : 'f';
+ if (key == Keys.G) return isShiftDown ? 'G' : 'g';
+ if (key == Keys.H) return isShiftDown ? 'H' : 'h';
+ if (key == Keys.I) return isShiftDown ? 'I' : 'i';
+ if (key == Keys.J) return isShiftDown ? 'J' : 'j';
+ if (key == Keys.K) return isShiftDown ? 'K' : 'k';
+ if (key == Keys.L) return isShiftDown ? 'L' : 'l';
+ if (key == Keys.M) return isShiftDown ? 'M' : 'm';
+ if (key == Keys.N) return isShiftDown ? 'N' : 'n';
+ if (key == Keys.O) return isShiftDown ? 'O' : 'o';
+ if (key == Keys.P) return isShiftDown ? 'P' : 'p';
+ if (key == Keys.Q) return isShiftDown ? 'Q' : 'q';
+ if (key == Keys.R) return isShiftDown ? 'R' : 'r';
+ if (key == Keys.S) return isShiftDown ? 'S' : 's';
+ if (key == Keys.T) return isShiftDown ? 'T' : 't';
+ if (key == Keys.U) return isShiftDown ? 'U' : 'u';
+ if (key == Keys.V) return isShiftDown ? 'V' : 'v';
+ if (key == Keys.W) return isShiftDown ? 'W' : 'w';
+ if (key == Keys.X) return isShiftDown ? 'X' : 'x';
+ if (key == Keys.Y) return isShiftDown ? 'Y' : 'y';
+ if (key == Keys.Z) return isShiftDown ? 'Z' : 'z';
+
+ if (((key == Keys.D0) && !isShiftDown) || (key == Keys.NumPad0)) return '0';
+ if (((key == Keys.D1) && !isShiftDown) || (key == Keys.NumPad1)) return '1';
+ if (((key == Keys.D2) && !isShiftDown) || (key == Keys.NumPad2)) return '2';
+ if (((key == Keys.D3) && !isShiftDown) || (key == Keys.NumPad3)) return '3';
+ if (((key == Keys.D4) && !isShiftDown) || (key == Keys.NumPad4)) return '4';
+ if (((key == Keys.D5) && !isShiftDown) || (key == Keys.NumPad5)) return '5';
+ if (((key == Keys.D6) && !isShiftDown) || (key == Keys.NumPad6)) return '6';
+ if (((key == Keys.D7) && !isShiftDown) || (key == Keys.NumPad7)) return '7';
+ if (((key == Keys.D8) && !isShiftDown) || (key == Keys.NumPad8)) return '8';
+ if (((key == Keys.D9) && !isShiftDown) || (key == Keys.NumPad9)) return '9';
+
+ if ((key == Keys.D0) && isShiftDown) return ')';
+ if ((key == Keys.D1) && isShiftDown) return '!';
+ if ((key == Keys.D2) && isShiftDown) return '@';
+ if ((key == Keys.D3) && isShiftDown) return '#';
+ if ((key == Keys.D4) && isShiftDown) return '$';
+ if ((key == Keys.D5) && isShiftDown) return '%';
+ if ((key == Keys.D6) && isShiftDown) return '^';
+ if ((key == Keys.D7) && isShiftDown) return '&';
+ if ((key == Keys.D8) && isShiftDown) return '*';
+ if ((key == Keys.D9) && isShiftDown) return '(';
+
+ if (key == Keys.Space) return ' ';
+ if (key == Keys.Tab) return '\t';
+ if (key == Keys.Enter) return (char) 13;
+ if (key == Keys.Back) return (char) 8;
+
+ if (key == Keys.Add) return '+';
+ if (key == Keys.Decimal) return '.';
+ if (key == Keys.Divide) return '/';
+ if (key == Keys.Multiply) return '*';
+ if (key == Keys.OemBackslash) return '\\';
+ if ((key == Keys.OemComma) && !isShiftDown) return ',';
+ if ((key == Keys.OemComma) && isShiftDown) return '<';
+ if ((key == Keys.OemOpenBrackets) && !isShiftDown) return '[';
+ if ((key == Keys.OemOpenBrackets) && isShiftDown) return '{';
+ if ((key == Keys.OemCloseBrackets) && !isShiftDown) return ']';
+ if ((key == Keys.OemCloseBrackets) && isShiftDown) return '}';
+ if ((key == Keys.OemPeriod) && !isShiftDown) return '.';
+ if ((key == Keys.OemPeriod) && isShiftDown) return '>';
+ if ((key == Keys.OemPipe) && !isShiftDown) return '\\';
+ if ((key == Keys.OemPipe) && isShiftDown) return '|';
+ if ((key == Keys.OemPlus) && !isShiftDown) return '=';
+ if ((key == Keys.OemPlus) && isShiftDown) return '+';
+ if ((key == Keys.OemMinus) && !isShiftDown) return '-';
+ if ((key == Keys.OemMinus) && isShiftDown) return '_';
+ if ((key == Keys.OemQuestion) && !isShiftDown) return '/';
+ if ((key == Keys.OemQuestion) && isShiftDown) return '?';
+ if ((key == Keys.OemQuotes) && !isShiftDown) return '\'';
+ if ((key == Keys.OemQuotes) && isShiftDown) return '"';
+ if ((key == Keys.OemSemicolon) && !isShiftDown) return ';';
+ if ((key == Keys.OemSemicolon) && isShiftDown) return ':';
+ if ((key == Keys.OemTilde) && !isShiftDown) return '`';
+ if ((key == Keys.OemTilde) && isShiftDown) return '~';
+ if (key == Keys.Subtract) return '-';
+
+ return null;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListener.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListener.cs
new file mode 100644
index 0000000..9fc85a0
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListener.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public class KeyboardListener : InputListener
+ {
+ private Array _keysValues = Enum.GetValues(typeof(Keys));
+
+ private bool _isInitial;
+ private TimeSpan _lastPressTime;
+
+ private Keys _previousKey;
+ private KeyboardState _previousState;
+
+ public KeyboardListener()
+ : this(new KeyboardListenerSettings())
+ {
+ }
+
+ public KeyboardListener(KeyboardListenerSettings settings)
+ {
+ RepeatPress = settings.RepeatPress;
+ InitialDelay = settings.InitialDelayMilliseconds;
+ RepeatDelay = settings.RepeatDelayMilliseconds;
+ }
+
+ public bool RepeatPress { get; }
+ public int InitialDelay { get; }
+ public int RepeatDelay { get; }
+
+ public event EventHandler<KeyboardEventArgs> KeyTyped;
+ public event EventHandler<KeyboardEventArgs> KeyPressed;
+ public event EventHandler<KeyboardEventArgs> KeyReleased;
+
+ public override void Update(GameTime gameTime)
+ {
+ var currentState = Keyboard.GetState();
+
+ RaisePressedEvents(gameTime, currentState);
+ RaiseReleasedEvents(currentState);
+
+ if (RepeatPress)
+ RaiseRepeatEvents(gameTime, currentState);
+
+ _previousState = currentState;
+ }
+
+ private void RaisePressedEvents(GameTime gameTime, KeyboardState currentState)
+ {
+ if (!currentState.IsKeyDown(Keys.LeftAlt) && !currentState.IsKeyDown(Keys.RightAlt))
+ {
+ var pressedKeys = _keysValues
+ .Cast<Keys>()
+ .Where(key => currentState.IsKeyDown(key) && _previousState.IsKeyUp(key));
+
+ foreach (var key in pressedKeys)
+ {
+ var args = new KeyboardEventArgs(key, currentState);
+
+ KeyPressed?.Invoke(this, args);
+
+ if (args.Character.HasValue)
+ KeyTyped?.Invoke(this, args);
+
+ _previousKey = key;
+ _lastPressTime = gameTime.TotalGameTime;
+ _isInitial = true;
+ }
+ }
+ }
+
+ private void RaiseReleasedEvents(KeyboardState currentState)
+ {
+ var releasedKeys = _keysValues
+ .Cast<Keys>()
+ .Where(key => currentState.IsKeyUp(key) && _previousState.IsKeyDown(key));
+
+ foreach (var key in releasedKeys)
+ KeyReleased?.Invoke(this, new KeyboardEventArgs(key, currentState));
+ }
+
+ private void RaiseRepeatEvents(GameTime gameTime, KeyboardState currentState)
+ {
+ var elapsedTime = (gameTime.TotalGameTime - _lastPressTime).TotalMilliseconds;
+
+ if (currentState.IsKeyDown(_previousKey) &&
+ (_isInitial && elapsedTime > InitialDelay || !_isInitial && elapsedTime > RepeatDelay))
+ {
+ var args = new KeyboardEventArgs(_previousKey, currentState);
+
+ KeyPressed?.Invoke(this, args);
+
+ if (args.Character.HasValue)
+ KeyTyped?.Invoke(this, args);
+
+ _lastPressTime = gameTime.TotalGameTime;
+ _isInitial = false;
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListenerSettings.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListenerSettings.cs
new file mode 100644
index 0000000..86481ed
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListenerSettings.cs
@@ -0,0 +1,21 @@
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public class KeyboardListenerSettings : InputListenerSettings<KeyboardListener>
+ {
+ public KeyboardListenerSettings()
+ {
+ RepeatPress = true;
+ InitialDelayMilliseconds = 800;
+ RepeatDelayMilliseconds = 50;
+ }
+
+ public bool RepeatPress { get; set; }
+ public int InitialDelayMilliseconds { get; set; }
+ public int RepeatDelayMilliseconds { get; set; }
+
+ public override KeyboardListener CreateListener()
+ {
+ return new KeyboardListener(this);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardModifiers.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardModifiers.cs
new file mode 100644
index 0000000..de59905
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardModifiers.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ [Flags]
+ public enum KeyboardModifiers
+ {
+ Control = 1,
+ Shift = 2,
+ Alt = 4,
+ None = 0
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseEventArgs.cs
new file mode 100644
index 0000000..2717325
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseEventArgs.cs
@@ -0,0 +1,35 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGame.Extended.ViewportAdapters;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public class MouseEventArgs : EventArgs
+ {
+ public MouseEventArgs(ViewportAdapter viewportAdapter, TimeSpan time, MouseState previousState,
+ MouseState currentState,
+ MouseButton button = MouseButton.None)
+ {
+ PreviousState = previousState;
+ CurrentState = currentState;
+ Position = viewportAdapter?.PointToScreen(currentState.X, currentState.Y)
+ ?? new Point(currentState.X, currentState.Y);
+ Button = button;
+ ScrollWheelValue = currentState.ScrollWheelValue;
+ ScrollWheelDelta = currentState.ScrollWheelValue - previousState.ScrollWheelValue;
+ Time = time;
+ }
+
+ public TimeSpan Time { get; }
+
+ public MouseState PreviousState { get; }
+ public MouseState CurrentState { get; }
+ public Point Position { get; }
+ public MouseButton Button { get; }
+ public int ScrollWheelValue { get; }
+ public int ScrollWheelDelta { get; }
+
+ public Vector2 DistanceMoved => CurrentState.Position.ToVector2() - PreviousState.Position.ToVector2();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListener.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListener.cs
new file mode 100644
index 0000000..e71a67f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListener.cs
@@ -0,0 +1,193 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGame.Extended.ViewportAdapters;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ /// <summary>
+ /// Handles mouse input.
+ /// </summary>
+ /// <remarks>
+ /// Due to nature of the listener, even when game is not in focus, listener will continue to be updated.
+ /// To avoid that, manual pause of Update() method is required whenever game loses focus.
+ /// To avoid having to do it manually, register listener to <see cref="InputListenerComponent" />
+ /// </remarks>
+ public class MouseListener : InputListener
+ {
+ private MouseState _currentState;
+ private bool _dragging;
+ private GameTime _gameTime;
+ private bool _hasDoubleClicked;
+ private MouseEventArgs _mouseDownArgs;
+ private MouseEventArgs _previousClickArgs;
+ private MouseState _previousState;
+
+ public MouseListener()
+ : this(new MouseListenerSettings())
+ {
+ }
+
+ public MouseListener(ViewportAdapter viewportAdapter)
+ : this(new MouseListenerSettings())
+ {
+ ViewportAdapter = viewportAdapter;
+ }
+
+ public MouseListener(MouseListenerSettings settings)
+ {
+ ViewportAdapter = settings.ViewportAdapter;
+ DoubleClickMilliseconds = settings.DoubleClickMilliseconds;
+ DragThreshold = settings.DragThreshold;
+ }
+
+ public ViewportAdapter ViewportAdapter { get; }
+
+ public int DoubleClickMilliseconds { get; }
+ public int DragThreshold { get; }
+
+ /// <summary>
+ /// Returns true if the mouse has moved between the current and previous frames.
+ /// </summary>
+ /// <value><c>true</c> if the mouse has moved; otherwise, <c>false</c>.</value>
+ public bool HasMouseMoved => (_previousState.X != _currentState.X) || (_previousState.Y != _currentState.Y);
+
+ public event EventHandler<MouseEventArgs> MouseDown;
+ public event EventHandler<MouseEventArgs> MouseUp;
+ public event EventHandler<MouseEventArgs> MouseClicked;
+ public event EventHandler<MouseEventArgs> MouseDoubleClicked;
+ public event EventHandler<MouseEventArgs> MouseMoved;
+ public event EventHandler<MouseEventArgs> MouseWheelMoved;
+ public event EventHandler<MouseEventArgs> MouseDragStart;
+ public event EventHandler<MouseEventArgs> MouseDrag;
+ public event EventHandler<MouseEventArgs> MouseDragEnd;
+
+ private void CheckButtonPressed(Func<MouseState, ButtonState> getButtonState, MouseButton button)
+ {
+ if ((getButtonState(_currentState) == ButtonState.Pressed) &&
+ (getButtonState(_previousState) == ButtonState.Released))
+ {
+ var args = new MouseEventArgs(ViewportAdapter, _gameTime.TotalGameTime, _previousState, _currentState, button);
+
+ MouseDown?.Invoke(this, args);
+ _mouseDownArgs = args;
+
+ if (_previousClickArgs != null)
+ {
+ // If the last click was recent
+ var clickMilliseconds = (args.Time - _previousClickArgs.Time).TotalMilliseconds;
+
+ if (clickMilliseconds <= DoubleClickMilliseconds)
+ {
+ MouseDoubleClicked?.Invoke(this, args);
+ _hasDoubleClicked = true;
+ }
+
+ _previousClickArgs = null;
+ }
+ }
+ }
+
+ private void CheckButtonReleased(Func<MouseState, ButtonState> getButtonState, MouseButton button)
+ {
+ if ((getButtonState(_currentState) == ButtonState.Released) &&
+ (getButtonState(_previousState) == ButtonState.Pressed))
+ {
+ var args = new MouseEventArgs(ViewportAdapter, _gameTime.TotalGameTime, _previousState, _currentState, button);
+
+ if (_mouseDownArgs.Button == args.Button)
+ {
+ var clickMovement = DistanceBetween(args.Position, _mouseDownArgs.Position);
+
+ // If the mouse hasn't moved much between mouse down and mouse up
+ if (clickMovement < DragThreshold)
+ {
+ if (!_hasDoubleClicked)
+ MouseClicked?.Invoke(this, args);
+ }
+ else // If the mouse has moved between mouse down and mouse up
+ {
+ MouseDragEnd?.Invoke(this, args);
+ _dragging = false;
+ }
+ }
+
+ MouseUp?.Invoke(this, args);
+
+ _hasDoubleClicked = false;
+ _previousClickArgs = args;
+ }
+ }
+
+ private void CheckMouseDragged(Func<MouseState, ButtonState> getButtonState, MouseButton button)
+ {
+ if ((getButtonState(_currentState) == ButtonState.Pressed) &&
+ (getButtonState(_previousState) == ButtonState.Pressed))
+ {
+ var args = new MouseEventArgs(ViewportAdapter, _gameTime.TotalGameTime, _previousState, _currentState, button);
+
+ if (_mouseDownArgs.Button == args.Button)
+ {
+ if (_dragging)
+ MouseDrag?.Invoke(this, args);
+ else
+ {
+ // Only start to drag based on DragThreshold
+ var clickMovement = DistanceBetween(args.Position, _mouseDownArgs.Position);
+
+ if (clickMovement > DragThreshold)
+ {
+ _dragging = true;
+ MouseDragStart?.Invoke(this, args);
+ }
+ }
+ }
+ }
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ _gameTime = gameTime;
+ _currentState = Mouse.GetState();
+
+ CheckButtonPressed(s => s.LeftButton, MouseButton.Left);
+ CheckButtonPressed(s => s.MiddleButton, MouseButton.Middle);
+ CheckButtonPressed(s => s.RightButton, MouseButton.Right);
+ CheckButtonPressed(s => s.XButton1, MouseButton.XButton1);
+ CheckButtonPressed(s => s.XButton2, MouseButton.XButton2);
+
+ CheckButtonReleased(s => s.LeftButton, MouseButton.Left);
+ CheckButtonReleased(s => s.MiddleButton, MouseButton.Middle);
+ CheckButtonReleased(s => s.RightButton, MouseButton.Right);
+ CheckButtonReleased(s => s.XButton1, MouseButton.XButton1);
+ CheckButtonReleased(s => s.XButton2, MouseButton.XButton2);
+
+ // Check for any sort of mouse movement.
+ if (HasMouseMoved)
+ {
+ MouseMoved?.Invoke(this,
+ new MouseEventArgs(ViewportAdapter, gameTime.TotalGameTime, _previousState, _currentState));
+
+ CheckMouseDragged(s => s.LeftButton, MouseButton.Left);
+ CheckMouseDragged(s => s.MiddleButton, MouseButton.Middle);
+ CheckMouseDragged(s => s.RightButton, MouseButton.Right);
+ CheckMouseDragged(s => s.XButton1, MouseButton.XButton1);
+ CheckMouseDragged(s => s.XButton2, MouseButton.XButton2);
+ }
+
+ // Handle mouse wheel events.
+ if (_previousState.ScrollWheelValue != _currentState.ScrollWheelValue)
+ {
+ MouseWheelMoved?.Invoke(this,
+ new MouseEventArgs(ViewportAdapter, gameTime.TotalGameTime, _previousState, _currentState));
+ }
+
+ _previousState = _currentState;
+ }
+
+ private static int DistanceBetween(Point a, Point b)
+ {
+ return Math.Abs(a.X - b.X) + Math.Abs(a.Y - b.Y);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListenerSettings.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListenerSettings.cs
new file mode 100644
index 0000000..1c0ca3d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListenerSettings.cs
@@ -0,0 +1,23 @@
+using MonoGame.Extended.ViewportAdapters;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public class MouseListenerSettings : InputListenerSettings<MouseListener>
+ {
+ public MouseListenerSettings()
+ {
+ // initial values are windows defaults
+ DoubleClickMilliseconds = 500;
+ DragThreshold = 2;
+ }
+
+ public int DragThreshold { get; set; }
+ public int DoubleClickMilliseconds { get; set; }
+ public ViewportAdapter ViewportAdapter { get; set; }
+
+ public override MouseListener CreateListener()
+ {
+ return new MouseListener(this);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchEventArgs.cs
new file mode 100644
index 0000000..8172885
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchEventArgs.cs
@@ -0,0 +1,39 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input.Touch;
+using MonoGame.Extended.ViewportAdapters;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public class TouchEventArgs : EventArgs
+ {
+ public TouchEventArgs(ViewportAdapter viewportAdapter, TimeSpan time, TouchLocation location)
+ {
+ ViewportAdapter = viewportAdapter;
+ RawTouchLocation = location;
+ Time = time;
+ Position = viewportAdapter?.PointToScreen((int)location.Position.X, (int)location.Position.Y)
+ ?? location.Position.ToPoint();
+ }
+
+ public ViewportAdapter ViewportAdapter { get; }
+ public TouchLocation RawTouchLocation { get; }
+ public TimeSpan Time { get; }
+ public Point Position { get; }
+
+ public override bool Equals(object other)
+ {
+ var args = other as TouchEventArgs;
+
+ if (args == null)
+ return false;
+
+ return ReferenceEquals(this, args) || RawTouchLocation.Id.Equals(args.RawTouchLocation.Id);
+ }
+
+ public override int GetHashCode()
+ {
+ return RawTouchLocation.Id.GetHashCode();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListener.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListener.cs
new file mode 100644
index 0000000..2a89cc9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListener.cs
@@ -0,0 +1,57 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input.Touch;
+using MonoGame.Extended.ViewportAdapters;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public class TouchListener : InputListener
+ {
+ public TouchListener()
+ : this(new TouchListenerSettings())
+ {
+ }
+
+ public TouchListener(ViewportAdapter viewportAdapter)
+ : this(new TouchListenerSettings())
+ {
+ ViewportAdapter = viewportAdapter;
+ }
+
+ public TouchListener(TouchListenerSettings settings)
+ {
+ ViewportAdapter = settings.ViewportAdapter;
+ }
+
+ public ViewportAdapter ViewportAdapter { get; set; }
+
+ public event EventHandler<TouchEventArgs> TouchStarted;
+ public event EventHandler<TouchEventArgs> TouchEnded;
+ public event EventHandler<TouchEventArgs> TouchMoved;
+ public event EventHandler<TouchEventArgs> TouchCancelled;
+
+ public override void Update(GameTime gameTime)
+ {
+ var touchCollection = TouchPanel.GetState();
+
+ foreach (var touchLocation in touchCollection)
+ {
+ switch (touchLocation.State)
+ {
+ case TouchLocationState.Pressed:
+ TouchStarted?.Invoke(this, new TouchEventArgs(ViewportAdapter, gameTime.TotalGameTime, touchLocation));
+ break;
+ case TouchLocationState.Moved:
+ TouchMoved?.Invoke(this, new TouchEventArgs(ViewportAdapter, gameTime.TotalGameTime, touchLocation));
+ break;
+ case TouchLocationState.Released:
+ TouchEnded?.Invoke(this, new TouchEventArgs(ViewportAdapter, gameTime.TotalGameTime, touchLocation));
+ break;
+ case TouchLocationState.Invalid:
+ TouchCancelled?.Invoke(this, new TouchEventArgs(ViewportAdapter, gameTime.TotalGameTime, touchLocation));
+ break;
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListenerSettings.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListenerSettings.cs
new file mode 100644
index 0000000..6d42b42
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListenerSettings.cs
@@ -0,0 +1,18 @@
+using MonoGame.Extended.ViewportAdapters;
+
+namespace MonoGame.Extended.Input.InputListeners
+{
+ public class TouchListenerSettings : InputListenerSettings<TouchListener>
+ {
+ public TouchListenerSettings()
+ {
+ }
+
+ public ViewportAdapter ViewportAdapter { get; set; }
+
+ public override TouchListener CreateListener()
+ {
+ return new TouchListener(this);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardExtended.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardExtended.cs
new file mode 100644
index 0000000..0ed7c76
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardExtended.cs
@@ -0,0 +1,22 @@
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGame.Extended.Input
+{
+ public static class KeyboardExtended
+ {
+ // TODO: This global static state was a horrible idea.
+ private static KeyboardState _currentKeyboardState;
+ private static KeyboardState _previousKeyboardState;
+
+ public static KeyboardStateExtended GetState()
+ {
+ return new KeyboardStateExtended(_currentKeyboardState, _previousKeyboardState);
+ }
+
+ public static void Refresh()
+ {
+ _previousKeyboardState = _currentKeyboardState;
+ _currentKeyboardState = Keyboard.GetState();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardStateExtended.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardStateExtended.cs
new file mode 100644
index 0000000..ee18677
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardStateExtended.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Linq;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGame.Extended.Input
+{
+ public struct KeyboardStateExtended
+ {
+ private KeyboardState _currentKeyboardState;
+ private KeyboardState _previousKeyboardState;
+
+ public KeyboardStateExtended(KeyboardState currentKeyboardState, KeyboardState previousKeyboardState)
+ {
+ _currentKeyboardState = currentKeyboardState;
+ _previousKeyboardState = previousKeyboardState;
+ }
+
+ public bool CapsLock => _currentKeyboardState.CapsLock;
+ public bool NumLock => _currentKeyboardState.NumLock;
+ public bool IsShiftDown() => _currentKeyboardState.IsKeyDown(Keys.LeftShift) || _currentKeyboardState.IsKeyDown(Keys.RightShift);
+ public bool IsControlDown() => _currentKeyboardState.IsKeyDown(Keys.LeftControl) || _currentKeyboardState.IsKeyDown(Keys.RightControl);
+ public bool IsAltDown() => _currentKeyboardState.IsKeyDown(Keys.LeftAlt) || _currentKeyboardState.IsKeyDown(Keys.RightAlt);
+ public bool IsKeyDown(Keys key) => _currentKeyboardState.IsKeyDown(key);
+ public bool IsKeyUp(Keys key) => _currentKeyboardState.IsKeyUp(key);
+ public Keys[] GetPressedKeys() => _currentKeyboardState.GetPressedKeys();
+ public void GetPressedKeys(Keys[] keys) => _currentKeyboardState.GetPressedKeys(keys);
+
+ /// <summary>
+ /// Gets whether the given key was down on the previous state, but is now up.
+ /// </summary>
+ /// <param name="key">The key to check.</param>
+ /// <returns>true if the key was released this state-change, otherwise false.</returns>
+ [Obsolete($"Deprecated in favor of {nameof(IsKeyReleased)}")]
+ public bool WasKeyJustDown(Keys key) => _previousKeyboardState.IsKeyDown(key) && _currentKeyboardState.IsKeyUp(key);
+
+ /// <summary>
+ /// Gets whether the given key was up on the previous state, but is now down.
+ /// </summary>
+ /// <param name="key">The key to check.</param>
+ /// <returns>true if the key was pressed this state-change, otherwise false.</returns>
+ [Obsolete($"Deprecated in favor of {nameof(IsKeyPressed)}")]
+ public bool WasKeyJustUp(Keys key) => _previousKeyboardState.IsKeyUp(key) && _currentKeyboardState.IsKeyDown(key);
+
+ /// <summary>
+ /// Gets whether the given key was down on the previous state, but is now up.
+ /// </summary>
+ /// <param name="key">The key to check.</param>
+ /// <returns>true if the key was released this state-change, otherwise false.</returns>
+ public readonly bool IsKeyReleased(Keys key) => _previousKeyboardState.IsKeyDown(key) && _currentKeyboardState.IsKeyUp(key);
+
+ /// <summary>
+ /// Gets whether the given key was up on the previous state, but is now down.
+ /// </summary>
+ /// <param name="key">The key to check.</param>
+ /// <returns>true if the key was pressed this state-change, otherwise false.</returns>
+ public readonly bool IsKeyPressed(Keys key) => _previousKeyboardState.IsKeyUp(key) && _currentKeyboardState.IsKeyDown(key);
+
+ public bool WasAnyKeyJustDown() => _previousKeyboardState.GetPressedKeyCount() > 0;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MonoGame.Extended.Input.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MonoGame.Extended.Input.csproj
new file mode 100644
index 0000000..4b3bd82
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MonoGame.Extended.Input.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>An event based input system to MonoGame more awesome.</Description>
+ <PackageTags>monogame input event based listeners</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseButton.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseButton.cs
new file mode 100644
index 0000000..e4a00f8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseButton.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace MonoGame.Extended.Input
+{
+ [Flags]
+ public enum MouseButton
+ {
+ None = 0,
+ Left = 1,
+ Middle = 2,
+ Right = 4,
+ XButton1 = 8,
+ XButton2 = 16
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseExtended.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseExtended.cs
new file mode 100644
index 0000000..61d6d18
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseExtended.cs
@@ -0,0 +1,34 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGame.Extended.Input
+{
+ public static class MouseExtended
+ {
+ // TODO: This global static state was a horrible idea.
+ private static MouseState _currentMouseState;
+ private static MouseState _previousMouseState;
+
+ public static MouseStateExtended GetState()
+ {
+ return new MouseStateExtended(_currentMouseState, _previousMouseState);
+ }
+
+ public static void Refresh()
+ {
+ _previousMouseState = _currentMouseState;
+ _currentMouseState = Mouse.GetState();
+ }
+
+ public static void SetPosition(int x, int y) => Mouse.SetPosition(x, y);
+ public static void SetPosition(Point point) => Mouse.SetPosition(point.X, point.Y);
+ public static void SetCursor(MouseCursor cursor) => Mouse.SetCursor(cursor);
+
+ public static IntPtr WindowHandle
+ {
+ get => Mouse.WindowHandle;
+ set => Mouse.WindowHandle = value;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseStateExtended.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseStateExtended.cs
new file mode 100644
index 0000000..0ec6943
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseStateExtended.cs
@@ -0,0 +1,149 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGame.Extended.Input
+{
+ public struct MouseStateExtended
+ {
+ private readonly MouseState _currentMouseState;
+ private readonly MouseState _previousMouseState;
+
+ public MouseStateExtended(MouseState currentMouseState, MouseState previousMouseState)
+ {
+ _currentMouseState = currentMouseState;
+ _previousMouseState = previousMouseState;
+ }
+
+ public int X => _currentMouseState.X;
+ public int Y => _currentMouseState.Y;
+ public Point Position => _currentMouseState.Position;
+ public bool PositionChanged => _currentMouseState.Position != _previousMouseState.Position;
+
+ public int DeltaX => _previousMouseState.X - _currentMouseState.X;
+ public int DeltaY => _previousMouseState.Y - _currentMouseState.Y;
+ public Point DeltaPosition => new Point(DeltaX, DeltaY);
+
+ public int ScrollWheelValue => _currentMouseState.ScrollWheelValue;
+ public int DeltaScrollWheelValue => _previousMouseState.ScrollWheelValue - _currentMouseState.ScrollWheelValue;
+
+ public ButtonState LeftButton => _currentMouseState.LeftButton;
+ public ButtonState MiddleButton => _currentMouseState.MiddleButton;
+ public ButtonState RightButton => _currentMouseState.RightButton;
+ public ButtonState XButton1 => _currentMouseState.XButton1;
+ public ButtonState XButton2 => _currentMouseState.XButton2;
+
+ public bool IsButtonDown(MouseButton button)
+ {
+ // ReSharper disable once SwitchStatementMissingSomeCases
+ switch (button)
+ {
+ case MouseButton.Left: return IsPressed(m => m.LeftButton);
+ case MouseButton.Middle: return IsPressed(m => m.MiddleButton);
+ case MouseButton.Right: return IsPressed(m => m.RightButton);
+ case MouseButton.XButton1: return IsPressed(m => m.XButton1);
+ case MouseButton.XButton2: return IsPressed(m => m.XButton2);
+ }
+
+ return false;
+ }
+
+ public bool IsButtonUp(MouseButton button)
+ {
+ // ReSharper disable once SwitchStatementMissingSomeCases
+ switch (button)
+ {
+ case MouseButton.Left: return IsReleased(m => m.LeftButton);
+ case MouseButton.Middle: return IsReleased(m => m.MiddleButton);
+ case MouseButton.Right: return IsReleased(m => m.RightButton);
+ case MouseButton.XButton1: return IsReleased(m => m.XButton1);
+ case MouseButton.XButton2: return IsReleased(m => m.XButton2);
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Get the just-down state for the mouse on this state-change: true if the mouse button has just been pressed.
+ /// </summary>
+ /// <param name="button"></param>
+ /// <remarks>Deprecated because of inconsistency with <see cref="KeyboardStateExtended"/></remarks>
+ /// <returns>The just-down state for the mouse on this state-change.</returns>
+ [Obsolete($"Deprecated in favor of {nameof(IsButtonPressed)}")]
+ public bool WasButtonJustDown(MouseButton button)
+ {
+ // ReSharper disable once SwitchStatementMissingSomeCases
+ switch (button)
+ {
+ case MouseButton.Left: return WasJustPressed(m => m.LeftButton);
+ case MouseButton.Middle: return WasJustPressed(m => m.MiddleButton);
+ case MouseButton.Right: return WasJustPressed(m => m.RightButton);
+ case MouseButton.XButton1: return WasJustPressed(m => m.XButton1);
+ case MouseButton.XButton2: return WasJustPressed(m => m.XButton2);
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Get the just-up state for the mouse on this state-change: true if the mouse button has just been released.
+ /// </summary>
+ /// <param name="button"></param>
+ /// <remarks>Deprecated because of inconsistency with <see cref="KeyboardStateExtended"/></remarks>
+ /// <returns>The just-up state for the mouse on this state-change.</returns>
+ [Obsolete($"Deprecated in favor of {nameof(IsButtonReleased)}")]
+ public bool WasButtonJustUp(MouseButton button)
+ {
+ // ReSharper disable once SwitchStatementMissingSomeCases
+ switch (button)
+ {
+ case MouseButton.Left: return WasJustReleased(m => m.LeftButton);
+ case MouseButton.Middle: return WasJustReleased(m => m.MiddleButton);
+ case MouseButton.Right: return WasJustReleased(m => m.RightButton);
+ case MouseButton.XButton1: return WasJustReleased(m => m.XButton1);
+ case MouseButton.XButton2: return WasJustReleased(m => m.XButton2);
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Get the pressed state of a mouse button, for this state-change.
+ /// </summary>
+ /// <param name="button">The button to check.</param>
+ /// <returns>true if the given mouse button was pressed this state-change, otherwise false</returns>
+ public readonly bool IsButtonPressed(MouseButton button) => button switch
+ {
+ MouseButton.Left => WasJustPressed(m => m.LeftButton),
+ MouseButton.Middle => WasJustPressed(m => m.MiddleButton),
+ MouseButton.Right => WasJustPressed(m => m.RightButton),
+ MouseButton.XButton1 => WasJustPressed(m => m.XButton1),
+ MouseButton.XButton2 => WasJustPressed(m => m.XButton2),
+ _ => false,
+ };
+
+ /// <summary>
+ /// Get the released state of a mouse button, for this state-change.
+ /// </summary>
+ /// <param name="button">The button to check.</param>
+ /// <returns>true if the given mouse button was released this state-change, otherwise false</returns>
+ public readonly bool IsButtonReleased(MouseButton button) => button switch
+ {
+ MouseButton.Left => WasJustReleased(m => m.LeftButton),
+ MouseButton.Middle => WasJustReleased(m => m.MiddleButton),
+ MouseButton.Right => WasJustReleased(m => m.RightButton),
+ MouseButton.XButton1 => WasJustReleased(m => m.XButton1),
+ MouseButton.XButton2 => WasJustReleased(m => m.XButton2),
+ _ => false,
+ };
+
+ private readonly bool IsPressed(Func<MouseState, ButtonState> button)
+ => button(_currentMouseState) == ButtonState.Pressed;
+ private readonly bool IsReleased(Func<MouseState, ButtonState> button)
+ => button(_currentMouseState) == ButtonState.Released;
+ private readonly bool WasJustPressed(Func<MouseState, ButtonState> button)
+ => button(_previousMouseState) == ButtonState.Released && button(_currentMouseState) == ButtonState.Pressed;
+ private readonly bool WasJustReleased(Func<MouseState, ButtonState> button)
+ => button(_previousMouseState) == ButtonState.Pressed && button(_currentMouseState) == ButtonState.Released;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/FastRandomExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/FastRandomExtensions.cs
new file mode 100644
index 0000000..66c99e7
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/FastRandomExtensions.cs
@@ -0,0 +1,17 @@
+using MonoGame.Extended;
+
+namespace MonoGame.Extended.Particles
+{
+ public static class FastRandomExtensions
+ {
+ public static void NextColor(this FastRandom random, out HslColor color, Range<HslColor> range)
+ {
+ var maxH = range.Max.H >= range.Min.H
+ ? range.Max.H
+ : range.Max.H + 360;
+ color = new HslColor(random.NextSingle(range.Min.H, maxH),
+ random.NextSingle(range.Min.S, range.Max.S),
+ random.NextSingle(range.Min.L, range.Max.L));
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/LineSegment.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/LineSegment.cs
new file mode 100644
index 0000000..a3c1422
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/LineSegment.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Runtime.InteropServices;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles
+{
+ /// <summary>
+ /// Defines a part of a line that is bounded by two distinct end points.
+ /// </summary>
+ [StructLayout(LayoutKind.Sequential)]
+ public struct LineSegment : IEquatable<LineSegment>
+ {
+ internal readonly Vector2 _point1;
+ internal readonly Vector2 _point2;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LineSegment" /> structure.
+ /// </summary>
+ /// <param name="point1"></param>
+ /// <param name="point2"></param>
+ public LineSegment(Vector2 point1, Vector2 point2)
+ {
+ _point1 = point1;
+ _point2 = point2;
+ }
+
+ public static LineSegment FromPoints(Vector2 p1, Vector2 p2) => new LineSegment(p1, p2);
+ public static LineSegment FromOrigin(Vector2 origin, Vector2 vector) => new LineSegment(origin, origin + vector);
+
+ public Vector2 Origin => _point1;
+
+ public Vector2 Direction
+ {
+ get
+ {
+ var coord = _point2 - _point1;
+ return new Vector2(coord.X, coord.Y);
+ }
+ }
+
+ public LineSegment Translate(Vector2 t)
+ {
+ return new LineSegment(_point1 + t, _point2 + t);
+ }
+
+ public Vector2 ToVector()
+ {
+ var t = _point2 - _point1;
+ return new Vector2(t.X, t.Y);
+ }
+
+ public bool Equals(LineSegment other)
+ {
+ // ReSharper disable ImpureMethodCallOnReadonlyValueField
+ return _point1.Equals(other._point1) && _point2.Equals(other._point2);
+ // ReSharper restore ImpureMethodCallOnReadonlyValueField
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj))
+ return false;
+
+ return obj is LineSegment & Equals((LineSegment) obj);
+ }
+
+ public override int GetHashCode()
+ {
+ return (_point1.GetHashCode()*397) ^ _point2.GetHashCode();
+ }
+
+ public override string ToString()
+ {
+ return $"({_point1.X}:{_point1.Y} {_point2.X}:{_point2.Y})";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/AgeModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/AgeModifier.cs
new file mode 100644
index 0000000..fac85f0
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/AgeModifier.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using MonoGame.Extended.Particles.Modifiers.Interpolators;
+
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public class AgeModifier : Modifier
+ {
+ [EditorBrowsable(EditorBrowsableState.Always)]
+ public List<Interpolator> Interpolators { get; set; } = new List<Interpolator>();
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ var n = Interpolators.Count;
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ for (var i = 0; i < n; i++)
+ {
+ var interpolator = Interpolators[i];
+ interpolator.Update(particle->Age, particle);
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/CircleContainerModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/CircleContainerModifier.cs
new file mode 100644
index 0000000..30d2802
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/CircleContainerModifier.cs
@@ -0,0 +1,52 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers.Containers
+{
+ public class CircleContainerModifier : Modifier
+ {
+ public float Radius { get; set; }
+ public bool Inside { get; set; } = true;
+ public float RestitutionCoefficient { get; set; } = 1;
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ var radiusSq = Radius*Radius;
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ var localPos = particle->Position - particle->TriggerPos;
+
+ var distSq = localPos.LengthSquared();
+ var normal = localPos;
+ normal.Normalize();
+
+ if (Inside)
+ {
+ if (distSq < radiusSq) continue;
+
+ SetReflected(distSq, particle, normal);
+ }
+ else
+ {
+ if (distSq > radiusSq) continue;
+
+ SetReflected(distSq, particle, -normal);
+ }
+ }
+ }
+
+ private unsafe void SetReflected(float distSq, Particle* particle, Vector2 normal)
+ {
+ var dist = (float) Math.Sqrt(distSq);
+ var d = dist - Radius; // how far outside the circle is the particle
+
+ var twoRestDot = 2*RestitutionCoefficient*
+ Vector2.Dot(particle->Velocity, normal);
+ particle->Velocity -= twoRestDot*normal;
+
+ // exact computation requires sqrt or goniometrics
+ particle->Position -= normal*d;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleContainerModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleContainerModifier.cs
new file mode 100644
index 0000000..a56e072
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleContainerModifier.cs
@@ -0,0 +1,59 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers.Containers
+{
+ public sealed class RectangleContainerModifier : Modifier
+ {
+ public int Width { get; set; }
+ public int Height { get; set; }
+ public float RestitutionCoefficient { get; set; } = 1;
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+
+ var left = particle->TriggerPos.X + Width*-0.5f;
+ var right = particle->TriggerPos.X + Width*0.5f;
+ var top = particle->TriggerPos.Y + Height*-0.5f;
+ var bottom = particle->TriggerPos.Y + Height*0.5f;
+
+ var xPos = particle->Position.X;
+ var xVel = particle->Velocity.X;
+ var yPos = particle->Position.Y;
+ var yVel = particle->Velocity.Y;
+
+ if ((int) particle->Position.X < left)
+ {
+ xPos = left + (left - xPos);
+ xVel = -xVel*RestitutionCoefficient;
+ }
+ else
+ {
+ if (particle->Position.X > right)
+ {
+ xPos = right - (xPos - right);
+ xVel = -xVel*RestitutionCoefficient;
+ }
+ }
+
+ if (particle->Position.Y < top)
+ {
+ yPos = top + (top - yPos);
+ yVel = -yVel*RestitutionCoefficient;
+ }
+ else
+ {
+ if ((int) particle->Position.Y > bottom)
+ {
+ yPos = bottom - (yPos - bottom);
+ yVel = -yVel*RestitutionCoefficient;
+ }
+ }
+ particle->Position = new Vector2(xPos, yPos);
+ particle->Velocity = new Vector2(xVel, yVel);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleLoopContainerModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleLoopContainerModifier.cs
new file mode 100644
index 0000000..4da6efa
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleLoopContainerModifier.cs
@@ -0,0 +1,42 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers.Containers
+{
+ public class RectangleLoopContainerModifier : Modifier
+ {
+ public int Width { get; set; }
+ public int Height { get; set; }
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ var left = particle->TriggerPos.X + Width*-0.5f;
+ var right = particle->TriggerPos.X + Width*0.5f;
+ var top = particle->TriggerPos.Y + Height*-0.5f;
+ var bottom = particle->TriggerPos.Y + Height*0.5f;
+
+ var xPos = particle->Position.X;
+ var yPos = particle->Position.Y;
+
+ if ((int) particle->Position.X < left)
+ xPos = particle->Position.X + Width;
+ else
+ {
+ if ((int) particle->Position.X > right)
+ xPos = particle->Position.X - Width;
+ }
+
+ if ((int) particle->Position.Y < top)
+ yPos = particle->Position.Y + Height;
+ else
+ {
+ if ((int) particle->Position.Y > bottom)
+ yPos = particle->Position.Y - Height;
+ }
+ particle->Position = new Vector2(xPos, yPos);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/DragModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/DragModifier.cs
new file mode 100644
index 0000000..f2d6d09
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/DragModifier.cs
@@ -0,0 +1,23 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public class DragModifier : Modifier
+ {
+ public float DragCoefficient { get; set; } = 0.47f;
+ public float Density { get; set; } = .5f;
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ var drag = -DragCoefficient*Density*particle->Mass*elapsedSeconds;
+
+ particle->Velocity = new Vector2(
+ particle->Velocity.X + particle->Velocity.X*drag,
+ particle->Velocity.Y + particle->Velocity.Y*drag);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ColorInterpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ColorInterpolator.cs
new file mode 100644
index 0000000..f94c689
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ColorInterpolator.cs
@@ -0,0 +1,13 @@
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+{
+ /// <summary>
+ /// Defines a modifier which interpolates the color of a particle over the course of its lifetime.
+ /// </summary>
+ public sealed class ColorInterpolator : Interpolator<HslColor>
+ {
+ public override unsafe void Update(float amount, Particle* particle)
+ {
+ particle->Color = HslColor.Lerp(StartValue, EndValue, amount);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/HueInterpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/HueInterpolator.cs
new file mode 100644
index 0000000..8b58a8e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/HueInterpolator.cs
@@ -0,0 +1,12 @@
+using MonoGame.Extended;
+
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+{
+ public class HueInterpolator : Interpolator<float>
+ {
+ public override unsafe void Update(float amount, Particle* particle)
+ {
+ particle->Color = new HslColor((EndValue - StartValue) * amount + StartValue, particle->Color.S, particle->Color.L);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/Interpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/Interpolator.cs
new file mode 100644
index 0000000..0ec9866
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/Interpolator.cs
@@ -0,0 +1,26 @@
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+{
+ public abstract class Interpolator
+ {
+ protected Interpolator()
+ {
+ Name = GetType().Name;
+ }
+
+ public string Name { get; set; }
+ public abstract unsafe void Update(float amount, Particle* particle);
+ }
+
+ public abstract class Interpolator<T> : Interpolator
+ {
+ /// <summary>
+ /// Gets or sets the intial value when the particles are created.
+ /// </summary>
+ public virtual T StartValue { get; set; }
+
+ /// <summary>
+ /// Gets or sets the final value when the particles are retired.
+ /// </summary>
+ public virtual T EndValue { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/OpacityInterpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/OpacityInterpolator.cs
new file mode 100644
index 0000000..0a26fcd
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/OpacityInterpolator.cs
@@ -0,0 +1,10 @@
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+{
+ public class OpacityInterpolator : Interpolator<float>
+ {
+ public override unsafe void Update(float amount, Particle* particle)
+ {
+ particle->Opacity = (EndValue - StartValue) * amount + StartValue;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/RotationInterpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/RotationInterpolator.cs
new file mode 100644
index 0000000..44a72de
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/RotationInterpolator.cs
@@ -0,0 +1,10 @@
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+{
+ public class RotationInterpolator : Interpolator<float>
+ {
+ public override unsafe void Update(float amount, Particle* particle)
+ {
+ particle->Rotation = (EndValue - StartValue) * amount + StartValue;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ScaleInterpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ScaleInterpolator.cs
new file mode 100644
index 0000000..aff9244
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ScaleInterpolator.cs
@@ -0,0 +1,12 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+{
+ public class ScaleInterpolator : Interpolator<Vector2>
+ {
+ public override unsafe void Update(float amount, Particle* particle)
+ {
+ particle->Scale = (EndValue - StartValue) * amount + StartValue;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/VelocityInterpolator b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/VelocityInterpolator
new file mode 100644
index 0000000..bad04b1
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/VelocityInterpolator
@@ -0,0 +1,12 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers.Interpolators
+{
+ public class VelocityInterpolator : Interpolator<Vector2>
+ {
+ public override unsafe void Update(float amount, Particle* particle)
+ {
+ particle->Velocity = (EndValue - StartValue) * amount + StartValue;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/LinearGravityModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/LinearGravityModifier.cs
new file mode 100644
index 0000000..e9ed216
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/LinearGravityModifier.cs
@@ -0,0 +1,23 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public class LinearGravityModifier : Modifier
+ {
+ public Vector2 Direction { get; set; }
+ public float Strength { get; set; }
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ var vector = Direction*(Strength*elapsedSeconds);
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ particle->Velocity = new Vector2(
+ particle->Velocity.X + vector.X*particle->Mass,
+ particle->Velocity.Y + vector.Y*particle->Mass);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Modifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Modifier.cs
new file mode 100644
index 0000000..bd62d25
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Modifier.cs
@@ -0,0 +1,18 @@
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public abstract class Modifier
+ {
+ protected Modifier()
+ {
+ Name = GetType().Name;
+ }
+
+ public string Name { get; set; }
+ public abstract void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator);
+
+ public override string ToString()
+ {
+ return $"{Name} [{GetType().Name}]";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/OpacityFastFadeModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/OpacityFastFadeModifier.cs
new file mode 100644
index 0000000..9cc44d8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/OpacityFastFadeModifier.cs
@@ -0,0 +1,14 @@
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public sealed class OpacityFastFadeModifier : Modifier
+ {
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ particle->Opacity = 1.0f - particle->Age;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/RotationModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/RotationModifier.cs
new file mode 100644
index 0000000..2b17058
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/RotationModifier.cs
@@ -0,0 +1,18 @@
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public class RotationModifier : Modifier
+ {
+ public float RotationRate { get; set; }
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ var rotationRateDelta = RotationRate*elapsedSeconds;
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ particle->Rotation += rotationRateDelta;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityColorModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityColorModifier.cs
new file mode 100644
index 0000000..ae9dc7b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityColorModifier.cs
@@ -0,0 +1,36 @@
+using System;
+
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public class VelocityColorModifier : Modifier
+ {
+ public HslColor StationaryColor { get; set; }
+ public HslColor VelocityColor { get; set; }
+ public float VelocityThreshold { get; set; }
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ var velocityThreshold2 = VelocityThreshold*VelocityThreshold;
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ var velocity2 = particle->Velocity.X*particle->Velocity.X +
+ particle->Velocity.Y*particle->Velocity.Y;
+ var deltaColor = VelocityColor - StationaryColor;
+
+ if (velocity2 >= velocityThreshold2)
+ VelocityColor.CopyTo(out particle->Color);
+ else
+ {
+ var t = (float) Math.Sqrt(velocity2)/VelocityThreshold;
+
+ particle->Color = new HslColor(
+ deltaColor.H*t + StationaryColor.H,
+ deltaColor.S*t + StationaryColor.S,
+ deltaColor.L*t + StationaryColor.L);
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityModifier.cs
new file mode 100644
index 0000000..f089abc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityModifier.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using MonoGame.Extended.Particles.Modifiers.Interpolators;
+
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public class VelocityModifier : Modifier
+ {
+ public List<Interpolator> Interpolators { get; set; } = new List<Interpolator>();
+
+ public float VelocityThreshold { get; set; }
+
+ public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ var velocityThreshold2 = VelocityThreshold*VelocityThreshold;
+ var n = Interpolators.Count;
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ var velocity2 = particle->Velocity.LengthSquared();
+
+ if (velocity2 >= velocityThreshold2)
+ {
+ for (var i = 0; i < n; i++)
+ {
+ var interpolator = Interpolators[i];
+ interpolator.Update(1, particle);
+ }
+ }
+ else
+ {
+ var t = (float) Math.Sqrt(velocity2)/VelocityThreshold;
+ for (var i = 0; i < n; i++)
+ {
+ var interpolator = Interpolators[i];
+ interpolator.Update(t, particle);
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VortexModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VortexModifier.cs
new file mode 100644
index 0000000..03a9999
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VortexModifier.cs
@@ -0,0 +1,30 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Modifiers
+{
+ public unsafe class VortexModifier : Modifier
+ {
+ // Note: not the real-life one
+ private const float _gravConst = 100000f;
+
+ public Vector2 Position { get; set; }
+ public float Mass { get; set; }
+ public float MaxSpeed { get; set; }
+
+ public override void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ var diff = Position + particle->TriggerPos - particle->Position;
+
+ var distance2 = diff.LengthSquared();
+
+ var speedGain = _gravConst*Mass/distance2*elapsedSeconds;
+ // normalize distances and multiply by speedGain
+ diff.Normalize();
+ particle->Velocity += diff*speedGain;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/MonoGame.Extended.Particles.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/MonoGame.Extended.Particles.csproj
new file mode 100644
index 0000000..d82f26e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/MonoGame.Extended.Particles.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>A high performance particle system to make MonoGame more awesome.</Description>
+ <PackageTags>monogame particle system</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Particle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Particle.cs
new file mode 100644
index 0000000..042d8ce
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Particle.cs
@@ -0,0 +1,24 @@
+using System.Runtime.InteropServices;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended;
+
+namespace MonoGame.Extended.Particles
+{
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct Particle
+ {
+ public float Inception;
+ public float Age;
+ public Vector2 Position;
+ public Vector2 TriggerPos;
+ public Vector2 Velocity;
+ public HslColor Color;
+ public float Opacity;
+ public Vector2 Scale;
+ public float Rotation;
+ public float Mass;
+ public float LayerDepth;
+
+ public static readonly int SizeInBytes = Marshal.SizeOf(typeof(Particle));
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleBuffer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleBuffer.cs
new file mode 100644
index 0000000..2e410fa
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleBuffer.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace MonoGame.Extended.Particles
+{
+ public class ParticleBuffer : IDisposable
+ {
+ private readonly ParticleIterator _iterator;
+ private readonly IntPtr _nativePointer;
+
+ // points to the first memory pos after the buffer
+ protected readonly unsafe Particle* BufferEnd;
+
+ private bool _disposed;
+ // points to the particle after the last active particle.
+ protected unsafe Particle* Tail;
+
+ public unsafe ParticleBuffer(int size)
+ {
+ Size = size;
+ _nativePointer = Marshal.AllocHGlobal(SizeInBytes);
+
+ BufferEnd = (Particle*) (_nativePointer + SizeInBytes);
+ Head = (Particle*) _nativePointer;
+ Tail = (Particle*) _nativePointer;
+
+ _iterator = new ParticleIterator(this);
+
+ GC.AddMemoryPressure(SizeInBytes);
+ }
+
+ public int Size { get; }
+
+ public ParticleIterator Iterator => _iterator.Reset();
+ // pointer to the first particle
+ public unsafe Particle* Head { get; private set; }
+
+ // Number of available particle spots in the buffer
+ public int Available => Size - Count;
+ // current number of particles
+ public int Count { get; private set; }
+ // total size of the buffer (add one extra spot in memory for margin between head and tail so the iterator can see that it's at the end)
+ public int SizeInBytes => Particle.SizeInBytes*(Size + 1);
+ // total size of active particles
+ public int ActiveSizeInBytes => Particle.SizeInBytes*Count;
+
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ Marshal.FreeHGlobal(_nativePointer);
+ _disposed = true;
+ GC.RemoveMemoryPressure(SizeInBytes);
+ }
+
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Release the given number of particles or the most available.
+ /// Returns a started iterator to iterate over the new particles.
+ /// </summary>
+ public unsafe ParticleIterator Release(int releaseQuantity)
+ {
+ var numToRelease = Math.Min(releaseQuantity, Available);
+
+ var prevCount = Count;
+ Count += numToRelease;
+
+ Tail += numToRelease;
+ if (Tail >= BufferEnd) Tail -= Size + 1;
+
+ return Iterator.Reset(prevCount);
+ }
+
+ public unsafe void Reclaim(int number)
+ {
+ Count -= number;
+
+ Head += number;
+ if (Head >= BufferEnd)
+ Head -= Size + 1;
+ }
+
+ //public void CopyTo(IntPtr destination)
+ //{
+ // memcpy(destination, _nativePointer, ActiveSizeInBytes);
+ //}
+
+ //public void CopyToReverse(IntPtr destination)
+ //{
+ // var offset = 0;
+ // for (var i = ActiveSizeInBytes - Particle.SizeInBytes; i >= 0; i -= Particle.SizeInBytes)
+ // {
+ // memcpy(IntPtr.Add(destination, offset), IntPtr.Add(_nativePointer, i), Particle.SizeInBytes);
+ // offset += Particle.SizeInBytes;
+ // }
+ //}
+
+ //[DllImport("msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false)]
+ //public static extern void memcpy(IntPtr dest, IntPtr src, int count);
+
+ ~ParticleBuffer()
+ {
+ Dispose();
+ }
+
+ public class ParticleIterator
+ {
+ private readonly ParticleBuffer _buffer;
+
+ private unsafe Particle* _current;
+
+ public int Total;
+
+ public ParticleIterator(ParticleBuffer buffer)
+ {
+ _buffer = buffer;
+ }
+
+ public unsafe bool HasNext => _current != _buffer.Tail;
+
+ public unsafe ParticleIterator Reset()
+ {
+ _current = _buffer.Head;
+ Total = _buffer.Count;
+ return this;
+ }
+
+ internal unsafe ParticleIterator Reset(int offset)
+ {
+ Total = _buffer.Count;
+
+ _current = _buffer.Head + offset;
+ if (_current >= _buffer.BufferEnd)
+ _current -= _buffer.Size + 1;
+
+ return this;
+ }
+
+ public unsafe Particle* Next()
+ {
+ var p = _current;
+ _current++;
+ if (_current == _buffer.BufferEnd)
+ _current = (Particle*) _buffer._nativePointer;
+
+ return p;
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEffect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEffect.cs
new file mode 100644
index 0000000..ce6c782
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEffect.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Particles.Serialization;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Particles
+{
+ public class ParticleEffect : Transform2, IDisposable
+ {
+ public ParticleEffect(string name = null)
+ {
+ Name = name;
+ Emitters = new List<ParticleEmitter>();
+ }
+
+ public void Dispose()
+ {
+ foreach (var emitter in Emitters)
+ emitter.Dispose();
+ }
+
+ public string Name { get; set; }
+ public List<ParticleEmitter> Emitters { get; set; }
+ public int ActiveParticles => Emitters.Sum(t => t.ActiveParticles);
+
+ public void FastForward(Vector2 position, float seconds, float triggerPeriod)
+ {
+ var time = 0f;
+ while (time < seconds)
+ {
+ Update(triggerPeriod);
+ Trigger(position);
+ time += triggerPeriod;
+ }
+ }
+
+ public static ParticleEffect FromFile(ITextureRegionService textureRegionService, string path)
+ {
+ using (var stream = TitleContainer.OpenStream(path))
+ {
+ return FromStream(textureRegionService, stream);
+ }
+ }
+
+ public static ParticleEffect FromStream(ITextureRegionService textureRegionService, Stream stream)
+ {
+ var options = ParticleJsonSerializerOptionsProvider.GetOptions(textureRegionService);
+ return JsonSerializer.Deserialize<ParticleEffect>(stream, options);
+ }
+
+ public void Update(float elapsedSeconds)
+ {
+ for (var i = 0; i < Emitters.Count; i++)
+ Emitters[i].Update(elapsedSeconds, Position);
+ }
+
+ public void Trigger()
+ {
+ Trigger(Position);
+ }
+
+ public void Trigger(Vector2 position, float layerDepth = 0)
+ {
+ for (var i = 0; i < Emitters.Count; i++)
+ Emitters[i].Trigger(position, layerDepth);
+ }
+
+ public void Trigger(LineSegment line, float layerDepth = 0)
+ {
+ for (var i = 0; i < Emitters.Count; i++)
+ Emitters[i].Trigger(line, layerDepth);
+ }
+
+ public override string ToString()
+ {
+ return Name;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEmitter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEmitter.cs
new file mode 100644
index 0000000..1a6273b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEmitter.cs
@@ -0,0 +1,222 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Particles.Modifiers;
+using MonoGame.Extended.Particles.Profiles;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Particles
+{
+ public unsafe class ParticleEmitter : IDisposable
+ {
+ private readonly FastRandom _random = new FastRandom(Math.Abs(Guid.NewGuid().GetHashCode()));
+ private float _totalSeconds;
+
+ [JsonConstructor]
+ public ParticleEmitter(string name, TextureRegion2D textureRegion, int capacity, TimeSpan lifeSpan, Profile profile)
+ {
+ if (profile == null)
+ throw new ArgumentNullException(nameof(profile));
+
+ _lifeSpanSeconds = (float)lifeSpan.TotalSeconds;
+
+ Name = name;
+ TextureRegion = textureRegion;
+ Buffer = new ParticleBuffer(capacity);
+ Offset = Vector2.Zero;
+ Profile = profile;
+ Modifiers = new List<Modifier>();
+ ModifierExecutionStrategy = ParticleModifierExecutionStrategy.Serial;
+ Parameters = new ParticleReleaseParameters();
+ }
+
+ public ParticleEmitter(TextureRegion2D textureRegion, int capacity, TimeSpan lifeSpan, Profile profile)
+ : this(null, textureRegion, capacity, lifeSpan, profile)
+ {
+ }
+
+ public void Dispose()
+ {
+ Buffer.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ ~ParticleEmitter()
+ {
+ Dispose();
+ }
+
+ public string Name { get; set; }
+ public int ActiveParticles => Buffer.Count;
+ public Vector2 Offset { get; set; }
+ public List<Modifier> Modifiers { get; }
+ public Profile Profile { get; set; }
+ public float LayerDepth { get; set; }
+ public ParticleReleaseParameters Parameters { get; set; }
+ public TextureRegion2D TextureRegion { get; set; }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public ParticleModifierExecutionStrategy ModifierExecutionStrategy { get; set; }
+
+ internal ParticleBuffer Buffer;
+
+ public int Capacity
+ {
+ get { return Buffer.Size; }
+ set
+ {
+ var oldBuffer = Buffer;
+ oldBuffer.Dispose();
+ Buffer = new ParticleBuffer(value);
+ }
+ }
+
+ private float _lifeSpanSeconds;
+ public TimeSpan LifeSpan
+ {
+ get { return TimeSpan.FromSeconds(_lifeSpanSeconds); }
+ set { _lifeSpanSeconds = (float) value.TotalSeconds; }
+ }
+
+ private float _nextAutoTrigger;
+
+ private bool _autoTrigger = true;
+ public bool AutoTrigger
+ {
+ get { return _autoTrigger; }
+ set
+ {
+ _autoTrigger = value;
+ _nextAutoTrigger = 0;
+ }
+ }
+
+ private float _autoTriggerFrequency;
+ public float AutoTriggerFrequency
+ {
+ get { return _autoTriggerFrequency; }
+ set
+ {
+ _autoTriggerFrequency = value;
+ _nextAutoTrigger = 0;
+ }
+ }
+
+ private void ReclaimExpiredParticles()
+ {
+ var iterator = Buffer.Iterator;
+ var expired = 0;
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+
+ if (_totalSeconds - particle->Inception < _lifeSpanSeconds)
+ break;
+
+ expired++;
+ }
+
+ if (expired != 0)
+ Buffer.Reclaim(expired);
+ }
+
+ public bool Update(float elapsedSeconds, Vector2 position = default(Vector2))
+ {
+ _totalSeconds += elapsedSeconds;
+
+ if (_autoTrigger)
+ {
+ _nextAutoTrigger -= elapsedSeconds;
+
+ if (_nextAutoTrigger <= 0)
+ {
+ Trigger(position, this.LayerDepth);
+ _nextAutoTrigger = _autoTriggerFrequency;
+ }
+ }
+
+ if (Buffer.Count == 0)
+ return false;
+
+ ReclaimExpiredParticles();
+
+ var iterator = Buffer.Iterator;
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ particle->Age = (_totalSeconds - particle->Inception) / _lifeSpanSeconds;
+ particle->Position = particle->Position + particle->Velocity * elapsedSeconds;
+ }
+
+ ModifierExecutionStrategy.ExecuteModifiers(Modifiers, elapsedSeconds, iterator);
+ return true;
+ }
+
+ public void Trigger(Vector2 position, float layerDepth = 0)
+ {
+ var numToRelease = _random.Next(Parameters.Quantity);
+ Release(position + Offset, numToRelease, layerDepth);
+ }
+
+ public void Trigger(LineSegment line, float layerDepth = 0)
+ {
+ var numToRelease = _random.Next(Parameters.Quantity);
+ var lineVector = line.ToVector();
+
+ for (var i = 0; i < numToRelease; i++)
+ {
+ var offset = lineVector * _random.NextSingle();
+ Release(line.Origin + offset, 1, layerDepth);
+ }
+ }
+
+ private void Release(Vector2 position, int numToRelease, float layerDepth)
+ {
+ var iterator = Buffer.Release(numToRelease);
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+
+ Vector2 heading;
+ Profile.GetOffsetAndHeading(out particle->Position, out heading);
+
+ particle->Age = 0f;
+ particle->Inception = _totalSeconds;
+ particle->Position += position;
+ particle->TriggerPos = position;
+
+ var speed = _random.NextSingle(Parameters.Speed);
+
+ particle->Velocity = heading * speed;
+
+ _random.NextColor(out particle->Color, Parameters.Color);
+
+ particle->Opacity = _random.NextSingle(Parameters.Opacity);
+
+ if(Parameters.MaintainAspectRatioOnScale)
+ {
+ var scale = _random.NextSingle(Parameters.Scale);
+ particle->Scale = new Vector2(scale, scale);
+ }
+ else
+ {
+ particle->Scale = new Vector2(_random.NextSingle(Parameters.ScaleX), _random.NextSingle(Parameters.ScaleY));
+ }
+
+ particle->Rotation = _random.NextSingle(Parameters.Rotation);
+ particle->Mass = _random.NextSingle(Parameters.Mass);
+ particle->LayerDepth = layerDepth;
+ }
+ }
+
+ public override string ToString()
+ {
+ return Name;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleExtensions.cs
new file mode 100644
index 0000000..7566e8d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleExtensions.cs
@@ -0,0 +1,49 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Particles
+{
+ public static class ParticleExtensions
+ {
+ public static void Draw(this SpriteBatch spriteBatch, ParticleEffect effect)
+ {
+ for (var i = 0; i < effect.Emitters.Count; i++)
+ UnsafeDraw(spriteBatch, effect.Emitters[i]);
+ }
+
+ public static void Draw(this SpriteBatch spriteBatch, ParticleEmitter emitter)
+ {
+ UnsafeDraw(spriteBatch, emitter);
+ }
+
+ private static unsafe void UnsafeDraw(SpriteBatch spriteBatch, ParticleEmitter emitter)
+ {
+ if(emitter.TextureRegion == null)
+ return;
+
+ var textureRegion = emitter.TextureRegion;
+ var origin = new Vector2(textureRegion.Width/2f, textureRegion.Height/2f);
+ var iterator = emitter.Buffer.Iterator;
+
+ while (iterator.HasNext)
+ {
+ var particle = iterator.Next();
+ var color = particle->Color.ToRgb();
+
+ if (spriteBatch.GraphicsDevice.BlendState == BlendState.AlphaBlend)
+ color *= particle->Opacity;
+ else
+ color.A = (byte) (particle->Opacity*255);
+
+ var position = new Vector2(particle->Position.X, particle->Position.Y);
+ var scale = particle->Scale;
+ var particleColor = new Color(color, particle->Opacity);
+ var rotation = particle->Rotation;
+ var layerDepth = particle->LayerDepth;
+
+ spriteBatch.Draw(textureRegion, position, particleColor, rotation, origin, scale, SpriteEffects.None, layerDepth);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleModifierExecutionStrategy.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleModifierExecutionStrategy.cs
new file mode 100644
index 0000000..20a4d51
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleModifierExecutionStrategy.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using MonoGame.Extended.Particles.Modifiers;
+
+namespace MonoGame.Extended.Particles
+{
+ using TPL = System.Threading.Tasks;
+
+ public abstract class ParticleModifierExecutionStrategy
+ {
+ public static readonly ParticleModifierExecutionStrategy Serial = new SerialModifierExecutionStrategy();
+ public static readonly ParticleModifierExecutionStrategy Parallel = new ParallelModifierExecutionStrategy();
+
+ internal abstract void ExecuteModifiers(List<Modifier> modifiers, float elapsedSeconds, ParticleBuffer.ParticleIterator iterator);
+
+ internal class SerialModifierExecutionStrategy : ParticleModifierExecutionStrategy
+ {
+ internal override void ExecuteModifiers(List<Modifier> modifiers, float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ for (var i = 0; i < modifiers.Count; i++)
+ modifiers[i].Update(elapsedSeconds, iterator.Reset());
+ }
+
+ public override string ToString()
+ {
+ return nameof(Serial);
+ }
+ }
+
+ internal class ParallelModifierExecutionStrategy : ParticleModifierExecutionStrategy
+ {
+ internal override void ExecuteModifiers(List<Modifier> modifiers, float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+ {
+ TPL.Parallel.ForEach(modifiers, modifier => modifier.Update(elapsedSeconds, iterator.Reset()));
+ }
+
+ public override string ToString()
+ {
+ return nameof(Parallel);
+ }
+ }
+
+ public static ParticleModifierExecutionStrategy Parse(string value)
+ {
+ if (string.Equals(nameof(Parallel), value, StringComparison.OrdinalIgnoreCase))
+ return Parallel;
+
+ if (string.Equals(nameof(Serial), value, StringComparison.OrdinalIgnoreCase))
+ return Serial;
+
+ throw new InvalidOperationException($"Unknown particle modifier execution strategy '{value}'");
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleReleaseParameters.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleReleaseParameters.cs
new file mode 100644
index 0000000..e19fbc2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleReleaseParameters.cs
@@ -0,0 +1,34 @@
+using Microsoft.Xna.Framework;
+using MonoGame.Extended;
+
+namespace MonoGame.Extended.Particles
+{
+ public class ParticleReleaseParameters
+ {
+ public ParticleReleaseParameters()
+ {
+ Quantity = 1;
+ Speed = new Range<float>(-1f, 1f);
+ Color = new Range<HslColor>(Microsoft.Xna.Framework.Color.White.ToHsl(), Microsoft.Xna.Framework.Color.White.ToHsl());
+ Opacity = new Range<float>(0f, 1f);
+ Scale = new Range<float>(1f, 1f);
+ Rotation = new Range<float>(-MathHelper.Pi, MathHelper.Pi);
+ Mass = 1f;
+ MaintainAspectRatioOnScale = true;
+ ScaleX = new Range<float>(1f, 1f);
+ ScaleY = new Range<float>(1f, 1f);
+ }
+
+ public Range<int> Quantity { get; set; }
+ public Range<float> Speed { get; set; }
+ public Range<HslColor> Color { get; set; }
+ public Range<float> Opacity { get; set; }
+ public Range<float> Scale { get; set; }
+ public Range<float> Rotation { get; set; }
+ public Range<float> Mass { get; set; }
+ public bool MaintainAspectRatioOnScale { get; set; }
+ public Range<float> ScaleX { get; set; }
+ public Range<float> ScaleY { get; set; }
+
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxFillProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxFillProfile.cs
new file mode 100644
index 0000000..e363713
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxFillProfile.cs
@@ -0,0 +1,18 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class BoxFillProfile : Profile
+ {
+ public float Width { get; set; }
+ public float Height { get; set; }
+
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ offset = new Vector2(Random.NextSingle(Width*-0.5f, Width*0.5f),
+ Random.NextSingle(Height*-0.5f, Height*0.5f));
+
+ Random.NextUnitVector(out heading);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxProfile.cs
new file mode 100644
index 0000000..7d31984
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxProfile.cs
@@ -0,0 +1,31 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class BoxProfile : Profile
+ {
+ public float Width { get; set; }
+ public float Height { get; set; }
+
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ switch (Random.Next(3))
+ {
+ case 0: // Left
+ offset = new Vector2(Width*-0.5f, Random.NextSingle(Height*-0.5f, Height*0.5f));
+ break;
+ case 1: // Top
+ offset = new Vector2(Random.NextSingle(Width*-0.5f, Width*0.5f), Height*-0.5f);
+ break;
+ case 2: // Right
+ offset = new Vector2(Width*0.5f, Random.NextSingle(Height*-0.5f, Height*0.5f));
+ break;
+ default: // Bottom
+ offset = new Vector2(Random.NextSingle(Width*-0.5f, Width*0.5f), Height*0.5f);
+ break;
+ }
+
+ Random.NextUnitVector(out heading);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxUniformProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxUniformProfile.cs
new file mode 100644
index 0000000..9f766dc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxUniformProfile.cs
@@ -0,0 +1,32 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class BoxUniformProfile : Profile
+ {
+ public float Width { get; set; }
+ public float Height { get; set; }
+
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ var value = Random.Next((int) (2*Width + 2*Height));
+
+ if (value < Width) // Top
+ offset = new Vector2(Random.NextSingle(Width*-0.5f, Width*0.5f), Height*-0.5f);
+ else
+ {
+ if (value < 2*Width) // Bottom
+ offset = new Vector2(Random.NextSingle(Width*-0.5f, Width*0.5f), Height*0.5f);
+ else
+ {
+ if (value < 2*Width + Height) // Left
+ offset = new Vector2(Width*-0.5f, Random.NextSingle(Height*-0.5f, Height*0.5f));
+ else // Right
+ offset = new Vector2(Width*0.5f, Random.NextSingle(Height*-0.5f, Height*0.5f));
+ }
+ }
+
+ Random.NextUnitVector(out heading);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/CircleProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/CircleProfile.cs
new file mode 100644
index 0000000..ef7f11e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/CircleProfile.cs
@@ -0,0 +1,24 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class CircleProfile : Profile
+ {
+ public float Radius { get; set; }
+ public CircleRadiation Radiate { get; set; }
+
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ var dist = Random.NextSingle(0f, Radius);
+
+ Random.NextUnitVector(out heading);
+
+ offset = Radiate == CircleRadiation.In
+ ? new Vector2(-heading.X*dist, -heading.Y*dist)
+ : new Vector2(heading.X*dist, heading.Y*dist);
+
+ if (Radiate == CircleRadiation.None)
+ Random.NextUnitVector(out heading);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/LineProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/LineProfile.cs
new file mode 100644
index 0000000..bea584e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/LineProfile.cs
@@ -0,0 +1,17 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class LineProfile : Profile
+ {
+ public Vector2 Axis { get; set; }
+ public float Length { get; set; }
+
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ var vect = Axis*Random.NextSingle(Length*-0.5f, Length*0.5f);
+ offset = new Vector2(vect.X, vect.Y);
+ Random.NextUnitVector(out heading);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/PointProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/PointProfile.cs
new file mode 100644
index 0000000..04456d3
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/PointProfile.cs
@@ -0,0 +1,14 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class PointProfile : Profile
+ {
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ offset = Vector2.Zero;
+
+ Random.NextUnitVector(out heading);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/Profile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/Profile.cs
new file mode 100644
index 0000000..bfde371
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/Profile.cs
@@ -0,0 +1,68 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public abstract class Profile
+ {
+ public enum CircleRadiation
+ {
+ None,
+ In,
+ Out
+ }
+
+ protected FastRandom Random { get; } = new FastRandom();
+
+ public abstract void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading);
+
+ public object Clone()
+ {
+ return MemberwiseClone();
+ }
+
+ public static Profile Point()
+ {
+ return new PointProfile();
+ }
+
+ public static Profile Line(Vector2 axis, float length)
+ {
+ return new LineProfile {Axis = axis, Length = length};
+ }
+
+ public static Profile Ring(float radius, CircleRadiation radiate)
+ {
+ return new RingProfile {Radius = radius, Radiate = radiate};
+ }
+
+ public static Profile Box(float width, float height)
+ {
+ return new BoxProfile {Width = width, Height = height};
+ }
+
+ public static Profile BoxFill(float width, float height)
+ {
+ return new BoxFillProfile {Width = width, Height = height};
+ }
+
+ public static Profile BoxUniform(float width, float height)
+ {
+ return new BoxUniformProfile {Width = width, Height = height};
+ }
+
+ public static Profile Circle(float radius, CircleRadiation radiate)
+ {
+ return new CircleProfile {Radius = radius, Radiate = radiate};
+ }
+
+ public static Profile Spray(Vector2 direction, float spread)
+ {
+ return new SprayProfile {Direction = direction, Spread = spread};
+ }
+
+ public override string ToString()
+ {
+ return GetType().ToString();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/RingProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/RingProfile.cs
new file mode 100644
index 0000000..d5ac2f8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/RingProfile.cs
@@ -0,0 +1,32 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class RingProfile : Profile
+ {
+ public float Radius { get; set; }
+ public CircleRadiation Radiate { get; set; }
+
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ Random.NextUnitVector(out heading);
+
+ switch (Radiate)
+ {
+ case CircleRadiation.In:
+ offset = new Vector2(-heading.X*Radius, -heading.Y*Radius);
+ break;
+ case CircleRadiation.Out:
+ offset = new Vector2(heading.X*Radius, heading.Y*Radius);
+ break;
+ case CircleRadiation.None:
+ offset = new Vector2(heading.X*Radius, heading.Y*Radius);
+ Random.NextUnitVector(out heading);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException($"{Radiate} is not supported");
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/SprayProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/SprayProfile.cs
new file mode 100644
index 0000000..6259210
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/SprayProfile.cs
@@ -0,0 +1,20 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Particles.Profiles
+{
+ public class SprayProfile : Profile
+ {
+ public Vector2 Direction { get; set; }
+ public float Spread { get; set; }
+
+ public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading)
+ {
+ var angle = (float) Math.Atan2(Direction.Y, Direction.X);
+
+ angle = Random.NextSingle(angle - Spread/2f, angle + Spread/2f);
+ offset = Vector2.Zero;
+ heading = new Vector2((float) Math.Cos(angle), (float) Math.Sin(angle));
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/InterpolatorJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/InterpolatorJsonConverter.cs
new file mode 100644
index 0000000..21bd7fc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/InterpolatorJsonConverter.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using MonoGame.Extended.Particles.Modifiers.Interpolators;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Particles.Serialization
+{
+ public class InterpolatorJsonConverter : BaseTypeJsonConverter<Interpolator>
+ {
+ public InterpolatorJsonConverter()
+ : base(GetSupportedTypes(), "Interpolator")
+ {
+ }
+
+ private static IEnumerable<TypeInfo> GetSupportedTypes()
+ {
+ return typeof(Interpolator)
+ .GetTypeInfo()
+ .Assembly
+ .DefinedTypes
+ .Where(type => typeof(Interpolator).GetTypeInfo().IsAssignableFrom(type) && !type.IsAbstract);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierExecutionStrategyJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierExecutionStrategyJsonConverter.cs
new file mode 100644
index 0000000..8fc894b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierExecutionStrategyJsonConverter.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Particles.Serialization;
+
+/// <summary>
+/// Converts a <see cref="ParticleModifierExecutionStrategy"/> value to or from JSON.
+/// </summary>
+public class ModifierExecutionStrategyJsonConverter : JsonConverter<ParticleModifierExecutionStrategy>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) =>
+ typeToConvert == typeof(ParticleModifierExecutionStrategy) ||
+ typeToConvert.GetTypeInfo().BaseType == typeof(ParticleModifierExecutionStrategy);
+
+ /// <inheritdoc />
+ public override ParticleModifierExecutionStrategy Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var value = JsonSerializer.Deserialize<string>(ref reader, options);
+ return ParticleModifierExecutionStrategy.Parse(value);
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, ParticleModifierExecutionStrategy value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteStringValue(value.ToString());
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierJsonConverter.cs
new file mode 100644
index 0000000..5985e40
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierJsonConverter.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using MonoGame.Extended.Particles.Modifiers;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Particles.Serialization
+{
+ public class ModifierJsonConverter : BaseTypeJsonConverter<Modifier>
+ {
+ public ModifierJsonConverter()
+ : base(GetSupportedTypes(), "Modifier")
+ {
+ }
+
+ private static IEnumerable<TypeInfo> GetSupportedTypes()
+ {
+ return typeof(Modifier)
+ .GetTypeInfo()
+ .Assembly
+ .DefinedTypes
+ .Where(type => typeof(Modifier).GetTypeInfo().IsAssignableFrom(type) && !type.IsAbstract);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ParticleJsonSerializerOptionsProvider.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ParticleJsonSerializerOptionsProvider.cs
new file mode 100644
index 0000000..489b3ee
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ParticleJsonSerializerOptionsProvider.cs
@@ -0,0 +1,34 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Particles.Serialization;
+
+public static class ParticleJsonSerializerOptionsProvider
+{
+ public static JsonSerializerOptions GetOptions(ITextureRegionService textureRegionService)
+ {
+ var options = new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ options.Converters.Add(new Vector2JsonConverter());
+ options.Converters.Add(new Size2JsonConverter());
+ options.Converters.Add(new ColorJsonConverter());
+ options.Converters.Add(new TextureRegion2DJsonConverter(textureRegionService));
+ options.Converters.Add(new ProfileJsonConverter());
+ options.Converters.Add(new ModifierJsonConverter());
+ options.Converters.Add(new InterpolatorJsonConverter());
+ options.Converters.Add(new TimeSpanJsonConverter());
+ options.Converters.Add(new RangeJsonConverter<int>());
+ options.Converters.Add(new RangeJsonConverter<float>());
+ options.Converters.Add(new RangeJsonConverter<HslColor>());
+ options.Converters.Add(new HslColorJsonConverter());
+ options.Converters.Add(new ModifierExecutionStrategyJsonConverter());
+
+ return options;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ProfileJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ProfileJsonConverter.cs
new file mode 100644
index 0000000..8d98079
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ProfileJsonConverter.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using MonoGame.Extended.Particles.Profiles;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Particles.Serialization
+{
+ public class ProfileJsonConverter : BaseTypeJsonConverter<Profile>
+ {
+ public ProfileJsonConverter()
+ : base(GetSupportedTypes(), nameof(Profile))
+ {
+ }
+
+ private static IEnumerable<TypeInfo> GetSupportedTypes()
+ {
+ return typeof(Profile)
+ .GetTypeInfo()
+ .Assembly
+ .DefinedTypes
+ .Where(type => type.IsSubclassOf(typeof(Profile)) && !type.IsAbstract);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/TimeSpanJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/TimeSpanJsonConverter.cs
new file mode 100644
index 0000000..0155b08
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/TimeSpanJsonConverter.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Particles.Serialization;
+
+public class TimeSpanJsonConverter : JsonConverter<TimeSpan>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(TimeSpan);
+
+ /// <inheritdoc />
+ public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.Number)
+ {
+ double seconds = reader.GetDouble();
+ return TimeSpan.FromSeconds(seconds);
+ }
+
+ return TimeSpan.Zero;
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteNumberValue(value.TotalSeconds);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/ContentReaderExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/ContentReaderExtensions.cs
new file mode 100644
index 0000000..86f1b6b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/ContentReaderExtensions.cs
@@ -0,0 +1,20 @@
+using Microsoft.Xna.Framework.Content;
+
+namespace MonoGame.Extended.Tiled
+{
+ public static class ContentReaderExtensions
+ {
+ public static void ReadTiledMapProperties(this ContentReader reader, TiledMapProperties properties)
+ {
+ var count = reader.ReadInt32();
+
+ for (var i = 0; i < count; i++)
+ {
+ var key = reader.ReadString();
+ var value = new TiledMapPropertyValue(reader.ReadString());
+ ReadTiledMapProperties(reader, value.Properties);
+ properties[key] = value;
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/MonoGame.Extended.Tiled.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/MonoGame.Extended.Tiled.csproj
new file mode 100644
index 0000000..a52cc65
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/MonoGame.Extended.Tiled.csproj
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>Support for Tiled maps to make MonoGame more awesome. See http://www.mapeditor.org</Description>
+ <PackageTags>monogame tiled maps orthographic isometric</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended.Graphics\MonoGame.Extended.Graphics.csproj" />
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModel.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModel.cs
new file mode 100644
index 0000000..791819a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModel.cs
@@ -0,0 +1,32 @@
+using Microsoft.Xna.Framework.Graphics;
+using System;
+
+namespace MonoGame.Extended.Tiled.Renderers
+{
+ public sealed class TiledMapAnimatedLayerModel : TiledMapLayerModel
+ {
+ public TiledMapAnimatedLayerModel(GraphicsDevice graphicsDevice, Texture2D texture, VertexPositionTexture[] vertices, ushort[] indices, TiledMapTilesetAnimatedTile[] animatedTilesetTiles, TiledMapTileFlipFlags[] animatedTilesetTileFlipFlags)
+ : base(graphicsDevice, texture, vertices, indices)
+ {
+ Vertices = vertices;
+ AnimatedTilesetTiles = animatedTilesetTiles;
+ _animatedTilesetFlipFlags = animatedTilesetTileFlipFlags;
+ }
+
+ public VertexPositionTexture[] Vertices { get; }
+ public TiledMapTilesetAnimatedTile[] AnimatedTilesetTiles { get; }
+ private readonly TiledMapTileFlipFlags[] _animatedTilesetFlipFlags;
+
+ public ReadOnlySpan<TiledMapTileFlipFlags> AnimatedTilesetFlipFlags => _animatedTilesetFlipFlags;
+
+ protected override VertexBuffer CreateVertexBuffer(GraphicsDevice graphicsDevice, int vertexCount)
+ {
+ return new DynamicVertexBuffer(graphicsDevice, VertexPositionTexture.VertexDeclaration, vertexCount, BufferUsage.WriteOnly);
+ }
+
+ protected override IndexBuffer CreateIndexBuffer(GraphicsDevice graphicsDevice, int indexCount)
+ {
+ return new DynamicIndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, indexCount, BufferUsage.WriteOnly); ;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModelBuilder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModelBuilder.cs
new file mode 100644
index 0000000..7afcc21
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModelBuilder.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled.Renderers
+{
+ public class TiledMapAnimatedLayerModelBuilder : TiledMapLayerModelBuilder<TiledMapAnimatedLayerModel>
+ {
+ public TiledMapAnimatedLayerModelBuilder()
+ {
+ AnimatedTilesetTiles = new List<TiledMapTilesetAnimatedTile>();
+ AnimatedTilesetFlipFlags = new List<TiledMapTileFlipFlags>();
+ }
+
+ public List<TiledMapTilesetAnimatedTile> AnimatedTilesetTiles { get; }
+ public List<TiledMapTileFlipFlags> AnimatedTilesetFlipFlags { get; }
+
+ protected override void ClearBuffers()
+ {
+ AnimatedTilesetTiles.Clear();
+ AnimatedTilesetFlipFlags.Clear();
+ }
+
+ protected override TiledMapAnimatedLayerModel CreateModel(GraphicsDevice graphicsDevice, Texture2D texture)
+ {
+ return new TiledMapAnimatedLayerModel(graphicsDevice, texture, Vertices.ToArray(), Indices.ToArray(), AnimatedTilesetTiles.ToArray(), AnimatedTilesetFlipFlags.ToArray());
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapEffect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapEffect.cs
new file mode 100644
index 0000000..b24e2e9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapEffect.cs
@@ -0,0 +1,37 @@
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.Graphics.Effects;
+
+namespace MonoGame.Extended.Tiled.Renderers
+{
+ public interface ITiledMapEffect : IEffectMatrices, ITextureEffect
+ {
+ float Alpha { get; set; }
+ }
+
+ public class TiledMapEffect : DefaultEffect, ITiledMapEffect
+ {
+ public TiledMapEffect(GraphicsDevice graphicsDevice)
+ : base(graphicsDevice)
+ {
+ Initialize();
+ }
+
+ public TiledMapEffect(GraphicsDevice graphicsDevice, byte[] byteCode)
+ : base(graphicsDevice, byteCode)
+ {
+ Initialize();
+ }
+
+ public TiledMapEffect(Effect cloneSource)
+ : base(cloneSource)
+ {
+ Initialize();
+ }
+
+ private void Initialize()
+ {
+ VertexColorEnabled = false;
+ TextureEnabled = true;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModel.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModel.cs
new file mode 100644
index 0000000..f837e1a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModel.cs
@@ -0,0 +1,38 @@
+using System;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled.Renderers
+{
+ public abstract class TiledMapLayerModel : IDisposable
+ {
+ protected TiledMapLayerModel(GraphicsDevice graphicsDevice, Texture2D texture, VertexPositionTexture[] vertices, ushort[] indices)
+ {
+ Texture = texture;
+
+ // ReSharper disable once VirtualMemberCallInConstructor
+ VertexBuffer = CreateVertexBuffer(graphicsDevice, vertices.Length);
+ VertexBuffer.SetData(vertices, 0, vertices.Length);
+
+ // ReSharper disable once VirtualMemberCallInConstructor
+ IndexBuffer = CreateIndexBuffer(graphicsDevice, indices.Length);
+ IndexBuffer.SetData(indices, 0, indices.Length);
+
+ TriangleCount = indices.Length / 3;
+ }
+
+ public void Dispose()
+ {
+ IndexBuffer.Dispose();
+ VertexBuffer.Dispose();
+ }
+
+ public Texture2D Texture { get; }
+ public VertexBuffer VertexBuffer { get; }
+ public IndexBuffer IndexBuffer { get; }
+ public int TriangleCount { get; }
+
+ protected abstract VertexBuffer CreateVertexBuffer(GraphicsDevice graphicsDevice, int vertexCount);
+ protected abstract IndexBuffer CreateIndexBuffer(GraphicsDevice graphicsDevice, int indexCount);
+
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModelBuilder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModelBuilder.cs
new file mode 100644
index 0000000..fbc99d6
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModelBuilder.cs
@@ -0,0 +1,125 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled.Renderers
+{
+ public abstract class TiledMapLayerModelBuilder<T>
+ {
+ protected TiledMapLayerModelBuilder()
+ {
+ Indices = new List<ushort>();
+ Vertices = new List<VertexPositionTexture>();
+ }
+
+ public List<ushort> Indices { get; }
+ public List<VertexPositionTexture> Vertices { get; }
+ public bool IsFull => Vertices.Count + TiledMapHelper.VerticesPerTile >= TiledMapHelper.MaximumVerticesPerModel;
+ public bool IsBuildable => Vertices.Any();
+
+ protected abstract void ClearBuffers();
+ protected abstract T CreateModel(GraphicsDevice graphicsDevice, Texture2D texture);
+
+ public T Build(GraphicsDevice graphicsDevice, Texture2D texture)
+ {
+ var model = CreateModel(graphicsDevice, texture);
+ Vertices.Clear();
+ Indices.Clear();
+ ClearBuffers();
+ return model;
+ }
+
+ public void AddSprite(Texture2D texture, Point2 position, Rectangle sourceRectangle, TiledMapTileFlipFlags flipFlags)
+ {
+ Indices.AddRange(CreateTileIndices(Vertices.Count));
+ Debug.Assert(Indices.Count <= TiledMapHelper.MaximumIndicesPerModel);
+
+ Vertices.AddRange(CreateVertices(texture, position, sourceRectangle, flipFlags));
+ Debug.Assert(Vertices.Count <= TiledMapHelper.MaximumVerticesPerModel);
+ }
+
+ private static IEnumerable<VertexPositionTexture> CreateVertices(Texture2D texture, Vector2 position, Rectangle sourceRectangle, TiledMapTileFlipFlags flags = TiledMapTileFlipFlags.None)
+ {
+ var reciprocalWidth = 1f / texture.Width;
+ var reciprocalHeight = 1f / texture.Height;
+ var texelLeft = sourceRectangle.X * reciprocalWidth;
+ var texelTop = sourceRectangle.Y * reciprocalHeight;
+ var texelRight = (sourceRectangle.X + sourceRectangle.Width) * reciprocalWidth;
+ var texelBottom = (sourceRectangle.Y + sourceRectangle.Height) * reciprocalHeight;
+
+ VertexPositionTexture vertexTopLeft, vertexTopRight, vertexBottomLeft, vertexBottomRight;
+
+ vertexTopLeft.Position = new Vector3(position, 0);
+ vertexTopRight.Position = new Vector3(position + new Vector2(sourceRectangle.Width, 0), 0);
+ vertexBottomLeft.Position = new Vector3(position + new Vector2(0, sourceRectangle.Height), 0);
+ vertexBottomRight.Position = new Vector3(position + new Vector2(sourceRectangle.Width, sourceRectangle.Height), 0);
+
+ vertexTopLeft.TextureCoordinate.Y = texelTop;
+ vertexTopLeft.TextureCoordinate.X = texelLeft;
+
+ vertexTopRight.TextureCoordinate.Y = texelTop;
+ vertexTopRight.TextureCoordinate.X = texelRight;
+
+ vertexBottomLeft.TextureCoordinate.Y = texelBottom;
+ vertexBottomLeft.TextureCoordinate.X = texelLeft;
+
+ vertexBottomRight.TextureCoordinate.Y = texelBottom;
+ vertexBottomRight.TextureCoordinate.X = texelRight;
+
+ var flipDiagonally = (flags & TiledMapTileFlipFlags.FlipDiagonally) != 0;
+ var flipHorizontally = (flags & TiledMapTileFlipFlags.FlipHorizontally) != 0;
+ var flipVertically = (flags & TiledMapTileFlipFlags.FlipVertically) != 0;
+
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.X, ref vertexBottomLeft.TextureCoordinate.X);
+ FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.Y, ref vertexBottomLeft.TextureCoordinate.Y);
+ }
+
+ if (flipHorizontally)
+ {
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.Y, ref vertexTopRight.TextureCoordinate.Y);
+ FloatHelper.Swap(ref vertexBottomLeft.TextureCoordinate.Y, ref vertexBottomRight.TextureCoordinate.Y);
+ }
+ else
+ {
+ FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.X, ref vertexTopRight.TextureCoordinate.X);
+ FloatHelper.Swap(ref vertexBottomLeft.TextureCoordinate.X, ref vertexBottomRight.TextureCoordinate.X);
+ }
+ }
+
+ if (flipVertically)
+ {
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.X, ref vertexBottomLeft.TextureCoordinate.X);
+ FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.X, ref vertexBottomRight.TextureCoordinate.X);
+ }
+ else
+ {
+ FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.Y, ref vertexBottomLeft.TextureCoordinate.Y);
+ FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.Y, ref vertexBottomRight.TextureCoordinate.Y);
+ }
+ }
+
+ yield return vertexTopLeft;
+ yield return vertexTopRight;
+ yield return vertexBottomLeft;
+ yield return vertexBottomRight;
+ }
+
+ private static IEnumerable<ushort> CreateTileIndices(int indexOffset)
+ {
+ yield return (ushort)(0 + indexOffset);
+ yield return (ushort)(1 + indexOffset);
+ yield return (ushort)(2 + indexOffset);
+ yield return (ushort)(1 + indexOffset);
+ yield return (ushort)(3 + indexOffset);
+ yield return (ushort)(2 + indexOffset);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModel.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModel.cs
new file mode 100644
index 0000000..52e56bc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModel.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+
+namespace MonoGame.Extended.Tiled.Renderers
+{
+ public class TiledMapModel : IDisposable
+ {
+ private readonly TiledMap _map;
+ private readonly Dictionary<TiledMapTileset, List<TiledMapTilesetAnimatedTile>> _animatedTilesByTileset;
+
+ public TiledMapModel(TiledMap map, Dictionary<TiledMapLayer, TiledMapLayerModel[]> layersOfLayerModels)
+ {
+ _map = map;
+ LayersOfLayerModels = layersOfLayerModels;
+ _animatedTilesByTileset = _map.Tilesets
+ .ToDictionary(i => i, i => i.Tiles.OfType<TiledMapTilesetAnimatedTile>()
+ .ToList());
+ }
+
+ public void Dispose()
+ {
+ foreach (var layerModel in LayersOfLayerModels)
+ foreach (var model in layerModel.Value)
+ model.Dispose();
+ }
+
+ public ReadOnlyCollection<TiledMapTileset> Tilesets => _map.Tilesets;
+ public ReadOnlyCollection<TiledMapLayer> Layers => _map.Layers;
+
+ // each layer has many models
+ public Dictionary<TiledMapLayer, TiledMapLayerModel[]> LayersOfLayerModels { get; }
+
+ public IEnumerable<TiledMapTilesetAnimatedTile> GetAnimatedTiles(int tilesetIndex)
+ {
+ var tileset = _map.Tilesets[tilesetIndex];
+ return _animatedTilesByTileset[tileset];
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModelBuilder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModelBuilder.cs
new file mode 100644
index 0000000..b96925e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModelBuilder.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled.Renderers
+{
+ public class TiledMapModelBuilder
+ {
+ private readonly GraphicsDevice _graphicsDevice;
+
+ public TiledMapModelBuilder(GraphicsDevice graphicsDevice)
+ {
+ _graphicsDevice = graphicsDevice;
+ }
+
+ private IEnumerable<TiledMapLayerModel> CreateLayerModels(TiledMap map, TiledMapLayer layer)
+ {
+ switch(layer)
+ {
+ case TiledMapTileLayer tileLayer:
+ return CreateTileLayerModels(map, tileLayer);
+ case TiledMapImageLayer imageLayer:
+ return CreateImageLayerModels(imageLayer);
+ default:
+ return new List<TiledMapLayerModel>();
+ }
+
+ }
+
+ private IEnumerable<TiledMapLayerModel> CreateImageLayerModels(TiledMapImageLayer imageLayer)
+ {
+ var modelBuilder = new TiledMapStaticLayerModelBuilder();
+ modelBuilder.AddSprite(imageLayer.Image, imageLayer.Position, imageLayer.Image.Bounds, TiledMapTileFlipFlags.None);
+ yield return modelBuilder.Build(_graphicsDevice, imageLayer.Image);
+ }
+
+ private IEnumerable<TiledMapLayerModel> CreateTileLayerModels(TiledMap map, TiledMapTileLayer tileLayer)
+ {
+ var layerModels = new List<TiledMapLayerModel>();
+ var staticLayerBuilder = new TiledMapStaticLayerModelBuilder();
+ var animatedLayerBuilder = new TiledMapAnimatedLayerModelBuilder();
+
+ foreach (var tileset in map.Tilesets)
+ {
+ var firstGlobalIdentifier = map.GetTilesetFirstGlobalIdentifier(tileset);
+ var lastGlobalIdentifier = tileset.TileCount + firstGlobalIdentifier - 1;
+ var texture = tileset.Texture;
+
+ foreach (var tile in tileLayer.Tiles.Where(t => firstGlobalIdentifier <= t.GlobalIdentifier && t.GlobalIdentifier <= lastGlobalIdentifier))
+ {
+ var tileGid = tile.GlobalIdentifier;
+ var localTileIdentifier = tileGid - firstGlobalIdentifier;
+ var position = GetTilePosition(map, tile);
+ var sourceRectangle = tileset.GetTileRegion(localTileIdentifier);
+ var flipFlags = tile.Flags;
+
+ // animated tiles
+ var tilesetTile = tileset.Tiles.FirstOrDefault(x => x.LocalTileIdentifier == localTileIdentifier);
+ if (tilesetTile?.Texture is not null)
+ {
+ position.Y += map.TileHeight - sourceRectangle.Height;
+ texture = tilesetTile.Texture;
+ }
+
+ if (tilesetTile is TiledMapTilesetAnimatedTile animatedTilesetTile)
+ {
+ animatedLayerBuilder.AddSprite(texture, position, sourceRectangle, flipFlags);
+ animatedTilesetTile.CreateTextureRotations(tileset, flipFlags);
+ animatedLayerBuilder.AnimatedTilesetTiles.Add(animatedTilesetTile);
+ animatedLayerBuilder.AnimatedTilesetFlipFlags.Add(flipFlags);
+
+ if (animatedLayerBuilder.IsFull)
+ layerModels.Add(animatedLayerBuilder.Build(_graphicsDevice, texture));
+ }
+ else
+ {
+ staticLayerBuilder.AddSprite(texture, position, sourceRectangle, flipFlags);
+
+ if (staticLayerBuilder.IsFull)
+ layerModels.Add(staticLayerBuilder.Build(_graphicsDevice, texture));
+ }
+ }
+
+ if (staticLayerBuilder.IsBuildable)
+ layerModels.Add(staticLayerBuilder.Build(_graphicsDevice, texture));
+
+ if (animatedLayerBuilder.IsBuildable)
+ layerModels.Add(animatedLayerBuilder.Build(_graphicsDevice, texture));
+ }
+
+ return layerModels;
+ }
+
+ public TiledMapModel Build(TiledMap map)
+ {
+ var dictionary = new Dictionary<TiledMapLayer, TiledMapLayerModel[]>();
+ foreach (var layer in map.Layers)
+ BuildLayer(map, layer, dictionary);
+
+ return new TiledMapModel(map, dictionary);
+ }
+
+ private void BuildLayer(TiledMap map, TiledMapLayer layer, Dictionary<TiledMapLayer, TiledMapLayerModel[]> dictionary)
+ {
+ if (layer is TiledMapGroupLayer groupLayer)
+ foreach (var subLayer in groupLayer.Layers)
+ BuildLayer(map, subLayer, dictionary);
+ else
+ dictionary.Add(layer, CreateLayerModels(map, layer).ToArray());
+ }
+
+ private static Point2 GetTilePosition(TiledMap map, TiledMapTile mapTile)
+ {
+ switch (map.Orientation)
+ {
+ case TiledMapOrientation.Orthogonal:
+ return TiledMapHelper.GetOrthogonalPosition(mapTile.X, mapTile.Y, map.TileWidth, map.TileHeight);
+ case TiledMapOrientation.Isometric:
+ return TiledMapHelper.GetIsometricPosition(mapTile.X, mapTile.Y, map.TileWidth, map.TileHeight);
+ default:
+ throw new NotSupportedException($"{map.Orientation} Tiled Maps are not yet implemented.");
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapRenderer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapRenderer.cs
new file mode 100644
index 0000000..66babad
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapRenderer.cs
@@ -0,0 +1,191 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled.Renderers
+{
+ public class TiledMapRenderer : IDisposable
+ {
+ private readonly TiledMapModelBuilder _mapModelBuilder;
+ private readonly TiledMapEffect _defaultEffect;
+ private readonly GraphicsDevice _graphicsDevice;
+ private TiledMapModel _mapModel;
+ private Matrix _worldMatrix = Matrix.Identity;
+
+ public TiledMapRenderer(GraphicsDevice graphicsDevice, TiledMap map = null)
+ {
+ if (graphicsDevice == null) throw new ArgumentNullException(nameof(graphicsDevice));
+
+ _graphicsDevice = graphicsDevice;
+ _defaultEffect = new TiledMapEffect(graphicsDevice);
+ _mapModelBuilder = new TiledMapModelBuilder(graphicsDevice);
+
+ if(map != null)
+ LoadMap(map);
+ }
+
+ public void Dispose()
+ {
+ _mapModel?.Dispose();
+ _defaultEffect.Dispose();
+ }
+
+ public void LoadMap(TiledMap map)
+ {
+ _mapModel?.Dispose();
+ _mapModel = map != null ? _mapModelBuilder.Build(map) : null;
+ }
+
+ public void Update(GameTime gameTime)
+ {
+ if(_mapModel == null)
+ return;
+
+ for (var tilesetIndex = 0; tilesetIndex < _mapModel.Tilesets.Count; tilesetIndex++)
+ {
+ foreach (var animatedTilesetTile in _mapModel.GetAnimatedTiles(tilesetIndex))
+ animatedTilesetTile.Update(gameTime);
+ }
+
+ foreach(var layer in _mapModel.LayersOfLayerModels)
+ UpdateAnimatedLayerModels(layer.Value.OfType<TiledMapAnimatedLayerModel>());
+ }
+
+ private static unsafe void UpdateAnimatedLayerModels(IEnumerable<TiledMapAnimatedLayerModel> animatedLayerModels)
+ {
+ foreach (var animatedModel in animatedLayerModels)
+ {
+ // update the texture coordinates for each animated tile
+ fixed (VertexPositionTexture* fixedVerticesPointer = animatedModel.Vertices)
+ {
+ var verticesPointer = fixedVerticesPointer;
+ for (int i = 0; i < animatedModel.AnimatedTilesetTiles.Length; i++)
+ {
+ var currentFrameTextureCoordinates = animatedModel.AnimatedTilesetTiles[i].CurrentAnimationFrame.GetTextureCoordinates(animatedModel.AnimatedTilesetFlipFlags[i]);
+
+ // ReSharper disable ArrangeRedundantParentheses
+ (*verticesPointer++).TextureCoordinate = currentFrameTextureCoordinates[0];
+ (*verticesPointer++).TextureCoordinate = currentFrameTextureCoordinates[1];
+ (*verticesPointer++).TextureCoordinate = currentFrameTextureCoordinates[2];
+ (*verticesPointer++).TextureCoordinate = currentFrameTextureCoordinates[3];
+ // ReSharper restore ArrangeRedundantParentheses
+ }
+ }
+
+ // copy (upload) the updated vertices to the GPU's memory
+ animatedModel.VertexBuffer.SetData(animatedModel.Vertices, 0, animatedModel.Vertices.Length);
+ }
+ }
+
+ public void Draw(Matrix? viewMatrix = null, Matrix? projectionMatrix = null, Effect effect = null, float depth = 0.0f)
+ {
+ var viewMatrix1 = viewMatrix ?? Matrix.Identity;
+ var projectionMatrix1 = projectionMatrix ?? Matrix.CreateOrthographicOffCenter(0, _graphicsDevice.Viewport.Width, _graphicsDevice.Viewport.Height, 0, 0, -1);
+
+ Draw(ref viewMatrix1, ref projectionMatrix1, effect, depth);
+ }
+
+ public void Draw(ref Matrix viewMatrix, ref Matrix projectionMatrix, Effect effect = null, float depth = 0.0f)
+ {
+ if (_mapModel == null)
+ return;
+
+ for (var index = 0; index < _mapModel.Layers.Count; index++)
+ Draw(index, ref viewMatrix, ref projectionMatrix, effect, depth);
+ }
+
+ public void Draw(TiledMapLayer layer, Matrix? viewMatrix = null, Matrix? projectionMatrix = null, Effect effect = null, float depth = 0.0f)
+ {
+ var viewMatrix1 = viewMatrix ?? Matrix.Identity;
+ var projectionMatrix1 = projectionMatrix ?? Matrix.CreateOrthographicOffCenter(0, _graphicsDevice.Viewport.Width, _graphicsDevice.Viewport.Height, 0, 0, -1);
+
+ Draw(layer, ref viewMatrix1, ref projectionMatrix1, effect, depth);
+ }
+
+ public void Draw(int layerIndex, Matrix? viewMatrix = null, Matrix? projectionMatrix = null, Effect effect = null, float depth = 0.0f)
+ {
+ var viewMatrix1 = viewMatrix ?? Matrix.Identity;
+ var projectionMatrix1 = projectionMatrix ?? Matrix.CreateOrthographicOffCenter(0, _graphicsDevice.Viewport.Width, _graphicsDevice.Viewport.Height, 0, 0, -1);
+
+ Draw(layerIndex, ref viewMatrix1, ref projectionMatrix1, effect, depth);
+ }
+
+ public void Draw(int layerIndex, ref Matrix viewMatrix, ref Matrix projectionMatrix, Effect effect = null, float depth = 0.0f)
+ {
+ var layer = _mapModel.Layers[layerIndex];
+
+ Draw(layer, ref viewMatrix, ref projectionMatrix, effect, depth);
+ }
+
+ public void Draw(TiledMapLayer layer, ref Matrix viewMatrix, ref Matrix projectionMatrix, Effect effect = null, float depth = 0.0f)
+ {
+ if (_mapModel == null)
+ return;
+
+ if (!layer.IsVisible)
+ return;
+
+ if (layer is TiledMapObjectLayer)
+ return;
+
+ Draw(layer, Vector2.Zero, Vector2.One, ref viewMatrix, ref projectionMatrix, effect, depth);
+ }
+
+ private void Draw(TiledMapLayer layer, Vector2 parentOffset, Vector2 parentParallaxFactor, ref Matrix viewMatrix, ref Matrix projectionMatrix, Effect effect, float depth)
+ {
+ var offset = parentOffset + layer.Offset;
+ var parallaxFactor = parentParallaxFactor * layer.ParallaxFactor;
+
+ if (layer is TiledMapGroupLayer groupLayer)
+ {
+ foreach (var subLayer in groupLayer.Layers)
+ Draw(subLayer, offset, parallaxFactor, ref viewMatrix, ref projectionMatrix, effect, depth);
+ }
+ else
+ {
+ _worldMatrix.Translation = new Vector3(offset, depth);
+
+ var effect1 = effect ?? _defaultEffect;
+ var tiledMapEffect = effect1 as ITiledMapEffect;
+ if (tiledMapEffect == null)
+ return;
+
+ // model-to-world transform
+ tiledMapEffect.World = _worldMatrix;
+ tiledMapEffect.View = parallaxFactor == Vector2.One ? viewMatrix : IncludeParallax(viewMatrix, parallaxFactor);
+ tiledMapEffect.Projection = projectionMatrix;
+
+ foreach (var layerModel in _mapModel.LayersOfLayerModels[layer])
+ {
+ // desired alpha
+ tiledMapEffect.Alpha = layer.Opacity;
+
+ // desired texture
+ tiledMapEffect.Texture = layerModel.Texture;
+
+ // bind the vertex and index buffer
+ _graphicsDevice.SetVertexBuffer(layerModel.VertexBuffer);
+ _graphicsDevice.Indices = layerModel.IndexBuffer;
+
+ // for each pass in our effect
+ foreach (var pass in effect1.CurrentTechnique.Passes)
+ {
+ // apply the pass, effectively choosing which vertex shader and fragment (pixel) shader to use
+ pass.Apply();
+
+ // draw the geometry from the vertex buffer / index buffer
+ _graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, layerModel.TriangleCount);
+ }
+ }
+ }
+ }
+
+ private Matrix IncludeParallax(Matrix viewMatrix, Vector2 parallaxFactor)
+ {
+ viewMatrix.Translation *=new Vector3(parallaxFactor, 1f);
+ return viewMatrix;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModel.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModel.cs
new file mode 100644
index 0000000..34bf683
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModel.cs
@@ -0,0 +1,22 @@
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled.Renderers
+{
+ public sealed class TiledMapStaticLayerModel : TiledMapLayerModel
+ {
+ public TiledMapStaticLayerModel(GraphicsDevice graphicsDevice, Texture2D texture, VertexPositionTexture[] vertices, ushort[] indices)
+ : base(graphicsDevice, texture, vertices, indices)
+ {
+ }
+
+ protected override VertexBuffer CreateVertexBuffer(GraphicsDevice graphicsDevice, int vertexCount)
+ {
+ return new VertexBuffer(graphicsDevice, VertexPositionTexture.VertexDeclaration, vertexCount, BufferUsage.WriteOnly);
+ }
+
+ protected override IndexBuffer CreateIndexBuffer(GraphicsDevice graphicsDevice, int indexCount)
+ {
+ return new IndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, indexCount, BufferUsage.WriteOnly); ;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModelBuilder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModelBuilder.cs
new file mode 100644
index 0000000..8c7d49b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModelBuilder.cs
@@ -0,0 +1,16 @@
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled.Renderers
+{
+ public class TiledMapStaticLayerModelBuilder : TiledMapLayerModelBuilder<TiledMapStaticLayerModel>
+ {
+ protected override void ClearBuffers()
+ {
+ }
+
+ protected override TiledMapStaticLayerModel CreateModel(GraphicsDevice graphicsDevice, Texture2D texture)
+ {
+ return new TiledMapStaticLayerModel(graphicsDevice, texture, Vertices.ToArray(), Indices.ToArray());
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapContent.cs
new file mode 100644
index 0000000..30ecbfd
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapContent.cs
@@ -0,0 +1,75 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ [XmlRoot(ElementName = "map")]
+ public class TiledMapContent
+ {
+ public TiledMapContent()
+ {
+ Properties = new List<TiledMapPropertyContent>();
+ Tilesets = new List<TiledMapTilesetContent>();
+ Layers = new List<TiledMapLayerContent>();
+ }
+
+ [XmlIgnore]
+ public string Name { get; set; }
+
+ [XmlIgnore]
+ public string FilePath { get; set; }
+
+ // Deprecated as of Tiled 1.9.0 (replaced by "class" attribute)
+ [XmlAttribute(DataType = "string", AttributeName = "type")]
+ public string Type { get; set; }
+
+ [XmlAttribute(DataType = "string", AttributeName = "class")]
+ public string Class { get; set; }
+
+ [XmlAttribute(AttributeName = "version")]
+ public string Version { get; set; }
+
+ [XmlAttribute(AttributeName = "orientation")]
+ public TiledMapOrientationContent Orientation { get; set; }
+
+ [XmlAttribute(AttributeName = "renderorder")]
+ public TiledMapTileDrawOrderContent RenderOrder { get; set; }
+
+ [XmlAttribute(AttributeName = "backgroundcolor")]
+ public string BackgroundColor { get; set; }
+
+ [XmlAttribute(AttributeName = "width")]
+ public int Width { get; set; }
+
+ [XmlAttribute(AttributeName = "height")]
+ public int Height { get; set; }
+
+ [XmlAttribute(AttributeName = "tilewidth")]
+ public int TileWidth { get; set; }
+
+ [XmlAttribute(AttributeName = "tileheight")]
+ public int TileHeight { get; set; }
+
+ [XmlAttribute(AttributeName = "hexsidelength")]
+ public int HexSideLength { get; set; }
+
+ [XmlAttribute(AttributeName = "staggeraxis")]
+ public TiledMapStaggerAxisContent StaggerAxis { get; set; }
+
+ [XmlAttribute(AttributeName = "staggerindex")]
+ public TiledMapStaggerIndexContent StaggerIndex { get; set; }
+
+ [XmlElement(ElementName = "tileset")]
+ public List<TiledMapTilesetContent> Tilesets { get; set; }
+
+ [XmlElement(ElementName = "layer", Type = typeof(TiledMapTileLayerContent))]
+ [XmlElement(ElementName = "imagelayer", Type = typeof(TiledMapImageLayerContent))]
+ [XmlElement(ElementName = "objectgroup", Type = typeof(TiledMapObjectLayerContent))]
+ [XmlElement(ElementName = "group", Type = typeof(TiledMapGroupLayerContent))]
+ public List<TiledMapLayerContent> Layers { get; set; }
+
+ [XmlArray("properties")]
+ [XmlArrayItem("property")]
+ public List<TiledMapPropertyContent> Properties { get; set; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapEllipseContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapEllipseContent.cs
new file mode 100644
index 0000000..78f7bdd
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapEllipseContent.cs
@@ -0,0 +1,6 @@
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapEllipseContent
+ {
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapGroupLayerContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapGroupLayerContent.cs
new file mode 100644
index 0000000..a72b276
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapGroupLayerContent.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapGroupLayerContent : TiledMapLayerContent
+ {
+ protected TiledMapGroupLayerContent()
+ : base(TiledMapLayerType.GroupLayer)
+ {
+ }
+
+ [XmlElement(ElementName = "layer", Type = typeof(TiledMapTileLayerContent))]
+ [XmlElement(ElementName = "imagelayer", Type = typeof(TiledMapImageLayerContent))]
+ [XmlElement(ElementName = "objectgroup", Type = typeof(TiledMapObjectLayerContent))]
+ [XmlElement(ElementName = "group", Type = typeof(TiledMapGroupLayerContent))]
+ public List<TiledMapLayerContent> Layers { get; set; }
+
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageContent.cs
new file mode 100644
index 0000000..714da37
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageContent.cs
@@ -0,0 +1,44 @@
+using System.Xml.Serialization;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapImageContent
+ {
+ //[XmlIgnore]
+ //public Texture2DContent Content { get; set; }
+
+ //[XmlIgnore]
+ //public ExternalReference<Texture2DContent> ContentRef { get; set; }
+
+ [XmlAttribute(AttributeName = "source")]
+ public string Source { get; set; }
+
+ [XmlAttribute(AttributeName = "width")]
+ public int Width { get; set; }
+
+ [XmlAttribute(AttributeName = "height")]
+ public int Height { get; set; }
+
+ [XmlAttribute(AttributeName = "format")]
+ public string Format { get; set; }
+
+ [XmlAttribute(AttributeName = "trans")]
+ public string RawTransparentColor { get; set; } = string.Empty;
+
+ [XmlIgnore]
+ public Color TransparentColor
+ {
+ get => RawTransparentColor == string.Empty ? Color.Transparent : ColorHelper.FromHex(RawTransparentColor);
+ set => RawTransparentColor = ColorHelper.ToHex(value);
+ }
+
+ [XmlElement(ElementName = "data")]
+ public TiledMapTileLayerDataContent Data { get; set; }
+
+ public override string ToString()
+ {
+ return Source;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageLayerContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageLayerContent.cs
new file mode 100644
index 0000000..f347d1e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageLayerContent.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapImageLayerContent : TiledMapLayerContent
+ {
+ [XmlAttribute(AttributeName = "x")]
+ public int X { get; set; }
+
+ [XmlAttribute(AttributeName = "y")]
+ public int Y { get; set; }
+
+ [XmlElement(ElementName = "image")]
+ public TiledMapImageContent Image { get; set; }
+
+ public TiledMapImageLayerContent()
+ : base(TiledMapLayerType.ImageLayer)
+ {
+ Opacity = 1.0f;
+ Visible = true;
+ Properties = new List<TiledMapPropertyContent>();
+ }
+
+ public override string ToString()
+ {
+ return $"{Name}: {Image}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerContent.cs
new file mode 100644
index 0000000..01d8a9d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerContent.cs
@@ -0,0 +1,61 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ [XmlInclude(typeof(TiledMapTileLayerContent))]
+ [XmlInclude(typeof(TiledMapImageLayerContent))]
+ [XmlInclude(typeof(TiledMapObjectLayerContent))]
+ public abstract class TiledMapLayerContent
+ {
+ protected TiledMapLayerContent(TiledMapLayerType layerType)
+ {
+ LayerType = layerType;
+ Opacity = 1.0f;
+ ParallaxX = 1.0f;
+ ParallaxY = 1.0f;
+ Visible = true;
+ Properties = new List<TiledMapPropertyContent>();
+ }
+
+ [XmlAttribute(AttributeName = "name")]
+ public string Name { get; set; }
+
+ // Deprecated as of Tiled 1.9.0 (replaced by "class" attribute)
+ [XmlAttribute(DataType = "string", AttributeName = "type")]
+ public string Type { get; set; }
+
+ [XmlAttribute(DataType = "string", AttributeName = "class")]
+ public string Class { get; set; }
+
+ [XmlAttribute(AttributeName = "opacity")]
+ public float Opacity { get; set; }
+
+ [XmlAttribute(AttributeName = "visible")]
+ public bool Visible { get; set; }
+
+ [XmlAttribute(AttributeName = "offsetx")]
+ public float OffsetX { get; set; }
+
+ [XmlAttribute(AttributeName = "offsety")]
+ public float OffsetY { get; set; }
+
+ [XmlAttribute(AttributeName = "parallaxx")]
+ public float ParallaxX { get; set; }
+
+ [XmlAttribute(AttributeName = "parallaxy")]
+ public float ParallaxY { get; set; }
+
+ [XmlArray("properties")]
+ [XmlArrayItem("property")]
+ public List<TiledMapPropertyContent> Properties { get; set; }
+
+ [XmlIgnore]
+ public TiledMapLayerType LayerType { get; }
+
+ public override string ToString()
+ {
+ return Name;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerModelContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerModelContent.cs
new file mode 100644
index 0000000..4c2a761
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerModelContent.cs
@@ -0,0 +1,139 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.IO;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapLayerModelContent
+ {
+ private readonly List<VertexPositionTexture> _vertices;
+ private readonly List<ushort> _indices;
+
+ public string LayerName { get; }
+ public ReadOnlyCollection<VertexPositionTexture> Vertices { get; }
+ public ReadOnlyCollection<ushort> Indices { get; }
+ public Size2 ImageSize { get; }
+ public string TextureAssetName { get; }
+
+ public TiledMapLayerModelContent(string layerName, TiledMapImageContent image)
+ {
+ LayerName = layerName;
+ _vertices = new List<VertexPositionTexture>();
+ Vertices = new ReadOnlyCollection<VertexPositionTexture>(_vertices);
+ _indices = new List<ushort>();
+ Indices = new ReadOnlyCollection<ushort>(_indices);
+ ImageSize = new Size2(image.Width, image.Height);
+ TextureAssetName = Path.ChangeExtension(image.Source, null);
+ }
+
+ public TiledMapLayerModelContent(string layerName, TiledMapTilesetContent tileset)
+ : this(layerName, tileset.Image)
+ {
+ }
+
+ public void AddTileVertices(Point2 position, Rectangle? sourceRectangle = null, TiledMapTileFlipFlags flags = TiledMapTileFlipFlags.None)
+ {
+ float texelLeft, texelTop, texelRight, texelBottom;
+ var sourceRectangle1 = sourceRectangle ?? new Rectangle(0, 0, (int)ImageSize.Width, (int)ImageSize.Height);
+
+ if (sourceRectangle.HasValue)
+ {
+ var reciprocalWidth = 1f / ImageSize.Width;
+ var reciprocalHeight = 1f / ImageSize.Height;
+ texelLeft = sourceRectangle1.X * reciprocalWidth;
+ texelTop = sourceRectangle1.Y * reciprocalHeight;
+ texelRight = (sourceRectangle1.X + sourceRectangle1.Width) * reciprocalWidth;
+ texelBottom = (sourceRectangle1.Y + sourceRectangle1.Height) * reciprocalHeight;
+ }
+ else
+ {
+ texelLeft = 0;
+ texelTop = 0;
+ texelBottom = 1;
+ texelRight = 1;
+ }
+
+ VertexPositionTexture vertexTopLeft, vertexTopRight, vertexBottomLeft, vertexBottomRight;
+
+ vertexTopLeft.Position = new Vector3(position, 0);
+ vertexTopRight.Position = new Vector3(position + new Vector2(sourceRectangle1.Width, 0), 0);
+ vertexBottomLeft.Position = new Vector3(position + new Vector2(0, sourceRectangle1.Height), 0);
+ vertexBottomRight.Position =
+ new Vector3(position + new Vector2(sourceRectangle1.Width, sourceRectangle1.Height), 0);
+
+ vertexTopLeft.TextureCoordinate.Y = texelTop;
+ vertexTopLeft.TextureCoordinate.X = texelLeft;
+
+ vertexTopRight.TextureCoordinate.Y = texelTop;
+ vertexTopRight.TextureCoordinate.X = texelRight;
+
+ vertexBottomLeft.TextureCoordinate.Y = texelBottom;
+ vertexBottomLeft.TextureCoordinate.X = texelLeft;
+
+ vertexBottomRight.TextureCoordinate.Y = texelBottom;
+ vertexBottomRight.TextureCoordinate.X = texelRight;
+
+ var flipDiagonally = (flags & TiledMapTileFlipFlags.FlipDiagonally) != 0;
+ var flipHorizontally = (flags & TiledMapTileFlipFlags.FlipHorizontally) != 0;
+ var flipVertically = (flags & TiledMapTileFlipFlags.FlipVertically) != 0;
+
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.X, ref vertexBottomLeft.TextureCoordinate.X);
+ FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.Y, ref vertexBottomLeft.TextureCoordinate.Y);
+ }
+
+ if (flipHorizontally)
+ {
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.Y, ref vertexTopRight.TextureCoordinate.Y);
+ FloatHelper.Swap(ref vertexBottomLeft.TextureCoordinate.Y, ref vertexBottomRight.TextureCoordinate.Y);
+ }
+ else
+ {
+ FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.X, ref vertexTopRight.TextureCoordinate.X);
+ FloatHelper.Swap(ref vertexBottomLeft.TextureCoordinate.X, ref vertexBottomRight.TextureCoordinate.X);
+ }
+ }
+
+ if (flipVertically)
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.X, ref vertexBottomLeft.TextureCoordinate.X);
+ FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.X, ref vertexBottomRight.TextureCoordinate.X);
+ }
+ else
+ {
+ FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.Y, ref vertexBottomLeft.TextureCoordinate.Y);
+ FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.Y, ref vertexBottomRight.TextureCoordinate.Y);
+ }
+
+ _vertices.Add(vertexTopLeft);
+ _vertices.Add(vertexTopRight);
+ _vertices.Add(vertexBottomLeft);
+ _vertices.Add(vertexBottomRight);
+
+ Debug.Assert(Vertices.Count <= TiledMapHelper.MaximumVerticesPerModel);
+ }
+
+ public void AddTileIndices()
+ {
+ var indexOffset = Vertices.Count;
+
+ Debug.Assert(3 + indexOffset <= TiledMapHelper.MaximumVerticesPerModel);
+
+ _indices.Add((ushort)(0 + indexOffset));
+ _indices.Add((ushort)(1 + indexOffset));
+ _indices.Add((ushort)(2 + indexOffset));
+ _indices.Add((ushort)(1 + indexOffset));
+ _indices.Add((ushort)(3 + indexOffset));
+ _indices.Add((ushort)(2 + indexOffset));
+
+ Debug.Assert(Indices.Count <= TiledMapHelper.MaximumIndicesPerModel);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectContent.cs
new file mode 100644
index 0000000..76e2ba2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectContent.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ // This content class is going to be a lot more complex than the others we use.
+ // Objects can reference a template file which has starting values for the
+ // object. The value in the object file overrides any value specified in the
+ // template. All values have to be able to store a null value so we know if the
+ // XML parser actually found a value for the property and not just a default
+ // value. Default values are used when the object and any templates don't
+ // specify a value.
+ public class TiledMapObjectContent
+ {
+ // TODO: HACK These shouldn't be public
+ public uint? _globalIdentifier;
+ public int? _identifier;
+ public float? _height;
+ public float? _rotation;
+ public bool? _visible;
+ public float? _width;
+ public float? _x;
+ public float? _y;
+
+ [XmlAttribute(DataType = "int", AttributeName = "id")]
+ public int Identifier { get => _identifier ?? 0; set => _identifier = value; }
+
+ [XmlAttribute(DataType = "string", AttributeName = "name")]
+ public string Name { get; set; }
+
+ // Deprecated as of Tiled 1.9.0 (replaced by "class" attribute)
+ [XmlAttribute(DataType = "string", AttributeName = "type")]
+ public string Type { get; set; }
+
+ [XmlAttribute(DataType = "string", AttributeName = "class")]
+ public string Class { get; set; }
+
+ [XmlAttribute(DataType = "float", AttributeName = "x")]
+ public float X { get => _x ?? 0; set => _x = value; }
+
+ [XmlAttribute(DataType = "float", AttributeName = "y")]
+ public float Y { get => _y ?? 0; set => _y = value; }
+
+ [XmlAttribute(DataType = "float", AttributeName = "width")]
+ public float Width { get => _width ?? 0; set => _width = value; }
+
+ [XmlAttribute(DataType = "float", AttributeName = "height")]
+ public float Height { get => _height ?? 0; set => _height = value; }
+
+ [XmlAttribute(DataType = "float", AttributeName = "rotation")]
+ public float Rotation { get => _rotation ?? 0; set => _rotation = value; }
+
+ [XmlAttribute(DataType = "boolean", AttributeName = "visible")]
+ public bool Visible { get => _visible ?? true; set => _visible = value; }
+
+ [XmlArray("properties")]
+ [XmlArrayItem("property")]
+ public List<TiledMapPropertyContent> Properties { get; set; }
+
+ [XmlAttribute(DataType = "unsignedInt", AttributeName = "gid")]
+ public uint GlobalIdentifier { get => _globalIdentifier??0; set => _globalIdentifier = value; }
+
+ [XmlElement(ElementName = "ellipse")]
+ public TiledMapEllipseContent Ellipse { get; set; }
+
+ [XmlElement(ElementName = "polygon")]
+ public TiledMapPolygonContent Polygon { get; set; }
+
+ [XmlElement(ElementName = "polyline")]
+ public TiledMapPolylineContent Polyline { get; set; }
+
+ [XmlAttribute(DataType = "string", AttributeName = "template")]
+ public string TemplateSource { get; set; }
+
+
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectDrawOrderContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectDrawOrderContent.cs
new file mode 100644
index 0000000..f441375
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectDrawOrderContent.cs
@@ -0,0 +1,10 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public enum TiledMapObjectDrawOrderContent : byte
+ {
+ [XmlEnum(Name = "topdown")] TopDown,
+ [XmlEnum(Name = "index")] Manual
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectLayerContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectLayerContent.cs
new file mode 100644
index 0000000..30669b5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectLayerContent.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapObjectLayerContent : TiledMapLayerContent
+ {
+ public TiledMapObjectLayerContent()
+ : base(TiledMapLayerType.ObjectLayer)
+ {
+ Objects = new List<TiledMapObjectContent>();
+ }
+
+ [XmlAttribute(AttributeName = "color")]
+ public string Color { get; set; }
+
+ [XmlElement(ElementName = "object")]
+ public List<TiledMapObjectContent> Objects { get; set; }
+
+ [XmlAttribute(AttributeName = "draworder")]
+ public TiledMapObjectDrawOrderContent DrawOrder { get; set; }
+
+ public override string ToString()
+ {
+ return Name;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectTemplateContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectTemplateContent.cs
new file mode 100644
index 0000000..096f528
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectTemplateContent.cs
@@ -0,0 +1,17 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ [XmlRoot(ElementName = "template")]
+ public class TiledMapObjectTemplateContent
+ {
+ [XmlElement(ElementName = "tileset")]
+ public TiledMapTilesetContent Tileset { get; set; }
+
+ //[XmlIgnore]
+ //public ExternalReference<TiledMapTilesetContent> TilesetReference { get; set; }
+
+ [XmlElement(ElementName = "object")]
+ public TiledMapObjectContent Object { get; set; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapOrientationContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapOrientationContent.cs
new file mode 100644
index 0000000..af83824
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapOrientationContent.cs
@@ -0,0 +1,12 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public enum TiledMapOrientationContent : byte
+ {
+ [XmlEnum(Name = "orthogonal")] Orthogonal,
+ [XmlEnum(Name = "isometric")] Isometric,
+ [XmlEnum(Name = "staggered")] Staggered,
+ [XmlEnum(Name = "hexagonal")] Hexagonal
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolygonContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolygonContent.cs
new file mode 100644
index 0000000..e4a3d5e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolygonContent.cs
@@ -0,0 +1,10 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapPolygonContent
+ {
+ [XmlAttribute(AttributeName = "points")]
+ public string Points { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolylineContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolylineContent.cs
new file mode 100644
index 0000000..ba15f59
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolylineContent.cs
@@ -0,0 +1,10 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapPolylineContent
+ {
+ [XmlAttribute(AttributeName = "points")]
+ public string Points { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPropertyContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPropertyContent.cs
new file mode 100644
index 0000000..10cdfc1
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPropertyContent.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapPropertyContent
+ {
+ [XmlAttribute(AttributeName = "name")]
+ public string Name { get; set; }
+
+ [XmlAttribute(AttributeName = "value")]
+ public string ValueAttribute { get; set; }
+
+ [XmlText]
+ public string ValueBody { get; set; }
+
+ [XmlArray("properties")]
+ [XmlArrayItem("property")]
+ public List<TiledMapPropertyContent> Properties { get; set; }
+
+ public string Value => ValueAttribute ?? ValueBody;
+
+ public override string ToString()
+ {
+ return $"{Name}: {Value}";
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerAxisContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerAxisContent.cs
new file mode 100644
index 0000000..7a073f5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerAxisContent.cs
@@ -0,0 +1,10 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public enum TiledMapStaggerAxisContent : byte
+ {
+ [XmlEnum("x")]X,
+ [XmlEnum("y")]Y
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerIndexContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerIndexContent.cs
new file mode 100644
index 0000000..834c3a6
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerIndexContent.cs
@@ -0,0 +1,10 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public enum TiledMapStaggerIndexContent : byte
+ {
+ [XmlEnum("even")]Even,
+ [XmlEnum("odd")]Odd
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileContent.cs
new file mode 100644
index 0000000..fcd7a69
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileContent.cs
@@ -0,0 +1,9 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public struct TiledMapTileContent
+ {
+ [XmlAttribute(AttributeName = "gid")] public uint GlobalIdentifier;
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileDrawOrderContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileDrawOrderContent.cs
new file mode 100644
index 0000000..4762afd
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileDrawOrderContent.cs
@@ -0,0 +1,12 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public enum TiledMapTileDrawOrderContent : byte
+ {
+ [XmlEnum(Name = "right-down")] RightDown,
+ [XmlEnum(Name = "right-up")] RightUp,
+ [XmlEnum(Name = "left-down")] LeftDown,
+ [XmlEnum(Name = "left-up")] LeftUp
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerContent.cs
new file mode 100644
index 0000000..49f5ccf
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerContent.cs
@@ -0,0 +1,30 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapTileLayerContent : TiledMapLayerContent
+ {
+ public TiledMapTileLayerContent()
+ : base(TiledMapLayerType.TileLayer)
+ {
+ }
+
+ [XmlAttribute(AttributeName = "x")]
+ public int X { get; set; }
+
+ [XmlAttribute(AttributeName = "y")]
+ public int Y { get; set; }
+
+ [XmlAttribute(AttributeName = "width")]
+ public int Width { get; set; }
+
+ [XmlAttribute(AttributeName = "height")]
+ public int Height { get; set; }
+
+ [XmlElement(ElementName = "data")]
+ public TiledMapTileLayerDataContent Data { get; set; }
+
+ [XmlIgnore]
+ public TiledMapTile[] Tiles { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataChunkContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataChunkContent.cs
new file mode 100644
index 0000000..0ec10d9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataChunkContent.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapTileLayerDataChunkContent
+ {
+ [XmlAttribute(AttributeName = "x")]
+ public int X { get; set; }
+
+ [XmlAttribute(AttributeName = "y")]
+ public int Y { get; set; }
+
+ [XmlAttribute(AttributeName = "width")]
+ public int Width { get; set; }
+
+ [XmlAttribute(AttributeName = "height")]
+ public int Height { get; set; }
+
+ [XmlElement(ElementName = "tile")]
+ public List<TiledMapTileContent> Tiles { get; set; }
+
+ [XmlText]
+ public string Value { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataContent.cs
new file mode 100644
index 0000000..c19a02e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataContent.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapTileLayerDataContent
+ {
+ public TiledMapTileLayerDataContent()
+ {
+ Tiles = new List<TiledMapTileContent>();
+ }
+
+ [XmlAttribute(AttributeName = "encoding")]
+ public string Encoding { get; set; }
+
+ [XmlAttribute(AttributeName = "compression")]
+ public string Compression { get; set; }
+
+ [XmlElement(ElementName = "tile")]
+ public List<TiledMapTileContent> Tiles { get; set; }
+
+ [XmlElement(ElementName = "chunk")]
+ public List<TiledMapTileLayerDataChunkContent> Chunks { get; set; }
+
+ [XmlText]
+ public string Value { get; set; }
+
+ public override string ToString()
+ {
+ return $"{Encoding} {Compression}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileOffsetContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileOffsetContent.cs
new file mode 100644
index 0000000..7d2af87
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileOffsetContent.cs
@@ -0,0 +1,17 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ [XmlRoot(ElementName = "tileoffset")]
+ public class TiledMapTileOffsetContent
+ {
+ [XmlAttribute(AttributeName = "x")] public int X;
+
+ [XmlAttribute(AttributeName = "y")] public int Y;
+
+ public override string ToString()
+ {
+ return $"{X}, {Y}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetContent.cs
new file mode 100644
index 0000000..fa04c31
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetContent.cs
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ [XmlRoot(ElementName = "tileset")]
+ public class TiledMapTilesetContent
+ {
+ public TiledMapTilesetContent()
+ {
+ TileOffset = new TiledMapTileOffsetContent();
+ Tiles = new List<TiledMapTilesetTileContent>();
+ Properties = new List<TiledMapPropertyContent>();
+ }
+
+ [XmlAttribute(AttributeName = "firstgid")]
+ public int FirstGlobalIdentifier { get; set; }
+
+ [XmlAttribute(AttributeName = "source")]
+ public string Source { get; set; }
+
+ [XmlAttribute(AttributeName = "name")]
+ public string Name { get; set; }
+
+ // Deprecated as of Tiled 1.9.0 (replaced by "class" attribute)
+ [XmlAttribute(DataType = "string", AttributeName = "type")]
+ public string Type { get; set; }
+
+ [XmlAttribute(DataType = "string", AttributeName = "class")]
+ public string Class { get; set; }
+
+ [XmlAttribute(AttributeName = "tilewidth")]
+ public int TileWidth { get; set; }
+
+ [XmlAttribute(AttributeName = "tileheight")]
+ public int TileHeight { get; set; }
+
+ [XmlAttribute(AttributeName = "spacing")]
+ public int Spacing { get; set; }
+
+ [XmlAttribute(AttributeName = "margin")]
+ public int Margin { get; set; }
+
+ [XmlAttribute(AttributeName = "columns")]
+ public int Columns { get; set; }
+
+ [XmlAttribute(AttributeName = "tilecount")]
+ public int TileCount { get; set; }
+
+ [XmlElement(ElementName = "tileoffset")]
+ public TiledMapTileOffsetContent TileOffset { get; set; }
+
+ [XmlElement(ElementName = "grid")]
+ public TiledMapTilesetGridContent Grid { get; set; }
+
+ [XmlElement(ElementName = "tile")]
+ public List<TiledMapTilesetTileContent> Tiles { get; set; }
+
+ [XmlArray("properties")]
+ [XmlArrayItem("property")]
+ public List<TiledMapPropertyContent> Properties { get; set; }
+
+ [XmlElement(ElementName = "image")]
+ public TiledMapImageContent Image { get; set; }
+
+ public bool ContainsGlobalIdentifier(int globalIdentifier)
+ {
+ return globalIdentifier >= FirstGlobalIdentifier && globalIdentifier < FirstGlobalIdentifier + TileCount;
+ }
+
+ public override string ToString()
+ {
+ return $"{Name}: {Image}";
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetGridContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetGridContent.cs
new file mode 100644
index 0000000..a9071b8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetGridContent.cs
@@ -0,0 +1,16 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapTilesetGridContent
+ {
+ [XmlAttribute(AttributeName = "orientation")]
+ public TiledMapOrientationContent Orientation { get; set; }
+
+ [XmlAttribute(AttributeName = "width")]
+ public int Width { get; set; }
+
+ [XmlAttribute(AttributeName = "height")]
+ public int Height { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileAnimationFrameContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileAnimationFrameContent.cs
new file mode 100644
index 0000000..5a17137
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileAnimationFrameContent.cs
@@ -0,0 +1,18 @@
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapTilesetTileAnimationFrameContent
+ {
+ [XmlAttribute(AttributeName = "tileid")]
+ public int TileIdentifier { get; set; }
+
+ [XmlAttribute(AttributeName = "duration")]
+ public int Duration { get; set; }
+
+ public override string ToString()
+ {
+ return $"TileID: {TileIdentifier}, Duration: {Duration}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileContent.cs
new file mode 100644
index 0000000..2a82197
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileContent.cs
@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MonoGame.Extended.Tiled.Serialization
+{
+ public class TiledMapTilesetTileContent
+ {
+ public TiledMapTilesetTileContent()
+ {
+ Properties = new List<TiledMapPropertyContent>();
+ Type = string.Empty;
+ }
+
+ [XmlAttribute(AttributeName = "id")]
+ public int LocalIdentifier { get; set; }
+
+ [XmlAttribute(AttributeName = "type")]
+ public string Type { get; set; }
+
+ [XmlElement(ElementName = "image")]
+ public TiledMapImageContent Image { get; set; }
+
+ [XmlArray("objectgroup")]
+ [XmlArrayItem("object")]
+ public List<TiledMapObjectContent> Objects { get; set; }
+
+ [XmlArray("animation")]
+ [XmlArrayItem("frame")]
+ public List<TiledMapTilesetTileAnimationFrameContent> Frames { get; set; }
+
+ [XmlArray("properties")]
+ [XmlArrayItem("property")]
+ public List<TiledMapPropertyContent> Properties { get; set; }
+
+ public override string ToString()
+ {
+ return LocalIdentifier.ToString();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMap.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMap.cs
new file mode 100644
index 0000000..b3486bd
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMap.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public sealed class TiledMap
+ {
+ private readonly List<TiledMapImageLayer> _imageLayers = new List<TiledMapImageLayer>();
+ private readonly List<TiledMapLayer> _layers = new List<TiledMapLayer>();
+ private readonly Dictionary<string, TiledMapLayer> _layersByName = new Dictionary<string, TiledMapLayer>();
+ private readonly List<TiledMapObjectLayer> _objectLayers = new List<TiledMapObjectLayer>();
+ private readonly List<TiledMapTileLayer> _tileLayers = new List<TiledMapTileLayer>();
+ private readonly List<TiledMapTileset> _tilesets = new List<TiledMapTileset>();
+ private readonly List<Tuple<TiledMapTileset, int>> _firstGlobalIdentifiers = new List<Tuple<TiledMapTileset, int>>();
+
+ public string Name { get; }
+ public string Type { get; }
+ public int Width { get; }
+ public int Height { get; }
+ public int TileWidth { get; }
+ public int TileHeight { get; }
+ public TiledMapTileDrawOrder RenderOrder { get; }
+ public TiledMapOrientation Orientation { get; }
+ public TiledMapProperties Properties { get; }
+ public ReadOnlyCollection<TiledMapTileset> Tilesets { get; }
+ public ReadOnlyCollection<TiledMapLayer> Layers { get; }
+ public ReadOnlyCollection<TiledMapImageLayer> ImageLayers { get; }
+ public ReadOnlyCollection<TiledMapTileLayer> TileLayers { get; }
+ public ReadOnlyCollection<TiledMapObjectLayer> ObjectLayers { get; }
+
+ public Color? BackgroundColor { get; set; }
+ public int WidthInPixels => Width * TileWidth;
+ public int HeightInPixels => Height * TileHeight;
+
+ private TiledMap()
+ {
+ Layers = new ReadOnlyCollection<TiledMapLayer>(_layers);
+ ImageLayers = new ReadOnlyCollection<TiledMapImageLayer>(_imageLayers);
+ TileLayers = new ReadOnlyCollection<TiledMapTileLayer>(_tileLayers);
+ ObjectLayers = new ReadOnlyCollection<TiledMapObjectLayer>(_objectLayers);
+ Tilesets = new ReadOnlyCollection<TiledMapTileset>(_tilesets);
+ Properties = new TiledMapProperties();
+ }
+
+ public TiledMap(string name, string type, int width, int height, int tileWidth, int tileHeight, TiledMapTileDrawOrder renderOrder, TiledMapOrientation orientation, Color? backgroundColor = null)
+ : this()
+ {
+ Name = name;
+ Type = type;
+ Width = width;
+ Height = height;
+ TileWidth = tileWidth;
+ TileHeight = tileHeight;
+ RenderOrder = renderOrder;
+ Orientation = orientation;
+ BackgroundColor = backgroundColor;
+ }
+
+ public void AddTileset(TiledMapTileset tileset, int firstGlobalIdentifier)
+ {
+ _tilesets.Add(tileset);
+ _firstGlobalIdentifiers.Add(new Tuple<TiledMapTileset, int>(tileset, firstGlobalIdentifier));
+ }
+
+ public void AddLayer(TiledMapLayer layer)
+ => AddLayer(layer, true);
+
+ private void AddLayer(TiledMapLayer layer, bool root)
+ {
+ if (root) _layers.Add(layer);
+
+ if (_layersByName.ContainsKey(layer.Name))
+ throw new ArgumentException($"The TiledMap '{Name}' contains two or more layers named '{layer.Name}'. Please ensure all layers have unique names.");
+
+ _layersByName.Add(layer.Name, layer);
+
+ switch(layer)
+ {
+ case TiledMapImageLayer imageLayer:
+ _imageLayers.Add(imageLayer);
+ break;
+ case TiledMapTileLayer tileLayer:
+ _tileLayers.Add(tileLayer);
+ break;
+ case TiledMapObjectLayer objectLayer:
+ _objectLayers.Add(objectLayer);
+ break;
+ case TiledMapGroupLayer groupLayer:
+ foreach (var subLayer in groupLayer.Layers)
+ AddLayer(subLayer, false);
+ break;
+ }
+ }
+
+ public TiledMapLayer GetLayer(string layerName)
+ {
+ TiledMapLayer layer;
+ _layersByName.TryGetValue(layerName, out layer);
+ return layer;
+ }
+
+ public T GetLayer<T>(string layerName)
+ where T : TiledMapLayer
+ {
+ return GetLayer(layerName) as T;
+ }
+
+ public TiledMapTileset GetTilesetByTileGlobalIdentifier(int tileIdentifier)
+ {
+ foreach (var tileset in _firstGlobalIdentifiers)
+ {
+ if (tileIdentifier >= tileset.Item2 && tileIdentifier < tileset.Item2 + tileset.Item1.TileCount)
+ return tileset.Item1;
+ }
+
+ return null;
+ }
+
+ public int GetTilesetFirstGlobalIdentifier(TiledMapTileset tileset)
+ {
+ return _firstGlobalIdentifiers.FirstOrDefault(t => t.Item1 == tileset).Item2;
+ }
+
+ private static int CountLayers(TiledMapLayer layer)
+ {
+ var value = 0;
+ if (layer is TiledMapGroupLayer groupLayer)
+ foreach (var subLayer in groupLayer.Layers)
+ value += CountLayers(subLayer);
+ else
+ value = 1;
+
+ return value;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapEllipseObject.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapEllipseObject.cs
new file mode 100644
index 0000000..9140d62
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapEllipseObject.cs
@@ -0,0 +1,17 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public sealed class TiledMapEllipseObject : TiledMapObject
+ {
+ public TiledMapEllipseObject(int identifier, string name, Size2 size, Vector2 position, float rotation = 0, float opacity = 1, bool isVisible = true, string type = null)
+ : base(identifier, name, size, position, rotation, opacity, isVisible, type)
+ {
+ Radius = new Vector2(size.Width / 2.0f, size.Height / 2.0f);
+ Center = new Vector2(position.X + Radius.X, position.Y);
+ }
+
+ public Vector2 Center { get; }
+ public Vector2 Radius { get; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapGroupLayer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapGroupLayer.cs
new file mode 100644
index 0000000..48d43d1
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapGroupLayer.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public class TiledMapGroupLayer : TiledMapLayer
+ {
+ public List<TiledMapLayer> Layers { get; }
+ public TiledMapGroupLayer(string name, string type, List<TiledMapLayer> layers, Vector2? offset = null, Vector2? parallaxFactor = null, float opacity = 1, bool isVisible = true)
+ : base(name, type, offset, parallaxFactor, opacity, isVisible)
+ {
+ Layers = layers;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapHelper.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapHelper.cs
new file mode 100644
index 0000000..43edf67
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapHelper.cs
@@ -0,0 +1,51 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public static class TiledMapHelper
+ {
+ // 4 vertices per tile
+ public const int VerticesPerTile = 4;
+ // 2 triangles per tile (mesh), with each triangle indexing 3 out of 4 vertices, so 6 vertices
+ public const int IndicesPerTile = 6;
+ // by using ushort type for indices we are limited to indexing vertices from 0 to 65535
+ // this limits us on how many vertices can fit inside a single vertex buffer (65536 vertices)
+ public const int MaximumVerticesPerModel = ushort.MaxValue + 1;
+ // and thus, we know how many tiles we can fit inside a vertex or index buffer (16384 tiles)
+ public const int MaximumTilesPerGeometryContent = MaximumVerticesPerModel / VerticesPerTile;
+ // and thus, we also know the maximum number of indices we can fit inside a single index buffer (98304 indices)
+ public const int MaximumIndicesPerModel = MaximumTilesPerGeometryContent * IndicesPerTile;
+ // these optimal maximum numbers of course are not considering texture bindings which would practically lower the actual number of tiles per vertex / index buffer
+ // thus, the reason why it is a good to have ONE giant tileset (at least per layer)
+
+ internal static Rectangle GetTileSourceRectangle(int localTileIdentifier, int tileWidth, int tileHeight, int columns, int margin, int spacing)
+ {
+ var x = margin + localTileIdentifier % columns * (tileWidth + spacing);
+ var y = margin + localTileIdentifier / columns * (tileHeight + spacing);
+ return new Rectangle(x, y, tileWidth, tileHeight);
+ }
+
+ internal static Point2 GetOrthogonalPosition(int tileX, int tileY, int tileWidth, int tileHeight)
+ {
+ var x = tileX * tileWidth;
+ var y = tileY * tileHeight;
+ return new Vector2(x, y);
+ }
+
+ internal static Point2 GetIsometricPosition(int tileX, int tileY, int tileWidth, int tileHeight)
+ {
+ // You can think of an isometric Tiled map as a regular orthogonal map that is rotated -45 degrees
+ // i.e.: the origin (0, 0) is the top tile of the diamond grid;
+ // (mapWidth, 0) is the far right tile of the diamond grid
+ // (0, mapHeight) is the far left tile of the diamond grid
+ // (mapWidth, mapHeight) is the bottom tile of the diamond grid
+
+ var halfTileWidth = tileWidth * 0.5f;
+ var halfTileHeight = tileHeight * 0.5f;
+ // -1 because we want the top the tile-diamond (top-center) to be the origin in tile space
+ var x = (tileX - tileY - 1) * halfTileWidth;
+ var y = (tileX + tileY) * halfTileHeight;
+ return new Vector2(x, y);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapImageLayer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapImageLayer.cs
new file mode 100644
index 0000000..33bb700
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapImageLayer.cs
@@ -0,0 +1,18 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled
+{
+ public class TiledMapImageLayer : TiledMapLayer, IMovable
+ {
+ public TiledMapImageLayer(string name, string type, Texture2D image, Vector2? position = null, Vector2? offset = null, Vector2? parallaxFactor = null, float opacity = 1.0f, bool isVisible = true)
+ : base(name, type, offset, parallaxFactor, opacity, isVisible)
+ {
+ Image = image;
+ Position = position ?? Vector2.Zero;
+ }
+
+ public Texture2D Image { get; }
+ public Vector2 Position { get; set; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayer.cs
new file mode 100644
index 0000000..6226dd1
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayer.cs
@@ -0,0 +1,26 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public abstract class TiledMapLayer
+ {
+ public string Name { get; }
+ public string Type { get; }
+ public bool IsVisible { get; set; }
+ public float Opacity { get; set; }
+ public Vector2 Offset { get; set; }
+ public Vector2 ParallaxFactor { get; set; }
+ public TiledMapProperties Properties { get; }
+
+ protected TiledMapLayer(string name, string type, Vector2? offset = null, Vector2? parallaxFactor = null, float opacity = 1.0f, bool isVisible = true)
+ {
+ Name = name;
+ Type = type;
+ Offset = offset ?? Vector2.Zero;
+ ParallaxFactor = parallaxFactor ?? Vector2.One;
+ Opacity = opacity;
+ IsVisible = isVisible;
+ Properties = new TiledMapProperties();
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayerType.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayerType.cs
new file mode 100644
index 0000000..8af5a2e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayerType.cs
@@ -0,0 +1,10 @@
+namespace MonoGame.Extended.Tiled
+{
+ public enum TiledMapLayerType : byte
+ {
+ ImageLayer = 0,
+ TileLayer = 1,
+ ObjectLayer = 2,
+ GroupLayer = 3
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObject.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObject.cs
new file mode 100644
index 0000000..1e25084
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObject.cs
@@ -0,0 +1,35 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public abstract class TiledMapObject
+ {
+ protected TiledMapObject(int identifier, string name, Size2 size, Vector2 position, float rotation = 0, float opacity = 1, bool isVisible = true, string type = null)
+ {
+ Identifier = identifier;
+ Name = name;
+ IsVisible = isVisible;
+ Rotation = rotation;
+ Position = position;
+ Size = size;
+ Opacity = opacity;
+ Type = type;
+ Properties = new TiledMapProperties();
+ }
+
+ public int Identifier { get; }
+ public string Name { get; set; }
+ public string Type { get; set; }
+ public bool IsVisible { get; set; }
+ public float Opacity { get; set; }
+ public float Rotation { get; set; }
+ public Vector2 Position { get; }
+ public Size2 Size { get; set; }
+ public TiledMapProperties Properties { get; }
+
+ public override string ToString()
+ {
+ return $"{Identifier}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectDrawOrder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectDrawOrder.cs
new file mode 100644
index 0000000..1668f66
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectDrawOrder.cs
@@ -0,0 +1,8 @@
+namespace MonoGame.Extended.Tiled
+{
+ public enum TiledMapObjectDrawOrder : byte
+ {
+ TopDown,
+ Index,
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectLayer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectLayer.cs
new file mode 100644
index 0000000..b39b8d7
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectLayer.cs
@@ -0,0 +1,20 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public class TiledMapObjectLayer : TiledMapLayer
+ {
+ public TiledMapObjectLayer(string name, string type, TiledMapObject[] objects, Color? color = null, TiledMapObjectDrawOrder drawOrder = TiledMapObjectDrawOrder.TopDown,
+ Vector2? offset = null, Vector2? parallaxFactor = null, float opacity = 1.0f, bool isVisible = true)
+ : base(name, type, offset, parallaxFactor, opacity, isVisible)
+ {
+ Color = color;
+ DrawOrder = drawOrder;
+ Objects = objects;
+ }
+
+ public Color? Color { get; }
+ public TiledMapObjectDrawOrder DrawOrder { get; }
+ public TiledMapObject[] Objects { get; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectType.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectType.cs
new file mode 100644
index 0000000..305500a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectType.cs
@@ -0,0 +1,11 @@
+namespace MonoGame.Extended.Tiled
+{
+ public enum TiledMapObjectType : byte
+ {
+ Rectangle,
+ Ellipse,
+ Polygon,
+ Polyline,
+ Tile
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapOrientation.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapOrientation.cs
new file mode 100644
index 0000000..798c1c8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapOrientation.cs
@@ -0,0 +1,9 @@
+namespace MonoGame.Extended.Tiled
+{
+ public enum TiledMapOrientation
+ {
+ Orthogonal,
+ Isometric,
+ Staggered
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolygonObject.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolygonObject.cs
new file mode 100644
index 0000000..2df343f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolygonObject.cs
@@ -0,0 +1,15 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public sealed class TiledMapPolygonObject : TiledMapObject
+ {
+ public TiledMapPolygonObject(int identifier, string name, Point2[] points, Size2 size, Vector2 position, float rotation = 0, float opacity = 1, bool isVisible = true, string type = null)
+ : base(identifier, name, size, position, rotation, opacity, isVisible, type)
+ {
+ Points = points;
+ }
+
+ public Point2[] Points { get; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolylineObject.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolylineObject.cs
new file mode 100644
index 0000000..de50dbc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolylineObject.cs
@@ -0,0 +1,15 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public sealed class TiledMapPolylineObject : TiledMapObject
+ {
+ public TiledMapPolylineObject(int identifier, string name, Point2[] points, Size2 size, Vector2 position, float rotation = 0, float opacity = 1, bool isVisible = true, string type = null)
+ : base(identifier, name, size, position, rotation, opacity, isVisible, type)
+ {
+ Points = points;
+ }
+
+ public Point2[] Points { get; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapProperties.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapProperties.cs
new file mode 100644
index 0000000..651ae5d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapProperties.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Tiled
+{
+ public class TiledMapProperties : Dictionary<string, TiledMapPropertyValue>
+ {
+ public bool TryGetValue(string key, out string value)
+ {
+ bool result = TryGetValue(key, out TiledMapPropertyValue tmpVal);
+ value = result ? null : tmpVal.Value;
+ return result;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPropertyValue.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPropertyValue.cs
new file mode 100644
index 0000000..d7a0893
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPropertyValue.cs
@@ -0,0 +1,32 @@
+namespace MonoGame.Extended.Tiled;
+
+public class TiledMapPropertyValue
+{
+ public string Value { get; }
+
+ public TiledMapProperties Properties;
+
+ public TiledMapPropertyValue()
+ {
+ Value = string.Empty;
+ Properties = new();
+ }
+
+ public TiledMapPropertyValue(string value)
+ {
+ Value = value;
+ Properties = new();
+ }
+
+ public TiledMapPropertyValue(TiledMapProperties properties)
+ {
+ Value = string.Empty;
+ Properties = properties;
+ }
+
+ public override string ToString() => Value;
+
+ //public static implicit operator TiledMapPropertyValue(string value) => new(value);
+ public static implicit operator string(TiledMapPropertyValue value) => value.Value;
+ public static implicit operator TiledMapProperties(TiledMapPropertyValue value) => value.Properties;
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapReader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapReader.cs
new file mode 100644
index 0000000..bd49655
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapReader.cs
@@ -0,0 +1,229 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.Content;
+
+namespace MonoGame.Extended.Tiled
+{
+ public class TiledMapReader : ContentTypeReader<TiledMap>
+ {
+ protected override TiledMap Read(ContentReader reader, TiledMap existingInstance)
+ {
+ if (existingInstance != null)
+ return existingInstance;
+
+ var map = ReadTiledMap(reader);
+ reader.ReadTiledMapProperties(map.Properties);
+ ReadTilesets(reader, map);
+ ReadLayers(reader, map);
+ return map;
+ }
+
+ private static TiledMap ReadTiledMap(ContentReader reader)
+ {
+ var name = reader.AssetName;
+ var type = reader.ReadString();
+ var width = reader.ReadInt32();
+ var height = reader.ReadInt32();
+ var tileWidth = reader.ReadInt32();
+ var tileHeight = reader.ReadInt32();
+ var backgroundColor = reader.ReadColor();
+ var renderOrder = (TiledMapTileDrawOrder)reader.ReadByte();
+ var orientation = (TiledMapOrientation)reader.ReadByte();
+
+ return new TiledMap(name, type, width, height, tileWidth, tileHeight, renderOrder, orientation, backgroundColor);
+ }
+
+ private static void ReadTilesets(ContentReader reader, TiledMap map)
+ {
+ var tilesetCount = reader.ReadInt32();
+
+ for (var i = 0; i < tilesetCount; i++)
+ {
+ var firstGlobalIdentifier = reader.ReadInt32();
+ var tileset = ReadTileset(reader, map);
+ map.AddTileset(tileset, firstGlobalIdentifier);
+ }
+ }
+
+ private static TiledMapTileset ReadTileset(ContentReader reader, TiledMap map)
+ {
+ var external = reader.ReadBoolean();
+ var tileset = external ? reader.ReadExternalReference<TiledMapTileset>() : TiledMapTilesetReader.ReadTileset(reader);
+
+ return tileset;
+ }
+
+ private static void ReadLayers(ContentReader reader, TiledMap map)
+ {
+ foreach (var layer in ReadGroup(reader, map))
+ map.AddLayer(layer);
+ }
+ private static List<TiledMapLayer> ReadGroup(ContentReader reader, TiledMap map)
+ {
+ var layerCount = reader.ReadInt32();
+ var value = new List<TiledMapLayer>(layerCount);
+
+ for (var i = 0; i < layerCount; i++)
+ value.Add(ReadLayer(reader, map));
+
+ return value;
+ }
+
+ private static TiledMapLayer ReadLayer(ContentReader reader, TiledMap map)
+ {
+ var layerType = (TiledMapLayerType)reader.ReadByte();
+ var name = reader.ReadString();
+ var type = reader.ReadString();
+ var isVisible = reader.ReadBoolean();
+ var opacity = reader.ReadSingle();
+ var offsetX = reader.ReadSingle();
+ var offsetY = reader.ReadSingle();
+ var offset = new Vector2(offsetX, offsetY);
+ var parallaxX = reader.ReadSingle();
+ var parallaxY = reader.ReadSingle();
+ var parallaxFactor = new Vector2(parallaxX, parallaxY);
+ var properties = new TiledMapProperties();
+
+ reader.ReadTiledMapProperties(properties);
+
+ TiledMapLayer layer;
+
+ switch (layerType)
+ {
+ case TiledMapLayerType.ImageLayer:
+ layer = ReadImageLayer(reader, name, type, offset, parallaxFactor, opacity, isVisible);
+ break;
+ case TiledMapLayerType.TileLayer:
+ layer = ReadTileLayer(reader, name, type, offset, parallaxFactor, opacity, isVisible, map);
+ break;
+ case TiledMapLayerType.ObjectLayer:
+ layer = ReadObjectLayer(reader, name, type, offset, parallaxFactor, opacity, isVisible, map);
+ break;
+ case TiledMapLayerType.GroupLayer:
+ layer = new TiledMapGroupLayer(name, type, ReadGroup(reader, map), offset, parallaxFactor, opacity, isVisible);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+
+ foreach (var property in properties)
+ layer.Properties.Add(property.Key, property.Value);
+
+ return layer;
+ }
+
+ private static TiledMapLayer ReadObjectLayer(ContentReader reader, string name, string type, Vector2 offset, Vector2 parallaxFactor, float opacity, bool isVisible, TiledMap map)
+ {
+ var color = reader.ReadColor();
+ var drawOrder = (TiledMapObjectDrawOrder)reader.ReadByte();
+ var objectCount = reader.ReadInt32();
+ var objects = new TiledMapObject[objectCount];
+
+ for (var i = 0; i < objectCount; i++)
+ objects[i] = ReadTiledMapObject(reader, map);
+
+ return new TiledMapObjectLayer(name, type, objects, color, drawOrder, offset, parallaxFactor, opacity, isVisible);
+ }
+
+ private static TiledMapObject ReadTiledMapObject(ContentReader reader, TiledMap map)
+ {
+ var objectType = (TiledMapObjectType)reader.ReadByte();
+ var identifier = reader.ReadInt32();
+ var name = reader.ReadString();
+ var type = reader.ReadString();
+ var position = new Vector2(reader.ReadSingle(), reader.ReadSingle());
+ var width = reader.ReadSingle();
+ var height = reader.ReadSingle();
+ var size = new Size2(width, height);
+ var rotation = reader.ReadSingle();
+ var isVisible = reader.ReadBoolean();
+ var properties = new TiledMapProperties();
+ const float opacity = 1.0f;
+
+ reader.ReadTiledMapProperties(properties);
+
+ TiledMapObject mapObject;
+
+ switch (objectType)
+ {
+ case TiledMapObjectType.Rectangle:
+ mapObject = new TiledMapRectangleObject(identifier, name, size, position, rotation, opacity, isVisible, type);
+ break;
+ case TiledMapObjectType.Tile:
+ var globalTileIdentifierWithFlags = reader.ReadUInt32();
+ var tile = new TiledMapTile(globalTileIdentifierWithFlags, (ushort)position.X, (ushort)position.Y);
+ var tileset = map.GetTilesetByTileGlobalIdentifier(tile.GlobalIdentifier);
+ var localTileIdentifier = tile.GlobalIdentifier - map.GetTilesetFirstGlobalIdentifier(tileset);
+ var tilesetTile = tileset.Tiles.FirstOrDefault(x => x.LocalTileIdentifier == localTileIdentifier);
+ mapObject = new TiledMapTileObject(identifier, name, tileset, tilesetTile, size, position, rotation, opacity, isVisible, type);
+ break;
+ case TiledMapObjectType.Ellipse:
+ mapObject = new TiledMapEllipseObject(identifier, name, size, position, rotation, opacity, isVisible);
+ break;
+ case TiledMapObjectType.Polygon:
+ mapObject = new TiledMapPolygonObject(identifier, name, ReadPoints(reader), size, position, rotation, opacity, isVisible, type);
+ break;
+ case TiledMapObjectType.Polyline:
+ mapObject = new TiledMapPolylineObject(identifier, name, ReadPoints(reader), size, position, rotation, opacity, isVisible, type);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+
+ foreach (var property in properties)
+ mapObject.Properties.Add(property.Key, property.Value);
+
+ return mapObject;
+ }
+
+ private static Point2[] ReadPoints(ContentReader reader)
+ {
+ var pointCount = reader.ReadInt32();
+ var points = new Point2[pointCount];
+
+ for (var i = 0; i < pointCount; i++)
+ {
+ var x = reader.ReadSingle();
+ var y = reader.ReadSingle();
+ points[i] = new Point2(x, y);
+ }
+
+ return points;
+ }
+
+ private static TiledMapImageLayer ReadImageLayer(ContentReader reader, string name, string type, Vector2 offset, Vector2 parallaxFactor, float opacity, bool isVisible)
+ {
+ var texture = reader.ReadExternalReference<Texture2D>();
+ var x = reader.ReadSingle();
+ var y = reader.ReadSingle();
+ var position = new Vector2(x, y);
+ return new TiledMapImageLayer(name, type, texture, position, offset, parallaxFactor, opacity, isVisible);
+ }
+
+ private static TiledMapTileLayer ReadTileLayer(ContentReader reader, string name, string type, Vector2 offset, Vector2 parallaxFactor, float opacity, bool isVisible, TiledMap map)
+ {
+ var width = reader.ReadInt32();
+ var height = reader.ReadInt32();
+ var tileWidth = map.TileWidth;
+ var tileHeight = map.TileHeight;
+
+ var tileCount = reader.ReadInt32();
+ var layer = new TiledMapTileLayer(name, type, width, height, tileWidth, tileHeight, offset, parallaxFactor, opacity, isVisible);
+
+ for (var i = 0; i < tileCount; i++)
+ {
+ var globalTileIdentifierWithFlags = reader.ReadUInt32();
+ var x = reader.ReadUInt16();
+ var y = reader.ReadUInt16();
+
+ layer.Tiles[x + y * width] = new TiledMapTile(globalTileIdentifierWithFlags, x, y);
+ }
+
+ return layer;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapRectangleObject.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapRectangleObject.cs
new file mode 100644
index 0000000..de4c7ec
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapRectangleObject.cs
@@ -0,0 +1,12 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public sealed class TiledMapRectangleObject : TiledMapObject
+ {
+ public TiledMapRectangleObject(int identifier, string name, Size2 size, Vector2 position, float rotation = 0, float opacity = 1, bool isVisible = true, string type = null)
+ : base(identifier, name, size, position, rotation, opacity, isVisible, type)
+ {
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTile.cs
new file mode 100644
index 0000000..d3147a4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTile.cs
@@ -0,0 +1,28 @@
+namespace MonoGame.Extended.Tiled
+{
+ public struct TiledMapTile
+ {
+ public readonly ushort X;
+ public readonly ushort Y;
+ public readonly uint GlobalTileIdentifierWithFlags;
+
+ public int GlobalIdentifier => (int)(GlobalTileIdentifierWithFlags & ~(uint)TiledMapTileFlipFlags.All);
+ public bool IsFlippedHorizontally => (GlobalTileIdentifierWithFlags & (uint)TiledMapTileFlipFlags.FlipHorizontally) != 0;
+ public bool IsFlippedVertically => (GlobalTileIdentifierWithFlags & (uint)TiledMapTileFlipFlags.FlipVertically) != 0;
+ public bool IsFlippedDiagonally => (GlobalTileIdentifierWithFlags & (uint)TiledMapTileFlipFlags.FlipDiagonally) != 0;
+ public bool IsBlank => GlobalIdentifier == 0;
+ public TiledMapTileFlipFlags Flags => (TiledMapTileFlipFlags)(GlobalTileIdentifierWithFlags & (uint)TiledMapTileFlipFlags.All);
+
+ public TiledMapTile(uint globalTileIdentifierWithFlags, ushort x, ushort y)
+ {
+ GlobalTileIdentifierWithFlags = globalTileIdentifierWithFlags;
+ X = x;
+ Y = y;
+ }
+
+ public override string ToString()
+ {
+ return $"GlobalIdentifier: {GlobalIdentifier}, Flags: {Flags}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileDrawOrder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileDrawOrder.cs
new file mode 100644
index 0000000..a40b596
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileDrawOrder.cs
@@ -0,0 +1,10 @@
+namespace MonoGame.Extended.Tiled
+{
+ public enum TiledMapTileDrawOrder : byte
+ {
+ RightDown,
+ RightUp,
+ LeftDown,
+ LeftUp
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileFlipFlags.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileFlipFlags.cs
new file mode 100644
index 0000000..f883bad
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileFlipFlags.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace MonoGame.Extended.Tiled
+{
+ [Flags]
+ public enum TiledMapTileFlipFlags : uint
+ {
+ None = 0,
+ FlipDiagonally = 0x20000000,
+ FlipVertically = 0x40000000,
+ FlipHorizontally = 0x80000000,
+ All = FlipDiagonally | FlipVertically | FlipHorizontally
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileLayer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileLayer.cs
new file mode 100644
index 0000000..5d07f06
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileLayer.cs
@@ -0,0 +1,66 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public class TiledMapTileLayer : TiledMapLayer
+ {
+ public TiledMapTileLayer(string name, string type, int width, int height, int tileWidth, int tileHeight, Vector2? offset = null,
+ Vector2? parallaxFactor = null, float opacity = 1, bool isVisible = true)
+ : base(name, type, offset, parallaxFactor, opacity, isVisible)
+ {
+ Width = width;
+ Height = height;
+ TileWidth = tileWidth;
+ TileHeight = tileHeight;
+ Tiles = new TiledMapTile[Width * Height];
+ }
+
+ public int Width { get; }
+ public int Height { get; }
+ public int TileWidth { get; }
+ public int TileHeight { get; }
+ public TiledMapTile[] Tiles { get; }
+
+ public int GetTileIndex(ushort x, ushort y)
+ {
+ return x + y * Width;
+ }
+
+ public bool TryGetTile(ushort x, ushort y, out TiledMapTile? tile)
+ {
+ if (x >= Width)
+ {
+ tile = null;
+ return false;
+ }
+ var index = GetTileIndex(x, y);
+
+ if (index < 0 || index >= Tiles.Length)
+ {
+ tile = null;
+ return false;
+ }
+
+ tile = Tiles[index];
+ return true;
+ }
+
+ public TiledMapTile GetTile(ushort x, ushort y)
+ {
+ var index = GetTileIndex(x, y);
+ return Tiles[index];
+ }
+
+ public void SetTile(ushort x, ushort y, uint globalIdentifier)
+ {
+ var index = GetTileIndex(x, y);
+ Tiles[index] = new TiledMapTile(globalIdentifier, x, y);
+ }
+
+ public void RemoveTile(ushort x, ushort y)
+ {
+ SetTile(x, y, 0);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileObject.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileObject.cs
new file mode 100644
index 0000000..b4e8679
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileObject.cs
@@ -0,0 +1,18 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public sealed class TiledMapTileObject : TiledMapObject
+ {
+ public TiledMapTileObject(int identifier, string name, TiledMapTileset tileset, TiledMapTilesetTile tile,
+ Size2 size, Vector2 position, float rotation = 0, float opacity = 1, bool isVisible = true, string type = null)
+ : base(identifier, name, size, position, rotation, opacity, isVisible, type)
+ {
+ Tileset = tileset;
+ Tile = tile;
+ }
+
+ public TiledMapTilesetTile Tile { get; }
+ public TiledMapTileset Tileset { get; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileset.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileset.cs
new file mode 100644
index 0000000..ed14e15
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileset.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Tiled
+{
+ public interface ITileset
+ {
+ int ActualWidth { get; }
+ int Columns { get; }
+ int ActualHeight { get; }
+ int Rows { get; }
+ int TileWidth { get; }
+ int TileHeight { get; }
+ Texture2D Texture { get; }
+ TextureRegion2D GetRegion(int column, int row);
+ }
+
+ public class TiledMapTileset : ITileset
+ {
+ public TiledMapTileset(Texture2D texture, string type, int tileWidth, int tileHeight, int tileCount, int spacing, int margin, int columns)
+ {
+ Texture = texture;
+ Type = type;
+ TileWidth = tileWidth;
+ TileHeight = tileHeight;
+ TileCount = tileCount;
+ Spacing = spacing;
+ Margin = margin;
+ Columns = columns;
+ Properties = new TiledMapProperties();
+ Tiles = new List<TiledMapTilesetTile>();
+ }
+
+ public string Name => Texture.Name;
+ public Texture2D Texture { get; }
+
+ public TextureRegion2D GetRegion(int column, int row)
+ {
+ var x = Margin + column * (TileWidth + Spacing);
+ var y = Margin + row * (TileHeight + Spacing);
+ return new TextureRegion2D(Texture, x, y, TileWidth, TileHeight);
+ }
+
+ public string Type { get; }
+ public int TileWidth { get; }
+ public int TileHeight { get; }
+ public int Spacing { get; }
+ public int Margin { get; }
+ public int TileCount { get; }
+ public int Columns { get; }
+ public List<TiledMapTilesetTile> Tiles { get; }
+ public TiledMapProperties Properties { get; }
+
+ public int Rows => (int)Math.Ceiling((double) TileCount / Columns);
+ public int ActualWidth => TileWidth * Columns;
+ public int ActualHeight => TileHeight * Rows;
+
+ public Rectangle GetTileRegion(int localTileIdentifier)
+ {
+ return Texture is not null
+ ? TiledMapHelper.GetTileSourceRectangle(localTileIdentifier, TileWidth, TileHeight, Columns, Margin,
+ Spacing)
+ : Tiles.FirstOrDefault(x => x.LocalTileIdentifier == localTileIdentifier).Texture.Bounds;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetAnimatedTile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetAnimatedTile.cs
new file mode 100644
index 0000000..1bbb770
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetAnimatedTile.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.ObjectModel;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled
+{
+ public class TiledMapTilesetAnimatedTile : TiledMapTilesetTile
+ {
+ private TimeSpan _timer = TimeSpan.Zero;
+ private int _frameIndex;
+
+ public ReadOnlyCollection<TiledMapTilesetTileAnimationFrame> AnimationFrames { get; }
+ public TiledMapTilesetTileAnimationFrame CurrentAnimationFrame { get; private set; }
+
+ public TiledMapTilesetAnimatedTile(int localTileIdentifier,
+ TiledMapTilesetTileAnimationFrame[] frames, string type = null, TiledMapObject[] objects = null, Texture2D texture = null)
+ : base(localTileIdentifier, type, objects, texture)
+ {
+ if (frames.Length == 0) throw new InvalidOperationException("There must be at least one tileset animation frame");
+
+ AnimationFrames = new ReadOnlyCollection<TiledMapTilesetTileAnimationFrame>(frames);
+ CurrentAnimationFrame = AnimationFrames[0];
+ }
+
+ public void CreateTextureRotations(TiledMapTileset tileset, TiledMapTileFlipFlags flipFlags)
+ {
+ for (int i = 0; i < AnimationFrames.Count; i++)
+ {
+ AnimationFrames[i].CreateTextureRotations(tileset, flipFlags);
+ }
+ }
+
+ public void Update(GameTime gameTime)
+ {
+ _timer += gameTime.ElapsedGameTime;
+
+ if (_timer <= CurrentAnimationFrame.Duration)
+ return;
+
+ _timer -= CurrentAnimationFrame.Duration;
+ _frameIndex = (_frameIndex + 1) % AnimationFrames.Count;
+ CurrentAnimationFrame = AnimationFrames[_frameIndex];
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetReader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetReader.cs
new file mode 100644
index 0000000..646f835
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetReader.cs
@@ -0,0 +1,150 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using System;
+
+namespace MonoGame.Extended.Tiled
+{
+ public class TiledMapTilesetReader : ContentTypeReader<TiledMapTileset>
+ {
+ protected override TiledMapTileset Read(ContentReader reader, TiledMapTileset existingInstance)
+ {
+ if (existingInstance != null)
+ return existingInstance;
+
+ return ReadTileset(reader);
+ }
+
+ public static TiledMapTileset ReadTileset(ContentReader reader)
+ {
+ var texture = reader.ReadExternalReference<Texture2D>();
+ var @class = reader.ReadString();
+ var tileWidth = reader.ReadInt32();
+ var tileHeight = reader.ReadInt32();
+ var tileCount = reader.ReadInt32();
+ var spacing = reader.ReadInt32();
+ var margin = reader.ReadInt32();
+ var columns = reader.ReadInt32();
+ var explicitTileCount = reader.ReadInt32();
+
+ var tileset = new TiledMapTileset(texture, @class, tileWidth, tileHeight, tileCount, spacing, margin, columns);
+
+ for (var tileIndex = 0; tileIndex < explicitTileCount; tileIndex++)
+ ReadTile(reader, tileset);
+
+ reader.ReadTiledMapProperties(tileset.Properties);
+ return tileset;
+ }
+
+ private static void ReadTile(ContentReader reader, TiledMapTileset tileset)
+ {
+ var texture = reader.ReadExternalReference<Texture2D>();
+
+ var localTileIdentifier = reader.ReadInt32();
+ var type = reader.ReadString();
+ var animationFramesCount = reader.ReadInt32();
+ var objectCount = reader.ReadInt32();
+ var objects = new TiledMapObject[objectCount];
+
+ for (var i = 0; i < objectCount; i++)
+ objects[i] = ReadTiledMapObject(reader, tileset);
+
+ var tilesetTile = animationFramesCount <= 0
+ ? new TiledMapTilesetTile(localTileIdentifier, type, objects, texture)
+ : new TiledMapTilesetAnimatedTile(localTileIdentifier,
+ ReadTiledMapTilesetAnimationFrames(reader, tileset, animationFramesCount), type, objects, texture);
+
+ reader.ReadTiledMapProperties(tilesetTile.Properties);
+ tileset.Tiles.Add(tilesetTile);
+ }
+
+ private static TiledMapTilesetTileAnimationFrame[] ReadTiledMapTilesetAnimationFrames(ContentReader reader, TiledMapTileset tileset, int animationFramesCount)
+ {
+ var animationFrames = new TiledMapTilesetTileAnimationFrame[animationFramesCount];
+
+ for (var i = 0; i < animationFramesCount; i++)
+ {
+ var localTileIdentifierForFrame = reader.ReadInt32();
+ var frameDurationInMilliseconds = reader.ReadInt32();
+ var tileSetTileFrame = new TiledMapTilesetTileAnimationFrame(tileset, localTileIdentifierForFrame, frameDurationInMilliseconds);
+ animationFrames[i] = tileSetTileFrame;
+ }
+
+ return animationFrames;
+ }
+
+ private static TiledMapTilesetTile ReadTiledMapTilesetTile(ContentReader reader, TiledMapTileset tileset, Func<TiledMapObject[], TiledMapTilesetTile> createTile)
+ {
+ var texture = reader.ReadExternalReference<Texture2D>();
+ var objectCount = reader.ReadInt32();
+ var objects = new TiledMapObject[objectCount];
+
+ for (var i = 0; i < objectCount; i++)
+ objects[i] = ReadTiledMapObject(reader, tileset);
+
+ return createTile(objects);
+ }
+
+ private static TiledMapObject ReadTiledMapObject(ContentReader reader, TiledMapTileset tileset)
+ {
+ var objectType = (TiledMapObjectType)reader.ReadByte();
+ var identifier = reader.ReadInt32();
+ var name = reader.ReadString();
+ var type = reader.ReadString();
+ var position = new Vector2(reader.ReadSingle(), reader.ReadSingle());
+ var width = reader.ReadSingle();
+ var height = reader.ReadSingle();
+ var size = new Size2(width, height);
+ var rotation = reader.ReadSingle();
+ var isVisible = reader.ReadBoolean();
+ var properties = new TiledMapProperties();
+ const float opacity = 1.0f;
+
+ reader.ReadTiledMapProperties(properties);
+
+ TiledMapObject mapObject;
+
+ switch (objectType)
+ {
+ case TiledMapObjectType.Rectangle:
+ mapObject = new TiledMapRectangleObject(identifier, name, size, position, rotation, opacity, isVisible, type);
+ break;
+ case TiledMapObjectType.Tile:
+ reader.ReadUInt32(); // Tile objects within TiledMapTilesetTiles currently ignore the gid and behave like rectangle objects.
+ mapObject = new TiledMapRectangleObject(identifier, name, size, position, rotation, opacity, isVisible, type);
+ break;
+ case TiledMapObjectType.Ellipse:
+ mapObject = new TiledMapEllipseObject(identifier, name, size, position, rotation, opacity, isVisible);
+ break;
+ case TiledMapObjectType.Polygon:
+ mapObject = new TiledMapPolygonObject(identifier, name, ReadPoints(reader), size, position, rotation, opacity, isVisible, type);
+ break;
+ case TiledMapObjectType.Polyline:
+ mapObject = new TiledMapPolylineObject(identifier, name, ReadPoints(reader), size, position, rotation, opacity, isVisible, type);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+
+ foreach (var property in properties)
+ mapObject.Properties.Add(property.Key, property.Value);
+
+ return mapObject;
+ }
+
+ private static Point2[] ReadPoints(ContentReader reader)
+ {
+ var pointCount = reader.ReadInt32();
+ var points = new Point2[pointCount];
+
+ for (var i = 0; i < pointCount; i++)
+ {
+ var x = reader.ReadSingle();
+ var y = reader.ReadSingle();
+ points[i] = new Point2(x, y);
+ }
+
+ return points;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTile.cs
new file mode 100644
index 0000000..e378aa3
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTile.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tiled
+{
+ [DebuggerDisplay("{LocalTileIdentifier}: Type: {Type}, Properties: {Properties.Count}, Objects: {Objects.Count}")]
+ public class TiledMapTilesetTile
+ {
+ // For remove libraries
+ public TiledMapTilesetTile(int localTileIdentifier, string type = null,
+ TiledMapObject[] objects = null)
+ {
+ LocalTileIdentifier = localTileIdentifier;
+ Type = type;
+ Objects = objects != null ? new List<TiledMapObject>(objects) : new List<TiledMapObject>();
+ Properties = new TiledMapProperties();
+ }
+
+ public TiledMapTilesetTile(int localTileIdentifier, string type = null,
+ TiledMapObject[] objects = null, Texture2D texture = null)
+ {
+ Texture = texture;
+ LocalTileIdentifier = localTileIdentifier;
+ Type = type;
+ Objects = objects != null ? new List<TiledMapObject>(objects) : new List<TiledMapObject>();
+ Properties = new TiledMapProperties();
+ }
+
+ public int LocalTileIdentifier { get; }
+ public string Type { get; }
+ public TiledMapProperties Properties { get; }
+ public List<TiledMapObject> Objects { get; }
+ public Texture2D Texture { get; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTileAnimationFrame.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTileAnimationFrame.cs
new file mode 100644
index 0000000..9dbca80
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTileAnimationFrame.cs
@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tiled
+{
+ public struct TiledMapTilesetTileAnimationFrame
+ {
+ public readonly int LocalTileIdentifier;
+ public readonly TimeSpan Duration;
+ public readonly Vector2[] TextureCoordinates;
+ private readonly Dictionary<TiledMapTileFlipFlags, Vector2[]> _flipDictionary = new Dictionary<TiledMapTileFlipFlags, Vector2[]>();
+
+ internal TiledMapTilesetTileAnimationFrame(TiledMapTileset tileset, int localTileIdentifier, int durationInMilliseconds)
+ {
+ LocalTileIdentifier = localTileIdentifier;
+ Duration = new TimeSpan(0, 0, 0, 0, durationInMilliseconds);
+ TextureCoordinates = new Vector2[4];
+ CreateTextureCoordinates(tileset);
+ }
+
+ public Vector2[] GetTextureCoordinates(TiledMapTileFlipFlags flipFlags)
+ {
+ if (!_flipDictionary.TryGetValue(flipFlags, out Vector2[] flippedTextureCoordiantes))
+ {
+ return TextureCoordinates;
+ }
+ else
+ {
+ return flippedTextureCoordiantes;
+ }
+ }
+
+ public void CreateTextureRotations(TiledMapTileset tileset, TiledMapTileFlipFlags flipFlags)
+ {
+ if (!_flipDictionary.ContainsKey(flipFlags))
+ {
+ if (flipFlags == TiledMapTileFlipFlags.None)
+ {
+ _flipDictionary.Add(flipFlags, TextureCoordinates);
+ }
+ else
+ {
+ _flipDictionary.Add(flipFlags, TransformTextureCoordinates(tileset, flipFlags));
+ }
+ }
+ }
+
+ public Vector2[] TransformTextureCoordinates(TiledMapTileset tileset, TiledMapTileFlipFlags flipFlags)
+ {
+ var sourceRectangle = tileset.GetTileRegion(LocalTileIdentifier);
+ var texture = tileset.Texture;
+ var texelLeft = (float)sourceRectangle.X / texture.Width;
+ var texelTop = (float)sourceRectangle.Y / texture.Height;
+ var texelRight = (sourceRectangle.X + sourceRectangle.Width) / (float)texture.Width;
+ var texelBottom = (sourceRectangle.Y + sourceRectangle.Height) / (float)texture.Height;
+
+ var flipDiagonally = (flipFlags & TiledMapTileFlipFlags.FlipDiagonally) != 0;
+ var flipHorizontally = (flipFlags & TiledMapTileFlipFlags.FlipHorizontally) != 0;
+ var flipVertically = (flipFlags & TiledMapTileFlipFlags.FlipVertically) != 0;
+ var transform = new Vector2[4];
+
+ transform[0].X = texelLeft;
+ transform[0].Y = texelTop;
+
+ transform[1].X = texelRight;
+ transform[1].Y = texelTop;
+
+ transform[2].X = texelLeft;
+ transform[2].Y = texelBottom;
+
+ transform[3].X = texelRight;
+ transform[3].Y = texelBottom;
+
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref transform[1].X, ref transform[2].X);
+ FloatHelper.Swap(ref transform[1].Y, ref transform[2].Y);
+ }
+
+ if (flipHorizontally)
+ {
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref transform[0].Y, ref transform[1].Y);
+ FloatHelper.Swap(ref transform[2].Y, ref transform[3].Y);
+ }
+ else
+ {
+ FloatHelper.Swap(ref transform[0].X, ref transform[1].X);
+ FloatHelper.Swap(ref transform[2].X, ref transform[3].X);
+ }
+ }
+
+ if (flipVertically)
+ {
+ if (flipDiagonally)
+ {
+ FloatHelper.Swap(ref transform[0].X, ref transform[2].X);
+ FloatHelper.Swap(ref transform[1].X, ref transform[3].X);
+ }
+ else
+ {
+ FloatHelper.Swap(ref transform[0].Y, ref transform[2].Y);
+ FloatHelper.Swap(ref transform[1].Y, ref transform[3].Y);
+ }
+ }
+
+ transform[0] = transform[0];
+ transform[1] = transform[1];
+ transform[2] = transform[2];
+ transform[3] = transform[3];
+
+ return transform;
+ }
+
+ private void CreateTextureCoordinates(TiledMapTileset tileset)
+ {
+ var sourceRectangle = tileset.GetTileRegion(LocalTileIdentifier);
+ var texture = tileset.Texture;
+ var texelLeft = (float)sourceRectangle.X / texture.Width;
+ var texelTop = (float)sourceRectangle.Y / texture.Height;
+ var texelRight = (sourceRectangle.X + sourceRectangle.Width) / (float)texture.Width;
+ var texelBottom = (sourceRectangle.Y + sourceRectangle.Height) / (float)texture.Height;
+
+ TextureCoordinates[0].X = texelLeft;
+ TextureCoordinates[0].Y = texelTop;
+
+ TextureCoordinates[1].X = texelRight;
+ TextureCoordinates[1].Y = texelTop;
+
+ TextureCoordinates[2].X = texelLeft;
+ TextureCoordinates[2].Y = texelBottom;
+
+ TextureCoordinates[3].X = texelRight;
+ TextureCoordinates[3].Y = texelBottom;
+ }
+
+ public override string ToString()
+ {
+ return $"{LocalTileIdentifier}:{Duration}";
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/ColorTween.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/ColorTween.cs
new file mode 100644
index 0000000..2e98e5d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/ColorTween.cs
@@ -0,0 +1,15 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tweening;
+
+public class ColorTween: Tween<Color>
+{
+ internal ColorTween(object target, float duration, float delay, TweenMember<Color> member, Color endValue) : base(target, duration, delay, member, endValue)
+ {
+ }
+
+ protected override void Interpolate(float n)
+ {
+ Member.Value = Color.Lerp(_startValue, _endValue, n);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/EasingFunctions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/EasingFunctions.cs
new file mode 100644
index 0000000..f93a5ff
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/EasingFunctions.cs
@@ -0,0 +1,120 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tweening
+{
+ public static class EasingFunctions
+ {
+ public static float Linear(float value) => value;
+
+ public static float CubicIn(float value) => Power.In(value, 3);
+ public static float CubicOut(float value) => Power.Out(value, 3);
+ public static float CubicInOut(float value) => Power.InOut(value, 3);
+
+ public static float QuadraticIn(float value) => Power.In(value, 2);
+ public static float QuadraticOut(float value) => Power.Out(value, 2);
+ public static float QuadraticInOut(float value) => Power.InOut(value, 2);
+
+ public static float QuarticIn(float value) => Power.In(value, 4);
+ public static float QuarticOut(float value) => Power.Out(value, 4);
+ public static float QuarticInOut(float value) => Power.InOut(value, 4);
+
+ public static float QuinticIn(float value) => Power.In(value, 5);
+ public static float QuinticOut(float value) => Power.Out(value, 5);
+ public static float QuinticInOut(float value) => Power.InOut(value, 5);
+
+ public static float SineIn(float value) => (float) Math.Sin(value*MathHelper.PiOver2 - MathHelper.PiOver2) + 1;
+ public static float SineOut(float value) => (float) Math.Sin(value*MathHelper.PiOver2);
+ public static float SineInOut(float value) => (float) (Math.Sin(value*MathHelper.Pi - MathHelper.PiOver2) + 1)/2;
+
+ public static float ExponentialIn(float value) => (float) Math.Pow(2, 10*(value - 1));
+ public static float ExponentialOut(float value) => Out(value, ExponentialIn);
+ public static float ExponentialInOut(float value) => InOut(value, ExponentialIn);
+
+ public static float CircleIn(float value) => (float) -(Math.Sqrt(1 - value * value) - 1);
+ public static float CircleOut(float value) => (float) Math.Sqrt(1 - (value - 1) * (value - 1));
+ public static float CircleInOut(float value) => (float) (value <= .5 ? (Math.Sqrt(1 - value * value * 4) - 1) / -2 : (Math.Sqrt(1 - (value * 2 - 2) * (value * 2 - 2)) + 1) / 2);
+
+ public static float ElasticIn(float value)
+ {
+ const int oscillations = 1;
+ const float springiness = 3f;
+ var e = (Math.Exp(springiness*value) - 1)/(Math.Exp(springiness) - 1);
+ return (float) (e*Math.Sin((MathHelper.PiOver2 + MathHelper.TwoPi*oscillations)*value));
+ }
+
+ public static float ElasticOut(float value) => Out(value, ElasticIn);
+ public static float ElasticInOut(float value) => InOut(value, ElasticIn);
+
+ public static float BackIn(float value)
+ {
+ const float amplitude = 1f;
+ return (float) (Math.Pow(value, 3) - value*amplitude*Math.Sin(value*MathHelper.Pi));
+ }
+
+ public static float BackOut(float value) => Out(value, BackIn);
+ public static float BackInOut(float value) => InOut(value, BackIn);
+
+ public static float BounceOut(float value) => Out(value, BounceIn);
+ public static float BounceInOut(float value) => InOut(value, BounceIn);
+
+ public static float BounceIn(float value)
+ {
+ const float bounceConst1 = 2.75f;
+ var bounceConst2 = (float) Math.Pow(bounceConst1, 2);
+
+ value = 1 - value; //flip x-axis
+
+ if (value < 1/bounceConst1) // big bounce
+ return 1f - bounceConst2*value*value;
+
+ if (value < 2/bounceConst1)
+ return 1 - (float) (bounceConst2*Math.Pow(value - 1.5f/bounceConst1, 2) + .75);
+
+ if (value < 2.5/bounceConst1)
+ return 1 - (float) (bounceConst2*Math.Pow(value - 2.25f/bounceConst1, 2) + .9375);
+
+ //small bounce
+ return 1f - (float) (bounceConst2*Math.Pow(value - 2.625f/bounceConst1, 2) + .984375);
+ }
+
+
+ private static float Out(float value, Func<float, float> function)
+ {
+ return 1 - function(1 - value);
+ }
+
+ private static float InOut(float value, Func<float, float> function)
+ {
+ if (value < 0.5f)
+ return 0.5f*function(value*2);
+
+ return 1f - 0.5f*function(2 - value*2);
+ }
+
+ private static class Power
+ {
+ public static float In(double value, int power)
+ {
+ return (float) Math.Pow(value, power);
+ }
+
+ public static float Out(double value, int power)
+ {
+ var sign = power%2 == 0 ? -1 : 1;
+ return (float) (sign*(Math.Pow(value - 1, power) + sign));
+ }
+
+ public static float InOut(double s, int power)
+ {
+ s *= 2;
+
+ if (s < 1)
+ return In(s, power)/2;
+
+ var sign = power%2 == 0 ? -1 : 1;
+ return (float) (sign/2.0*(Math.Pow(s - 2, power) + sign*2));
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearOperations.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearOperations.cs
new file mode 100644
index 0000000..12a03a8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearOperations.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Linq.Expressions;
+
+namespace MonoGame.Extended.Tweening;
+
+public class LinearOperations<T>
+{
+ static LinearOperations()
+ {
+ var a = Expression.Parameter(typeof(T));
+ var b = Expression.Parameter(typeof(T));
+ var c = Expression.Parameter(typeof(float));
+ Add = Expression.Lambda<Func<T, T, T>>(Expression.Add(a, b), a, b).Compile();
+ Subtract = Expression.Lambda<Func<T, T, T>>(Expression.Subtract(a, b), a, b).Compile();
+ Multiply = Expression.Lambda<Func<T, float, T>>(Expression.Multiply(a, c), a, c).Compile();
+ }
+
+ public static Func<T, T, T> Add { get; }
+ public static Func<T, T, T> Subtract { get; }
+ public static Func<T, float, T> Multiply { get; }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearTween.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearTween.cs
new file mode 100644
index 0000000..8469a13
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearTween.cs
@@ -0,0 +1,23 @@
+namespace MonoGame.Extended.Tweening;
+
+public class LinearTween<T>: Tween<T>
+ where T: struct
+{
+ private T _range;
+
+ internal LinearTween(object target, float duration, float delay, TweenMember<T> member, T endValue) : base(target, duration, delay, member, endValue)
+ {
+ }
+
+ protected override void Initialize()
+ {
+ base.Initialize();
+ _range = LinearOperations<T>.Subtract(_endValue, _startValue);
+ }
+
+ protected override void Interpolate(float n)
+ {
+ var value = LinearOperations<T>.Add(_startValue, LinearOperations<T>.Multiply(_range, n));
+ Member.Value = value;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/MonoGame.Extended.Tweening.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/MonoGame.Extended.Tweening.csproj
new file mode 100644
index 0000000..8395e42
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/MonoGame.Extended.Tweening.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>A tweening system to make MonoGame more awesome.</Description>
+ <PackageTags>monogame animations tweening</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tween.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tween.cs
new file mode 100644
index 0000000..ad29251
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tween.cs
@@ -0,0 +1,184 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tweening
+{
+ public abstract class Tween<T> : Tween
+ where T : struct
+ {
+ internal Tween(object target, float duration, float delay, TweenMember<T> member, T endValue)
+ : base(target, duration, delay)
+ {
+ Member = member;
+ _endValue = endValue;
+ }
+
+ public TweenMember<T> Member { get; }
+ public override string MemberName => Member.Name;
+
+ protected T _startValue;
+ protected T _endValue;
+
+ protected override void Initialize()
+ {
+ _startValue = Member.Value;
+ }
+
+ protected override void Swap()
+ {
+ _endValue = _startValue;
+ Initialize();
+ }
+ }
+
+ public abstract class Tween
+ {
+ internal Tween(object target, float duration, float delay)
+ {
+ Target = target;
+ Duration = duration;
+ Delay = delay;
+ IsAlive = true;
+
+ _remainingDelay = delay;
+ }
+
+ public object Target { get; }
+ public abstract string MemberName { get; }
+ public float Duration { get; }
+ public float Delay { get; }
+ public bool IsPaused { get; set; }
+ public bool IsRepeating => _remainingRepeats != 0;
+ public bool IsRepeatingForever => _remainingRepeats < 0;
+ public bool IsAutoReverse { get; private set; }
+ public bool IsAlive { get; private set; }
+ public bool IsComplete { get; private set; }
+ public float TimeRemaining => Duration - _elapsedDuration;
+ public float Completion => MathHelper.Clamp(_completion, 0, 1);
+
+ private Func<float, float> _easingFunction;
+ private bool _isInitialized;
+ private float _completion;
+ private float _elapsedDuration;
+ private float _remainingDelay;
+ private float _repeatDelay;
+ private int _remainingRepeats;
+ private Action<Tween> _onBegin;
+ private Action<Tween> _onEnd;
+
+ public Tween Easing(Func<float, float> easingFunction) { _easingFunction = easingFunction; return this; }
+ public Tween OnBegin(Action<Tween> action) { _onBegin = action; return this; }
+ public Tween OnEnd(Action<Tween> action) { _onEnd = action; return this; }
+ public Tween Pause() { IsPaused = true; return this; }
+ public Tween Resume() { IsPaused = false; return this; }
+
+ public Tween Repeat(int count, float repeatDelay = 0f)
+ {
+ _remainingRepeats = count;
+ _repeatDelay = repeatDelay;
+ return this;
+ }
+
+ public Tween RepeatForever(float repeatDelay = 0f)
+ {
+ _remainingRepeats = -1;
+ _repeatDelay = repeatDelay;
+ return this;
+ }
+
+ public Tween AutoReverse()
+ {
+ if (_remainingRepeats == 0)
+ _remainingRepeats = 1;
+
+ IsAutoReverse = true;
+ return this;
+ }
+
+ protected abstract void Initialize();
+ protected abstract void Interpolate(float n);
+ protected abstract void Swap();
+
+ public void Cancel()
+ {
+ _remainingRepeats = 0;
+ IsAlive = false;
+ }
+
+ public void CancelAndComplete()
+ {
+ if (IsAlive)
+ {
+ _completion = 1;
+
+ Interpolate(1);
+ IsComplete = true;
+ _onEnd?.Invoke(this);
+ }
+
+ Cancel();
+ }
+
+ public void Update(float elapsedSeconds)
+ {
+ if(IsPaused || !IsAlive)
+ return;
+
+ if (_remainingDelay > 0)
+ {
+ _remainingDelay -= elapsedSeconds;
+
+ if (_remainingDelay > 0)
+ return;
+ }
+
+ if (!_isInitialized)
+ {
+ _isInitialized = true;
+ Initialize();
+ _onBegin?.Invoke(this);
+ }
+
+ if (IsComplete)
+ {
+ IsComplete = false;
+ _elapsedDuration = 0;
+ _onBegin?.Invoke(this);
+
+ if (IsAutoReverse)
+ Swap();
+ }
+
+ _elapsedDuration += elapsedSeconds;
+
+ var n = _completion = _elapsedDuration / Duration;
+
+ if (_easingFunction != null)
+ n = _easingFunction(n);
+
+ if (_elapsedDuration >= Duration)
+ {
+ if (_remainingRepeats != 0)
+ {
+ if(_remainingRepeats > 0)
+ _remainingRepeats--;
+
+ _remainingDelay = _repeatDelay;
+ }
+ else if (_remainingRepeats == 0)
+ {
+ IsAlive = false;
+ }
+
+ n = _completion = 1;
+ IsComplete = true;
+ }
+
+ Interpolate(n);
+
+ if (IsComplete)
+ _onEnd?.Invoke(this);
+ }
+
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenFieldMember.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenFieldMember.cs
new file mode 100644
index 0000000..da2f75d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenFieldMember.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Diagnostics;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace MonoGame.Extended.Tweening
+{
+ public sealed class TweenFieldMember<T> : TweenMember<T>
+ where T : struct
+ {
+ private readonly FieldInfo _fieldInfo;
+
+ public TweenFieldMember(object target, FieldInfo fieldInfo)
+ : base(target, CompileGetMethod(fieldInfo), CompileSetMethod(fieldInfo))
+ {
+ _fieldInfo = fieldInfo;
+ }
+
+ private static Func<object, object> CompileGetMethod(FieldInfo fieldInfo)
+ {
+ var self = Expression.Parameter(typeof(object));
+ var instance = Expression.Convert(self, fieldInfo.DeclaringType);
+ var field = Expression.Field(instance, fieldInfo);
+ var convert = Expression.TypeAs(field, typeof(object));
+
+ return Expression.Lambda<Func<object, object>>(convert, self).Compile();
+ }
+
+ private static Action<object, object> CompileSetMethod(FieldInfo fieldInfo)
+ {
+ Debug.Assert(fieldInfo.DeclaringType != null);
+
+ var self = Expression.Parameter(typeof(object));
+ var value = Expression.Parameter(typeof(object));
+ var fieldExp = Expression.Field(Expression.Convert(self, fieldInfo.DeclaringType), fieldInfo);
+ var assignExp = Expression.Assign(fieldExp, Expression.Convert(value, fieldInfo.FieldType));
+
+ return Expression.Lambda<Action<object, object>>(assignExp, self, value).Compile();
+ }
+
+ public override Type Type => _fieldInfo.FieldType;
+ public override string Name => _fieldInfo.Name;
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenMember.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenMember.cs
new file mode 100644
index 0000000..adcee59
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenMember.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Linq.Expressions;
+
+namespace MonoGame.Extended.Tweening
+{
+ public abstract class TweenMember
+ {
+ protected TweenMember(object target)
+ {
+ Target = target;
+ }
+
+ public object Target { get; }
+ public abstract Type Type { get; }
+ public abstract string Name { get; }
+ }
+
+ public abstract class TweenMember<T> : TweenMember
+ where T : struct
+ {
+ protected TweenMember(object target, Func<object, object> getMethod, Action<object, object> setMethod)
+ : base(target)
+ {
+ _getMethod = getMethod;
+ _setMethod = setMethod;
+ }
+
+ private readonly Func<object, object> _getMethod;
+ private readonly Action<object, object> _setMethod;
+
+ public T Value
+ {
+ get { return (T) _getMethod(Target); }
+ set { _setMethod(Target, value); }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenPropertyMember.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenPropertyMember.cs
new file mode 100644
index 0000000..7b1db71
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenPropertyMember.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Diagnostics;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace MonoGame.Extended.Tweening
+{
+ public sealed class TweenPropertyMember<T> : TweenMember<T>
+ where T : struct
+ {
+ private readonly PropertyInfo _propertyInfo;
+
+ public TweenPropertyMember(object target, PropertyInfo propertyInfo)
+ : base(target, CompileGetMethod(propertyInfo), CompileSetMethod(propertyInfo))
+ {
+ _propertyInfo = propertyInfo;
+ }
+
+ public override Type Type => _propertyInfo.PropertyType;
+ public override string Name => _propertyInfo.Name;
+
+ private static Func<object, object> CompileGetMethod(PropertyInfo propertyInfo)
+ {
+ var param = Expression.Parameter(typeof(object));
+ var instance = Expression.Convert(param, propertyInfo.DeclaringType);
+ var convert = Expression.TypeAs(Expression.Property(instance, propertyInfo), typeof(object));
+ return Expression.Lambda<Func<object, object>>(convert, param).Compile();
+ }
+
+ private static Action<object, object> CompileSetMethod(PropertyInfo propertyInfo)
+ {
+ Debug.Assert(propertyInfo.DeclaringType != null);
+
+ var param = Expression.Parameter(typeof(object));
+ var argument = Expression.Parameter(typeof(object));
+ var expression = Expression.Convert(param, propertyInfo.DeclaringType);
+ var methodInfo = propertyInfo.SetMethod;
+ var arguments = Expression.Convert(argument, propertyInfo.PropertyType);
+ var setterCall = Expression.Call(expression, methodInfo, arguments);
+ return Expression.Lambda<Action<object, object>>(setterCall, param, argument).Compile();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tweener.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tweener.cs
new file mode 100644
index 0000000..61b55f9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tweener.cs
@@ -0,0 +1,133 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tweening
+{
+ public class Tweener : IDisposable
+ {
+ public Tweener()
+ {
+ }
+
+ public void Dispose()
+ {
+ CancelAll();
+ _activeTweens.Clear();
+ _memberCache.Clear();
+ }
+
+ public long AllocationCount { get; private set; }
+
+ private readonly List<Tween> _activeTweens = new List<Tween>();
+
+ public Tween<TMember> TweenTo<TTarget, TMember>(TTarget target, Expression<Func<TTarget, TMember>> expression, TMember toValue, float duration, float delay = 0f)
+ where TTarget : class
+ where TMember : struct
+ {
+ switch (toValue)
+ {
+ case Color toValueColor:
+ return (Tween<TMember>)(object)TweenTo<TTarget, Color, ColorTween>(target, expression as Expression<Func<TTarget, Color>>, toValueColor, duration, delay);
+ default:
+ return TweenTo<TTarget, TMember, LinearTween<TMember>>(target, expression, toValue, duration, delay);
+ }
+
+ }
+
+ public Tween<TMember> TweenTo<TTarget, TMember, TTween>(TTarget target, Expression<Func<TTarget, TMember>> expression, TMember toValue, float duration, float delay = 0f)
+ where TTarget : class
+ where TMember : struct
+ where TTween : Tween<TMember>
+ {
+ var memberExpression = (MemberExpression)expression.Body;
+ var memberInfo = memberExpression.Member;
+ var member = GetMember<TMember>(target, memberInfo.Name);
+ var activeTween = FindTween(target, member.Name);
+
+ activeTween?.Cancel();
+
+ AllocationCount++;
+ var tween = (TTween)Activator.CreateInstance(typeof(TTween),
+ BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null,
+ new object[]{target, duration, delay, member, toValue}, null);
+ _activeTweens.Add(tween);
+ return tween;
+ }
+
+ public void Update(float elapsedSeconds)
+ {
+ for (var i = _activeTweens.Count - 1; i >= 0; i--)
+ {
+ var tween = _activeTweens[i];
+
+ tween.Update(elapsedSeconds);
+
+ if (!tween.IsAlive)
+ _activeTweens.RemoveAt(i);
+ }
+ }
+
+ public Tween FindTween(object target, string memberName)
+ {
+ return _activeTweens.FirstOrDefault(t => t.Target == target && t.MemberName == memberName);
+ }
+
+ public void CancelAll()
+ {
+ foreach (var tween in _activeTweens)
+ tween.Cancel();
+ }
+
+ public void CancelAndCompleteAll()
+ {
+ foreach (var tween in _activeTweens)
+ tween.CancelAndComplete();
+ }
+
+ private struct TweenMemberKey
+ {
+#pragma warning disable 414
+ public object Target;
+ public string MemberName;
+#pragma warning restore 414
+ }
+
+ private readonly Dictionary<TweenMemberKey, TweenMember> _memberCache = new Dictionary<TweenMemberKey, TweenMember>();
+
+ private TweenMember<T> GetMember<T>(object target, string memberName)
+ where T : struct
+ {
+ var key = new TweenMemberKey { Target = target, MemberName = memberName };
+
+ if (_memberCache.TryGetValue(key, out var member))
+ return (TweenMember<T>) member;
+
+ member = CreateMember<T>(target, memberName);
+ _memberCache.Add(key, member);
+ return (TweenMember<T>) member;
+ }
+
+ private TweenMember<T> CreateMember<T>(object target, string memberName)
+ where T : struct
+ {
+ AllocationCount++;
+
+ var type = target.GetType();
+ var property = type.GetTypeInfo().GetProperty(memberName);
+
+ if (property != null)
+ return new TweenPropertyMember<T>(target, property);
+
+ var field = type.GetTypeInfo().GetField(memberName);
+
+ if (field != null)
+ return new TweenFieldMember<T>(target, field);
+
+ throw new InvalidOperationException($"'{memberName}' is not a property or field of the target");
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/AnimationComponent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/AnimationComponent.cs
new file mode 100644
index 0000000..28a0e57
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/AnimationComponent.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Sprites;
+
+namespace MonoGame.Extended.Animations
+{
+ public class AnimationComponent : GameComponent
+ {
+ public AnimationComponent(Game game)
+ : base(game)
+ {
+ Animations = new List<Animation>();
+ }
+
+ public List<Animation> Animations { get; }
+
+ public override void Update(GameTime gameTime)
+ {
+ base.Update(gameTime);
+
+ for (var i = Animations.Count - 1; i >= 0; i--)
+ {
+ var animation = Animations[i];
+ animation.Update(gameTime);
+ }
+
+ Animations.RemoveAll(a => a.IsDisposed);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFont.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFont.cs
new file mode 100644
index 0000000..7aaca08
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFont.cs
@@ -0,0 +1,354 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.BitmapFonts
+{
+ public class BitmapFont
+ {
+ private readonly Dictionary<int, BitmapFontRegion> _characterMap = new Dictionary<int, BitmapFontRegion>();
+
+ public BitmapFont(string name, IEnumerable<BitmapFontRegion> regions, int lineHeight)
+ {
+ foreach (var region in regions)
+ _characterMap.Add(region.Character, region);
+
+ Name = name;
+ LineHeight = lineHeight;
+ }
+
+ public string Name { get; }
+ public int LineHeight { get; }
+ public int LetterSpacing { get; set; }
+ public static bool UseKernings { get; set; } = true;
+
+ public BitmapFontRegion GetCharacterRegion(int character)
+ {
+ return _characterMap.TryGetValue(character, out var region) ? region : null;
+ }
+
+ public Size2 MeasureString(string text)
+ {
+ if (string.IsNullOrEmpty(text))
+ return Size2.Empty;
+
+ var stringRectangle = GetStringRectangle(text);
+ return new Size2(stringRectangle.Width, stringRectangle.Height);
+ }
+
+ public Size2 MeasureString(StringBuilder text)
+ {
+ if (text == null || text.Length == 0)
+ return Size2.Empty;
+
+ var stringRectangle = GetStringRectangle(text);
+ return new Size2(stringRectangle.Width, stringRectangle.Height);
+ }
+
+ public RectangleF GetStringRectangle(string text)
+ {
+ return GetStringRectangle(text, Point2.Zero);
+ }
+
+ public RectangleF GetStringRectangle(string text, Point2 position)
+ {
+ var glyphs = GetGlyphs(text, position);
+ var rectangle = new RectangleF(position.X, position.Y, 0, LineHeight);
+
+ foreach (var glyph in glyphs)
+ {
+ if (glyph.FontRegion != null)
+ {
+ var right = glyph.Position.X + glyph.FontRegion.Width;
+
+ if (right > rectangle.Right)
+ rectangle.Width = (int)(right - rectangle.Left);
+ }
+
+ if (glyph.Character == '\n')
+ rectangle.Height += LineHeight;
+ }
+
+ return rectangle;
+ }
+
+ public RectangleF GetStringRectangle(StringBuilder text, Point2? position = null)
+ {
+ var position1 = position ?? new Point2();
+ var glyphs = GetGlyphs(text, position1);
+ var rectangle = new RectangleF(position1.X, position1.Y, 0, LineHeight);
+
+ foreach (var glyph in glyphs)
+ {
+ if (glyph.FontRegion != null)
+ {
+ var right = glyph.Position.X + glyph.FontRegion.Width;
+
+ if (right > rectangle.Right)
+ rectangle.Width = (int)(right - rectangle.Left);
+ }
+
+ if (glyph.Character == '\n')
+ rectangle.Height += LineHeight;
+ }
+
+ return rectangle;
+ }
+
+ public StringGlyphEnumerable GetGlyphs(string text, Point2? position = null)
+ {
+ return new StringGlyphEnumerable(this, text, position);
+ }
+
+ public StringBuilderGlyphEnumerable GetGlyphs(StringBuilder text, Point2? position)
+ {
+ return new StringBuilderGlyphEnumerable(this, text, position);
+ }
+
+ public override string ToString()
+ {
+ return $"{Name}";
+ }
+
+ public struct StringGlyphEnumerable : IEnumerable<BitmapFontGlyph>
+ {
+ private readonly StringGlyphEnumerator _enumerator;
+
+ public StringGlyphEnumerable(BitmapFont font, string text, Point2? position)
+ {
+ _enumerator = new StringGlyphEnumerator(font, text, position);
+ }
+
+ public StringGlyphEnumerator GetEnumerator()
+ {
+ return _enumerator;
+ }
+
+ IEnumerator<BitmapFontGlyph> IEnumerable<BitmapFontGlyph>.GetEnumerator()
+ {
+ return _enumerator;
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _enumerator;
+ }
+ }
+
+ public struct StringGlyphEnumerator : IEnumerator<BitmapFontGlyph>
+ {
+ private readonly BitmapFont _font;
+ private readonly string _text;
+ private int _index;
+ private readonly Point2 _position;
+ private Vector2 _positionDelta;
+ private BitmapFontGlyph _currentGlyph;
+ private BitmapFontGlyph? _previousGlyph;
+
+ object IEnumerator.Current
+ {
+ get
+ {
+ // casting a struct to object will box it, behaviour we want to avoid...
+ throw new InvalidOperationException();
+ }
+ }
+
+ public BitmapFontGlyph Current => _currentGlyph;
+
+ public StringGlyphEnumerator(BitmapFont font, string text, Point2? position)
+ {
+ _font = font;
+ _text = text;
+ _index = -1;
+ _position = position ?? new Point2();
+ _positionDelta = new Vector2();
+ _currentGlyph = new BitmapFontGlyph();
+ _previousGlyph = null;
+ }
+
+ public bool MoveNext()
+ {
+ if (++_index >= _text.Length)
+ return false;
+
+ var character = GetUnicodeCodePoint(_text, ref _index);
+ _currentGlyph.Character = character;
+ _currentGlyph.FontRegion = _font.GetCharacterRegion(character);
+ _currentGlyph.Position = _position + _positionDelta;
+
+ if (_currentGlyph.FontRegion != null)
+ {
+ _currentGlyph.Position.X += _currentGlyph.FontRegion.XOffset;
+ _currentGlyph.Position.Y += _currentGlyph.FontRegion.YOffset;
+ _positionDelta.X += _currentGlyph.FontRegion.XAdvance + _font.LetterSpacing;
+ }
+
+ if (UseKernings && _previousGlyph?.FontRegion != null)
+ {
+ if (_previousGlyph.Value.FontRegion.Kernings.TryGetValue(character, out var amount))
+ {
+ _positionDelta.X += amount;
+ _currentGlyph.Position.X += amount;
+ }
+ }
+
+ _previousGlyph = _currentGlyph;
+
+ if (character != '\n')
+ return true;
+
+ _positionDelta.Y += _font.LineHeight;
+ _positionDelta.X = 0;
+ _previousGlyph = null;
+
+ return true;
+ }
+
+ private static int GetUnicodeCodePoint(string text, ref int index)
+ {
+ return char.IsHighSurrogate(text[index]) && ++index < text.Length
+ ? char.ConvertToUtf32(text[index - 1], text[index])
+ : text[index];
+ }
+
+ public void Dispose()
+ {
+ }
+
+ public void Reset()
+ {
+ _positionDelta = new Point2();
+ _index = -1;
+ _previousGlyph = null;
+ }
+ }
+
+ public struct StringBuilderGlyphEnumerable : IEnumerable<BitmapFontGlyph>
+ {
+ private readonly StringBuilderGlyphEnumerator _enumerator;
+
+ public StringBuilderGlyphEnumerable(BitmapFont font, StringBuilder text, Point2? position)
+ {
+ _enumerator = new StringBuilderGlyphEnumerator(font, text, position);
+ }
+
+ public StringBuilderGlyphEnumerator GetEnumerator()
+ {
+ return _enumerator;
+ }
+
+ IEnumerator<BitmapFontGlyph> IEnumerable<BitmapFontGlyph>.GetEnumerator()
+ {
+ return _enumerator;
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _enumerator;
+ }
+ }
+
+ public struct StringBuilderGlyphEnumerator : IEnumerator<BitmapFontGlyph>
+ {
+ private readonly BitmapFont _font;
+ private readonly StringBuilder _text;
+ private int _index;
+ private readonly Point2 _position;
+ private Vector2 _positionDelta;
+ private BitmapFontGlyph _currentGlyph;
+ private BitmapFontGlyph? _previousGlyph;
+
+ object IEnumerator.Current
+ {
+ get
+ {
+ // casting a struct to object will box it, behaviour we want to avoid...
+ throw new InvalidOperationException();
+ }
+ }
+
+ public BitmapFontGlyph Current => _currentGlyph;
+
+ public StringBuilderGlyphEnumerator(BitmapFont font, StringBuilder text, Point2? position)
+ {
+ _font = font;
+ _text = text;
+ _index = -1;
+ _position = position ?? new Point2();
+ _positionDelta = new Vector2();
+ _currentGlyph = new BitmapFontGlyph();
+ _previousGlyph = null;
+ }
+
+ public bool MoveNext()
+ {
+ if (++_index >= _text.Length)
+ return false;
+
+ var character = GetUnicodeCodePoint(_text, ref _index);
+ _currentGlyph = new BitmapFontGlyph
+ {
+ Character = character,
+ FontRegion = _font.GetCharacterRegion(character),
+ Position = _position + _positionDelta
+ };
+
+ if (_currentGlyph.FontRegion != null)
+ {
+ _currentGlyph.Position.X += _currentGlyph.FontRegion.XOffset;
+ _currentGlyph.Position.Y += _currentGlyph.FontRegion.YOffset;
+ _positionDelta.X += _currentGlyph.FontRegion.XAdvance + _font.LetterSpacing;
+ }
+
+ if (UseKernings && _previousGlyph.HasValue && _previousGlyph.Value.FontRegion != null)
+ {
+ int amount;
+ if (_previousGlyph.Value.FontRegion.Kernings.TryGetValue(character, out amount))
+ {
+ _positionDelta.X += amount;
+ _currentGlyph.Position.X += amount;
+ }
+ }
+
+ _previousGlyph = _currentGlyph;
+
+ if (character != '\n')
+ return true;
+
+ _positionDelta.Y += _font.LineHeight;
+ _positionDelta.X = _position.X;
+ _previousGlyph = null;
+
+ return true;
+ }
+
+ private static int GetUnicodeCodePoint(StringBuilder text, ref int index)
+ {
+ return char.IsHighSurrogate(text[index]) && ++index < text.Length
+ ? char.ConvertToUtf32(text[index - 1], text[index])
+ : text[index];
+ }
+
+ public void Dispose()
+ {
+ }
+
+ public void Reset()
+ {
+ _positionDelta = new Point2();
+ _index = -1;
+ _previousGlyph = null;
+ }
+ }
+ }
+
+ public struct BitmapFontGlyph
+ {
+ public int Character;
+ public Vector2 Position;
+ public BitmapFontRegion FontRegion;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontExtensions.cs
new file mode 100644
index 0000000..da072c5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontExtensions.cs
@@ -0,0 +1,156 @@
+using System;
+using System.Text;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.BitmapFonts
+{
+ public static class BitmapFontExtensions
+ {
+ /// <summary>
+ /// Adds a string to a batch of sprites for rendering using the specified font, text, position, color, rotation,
+ /// origin, scale, effects and layer.
+ /// </summary>
+ /// <param name="spriteBatch"></param>
+ /// <param name="bitmapFont">A font for displaying text.</param>
+ /// <param name="text">The text message to display.</param>
+ /// <param name="position">The location (in screen coordinates) to draw the text.</param>
+ /// <param name="color">
+ /// The <see cref="Color" /> to tint a sprite. Use <see cref="Color.White" /> for full color with no
+ /// tinting.
+ /// </param>
+ /// <param name="rotation">Specifies the angle (in radians) to rotate the text about its origin.</param>
+ /// <param name="origin">The origin for each letter; the default is (0,0) which represents the upper-left corner.</param>
+ /// <param name="scale">Scale factor.</param>
+ /// <param name="effect">Effects to apply.</param>
+ /// <param name="layerDepth">
+ /// The depth of a layer. By default, 0 represents the front layer and 1 represents a back layer.
+ /// Use SpriteSortMode if you want sprites to be sorted during drawing.
+ /// </param>
+ /// <param name="clippingRectangle">Clips the boundaries of the text so that it's not drawn outside the clipping rectangle</param>
+ public static void DrawString(this SpriteBatch spriteBatch, BitmapFont bitmapFont, string text, Vector2 position,
+ Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effect, float layerDepth, Rectangle? clippingRectangle = null)
+ {
+ if (text == null)
+ throw new ArgumentNullException(nameof(text));
+ if (effect != SpriteEffects.None)
+ throw new NotSupportedException($"{effect} is not currently supported for {nameof(BitmapFont)}");
+
+ var glyphs = bitmapFont.GetGlyphs(text, position);
+ foreach (var glyph in glyphs)
+ {
+ if (glyph.FontRegion == null)
+ continue;
+ var characterOrigin = position - glyph.Position + origin;
+ spriteBatch.Draw(glyph.FontRegion.TextureRegion, position, color, rotation, characterOrigin, scale, effect, layerDepth, clippingRectangle);
+ }
+ }
+
+ public static void DrawString(this SpriteBatch spriteBatch, BitmapFont bitmapFont, StringBuilder text, Vector2 position,
+ Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effect, float layerDepth, Rectangle? clippingRectangle = null)
+ {
+ if (text == null)
+ throw new ArgumentNullException(nameof(text));
+ if (effect != SpriteEffects.None)
+ throw new NotSupportedException($"{effect} is not currently supported for {nameof(BitmapFont)}");
+
+ var glyphs = bitmapFont.GetGlyphs(text, position);
+ foreach (var glyph in glyphs)
+ {
+ if (glyph.FontRegion == null)
+ continue;
+ var characterOrigin = position - glyph.Position + origin;
+ spriteBatch.Draw(glyph.FontRegion.TextureRegion, position, color, rotation, characterOrigin, scale, effect, layerDepth, clippingRectangle);
+ }
+ }
+
+ /// <summary>
+ /// Adds a string to a batch of sprites for rendering using the specified font, text, position, color, rotation,
+ /// origin, scale, effects and layer.
+ /// </summary>
+ /// <param name="spriteBatch"></param>
+ /// <param name="font">A font for displaying text.</param>
+ /// <param name="text">The text message to display.</param>
+ /// <param name="position">The location (in screen coordinates) to draw the text.</param>
+ /// <param name="color">
+ /// The <see cref="Color" /> to tint a sprite. Use <see cref="Color.White" /> for full color with no
+ /// tinting.
+ /// </param>
+ /// <param name="rotation">Specifies the angle (in radians) to rotate the text about its origin.</param>
+ /// <param name="origin">The origin for each letter; the default is (0,0) which represents the upper-left corner.</param>
+ /// <param name="scale">Scale factor.</param>
+ /// <param name="effect">Effects to apply.</param>
+ /// <param name="layerDepth">
+ /// The depth of a layer. By default, 0 represents the front layer and 1 represents a back layer.
+ /// Use SpriteSortMode if you want sprites to be sorted during drawing.
+ /// </param>
+ /// <param name="clippingRectangle">Clips the boundaries of the text so that it's not drawn outside the clipping rectangle</param>
+ public static void DrawString(this SpriteBatch spriteBatch, BitmapFont font, string text, Vector2 position,
+ Color color, float rotation, Vector2 origin, float scale, SpriteEffects effect, float layerDepth, Rectangle? clippingRectangle = null)
+ {
+ DrawString(spriteBatch, font, text, position, color, rotation, origin, new Vector2(scale, scale), effect, layerDepth, clippingRectangle);
+ }
+
+ public static void DrawString(this SpriteBatch spriteBatch, BitmapFont font, StringBuilder text, Vector2 position,
+ Color color, float rotation, Vector2 origin, float scale, SpriteEffects effect, float layerDepth, Rectangle? clippingRectangle = null)
+ {
+ DrawString(spriteBatch, font, text, position, color, rotation, origin, new Vector2(scale, scale), effect, layerDepth, clippingRectangle);
+ }
+
+ /// <summary>
+ /// Adds a string to a batch of sprites for rendering using the specified font, text, position, color, layer,
+ /// and width (in pixels) where to wrap the text at.
+ /// </summary>
+ /// <remarks>
+ /// <see cref="BitmapFont" /> objects are loaded from the Content Manager. See the <see cref="BitmapFont" /> class for
+ /// more information.
+ /// Before any calls to this method you must call <see cref="SpriteBatch.Begin" />. Once all calls
+ /// are complete, call <see cref="SpriteBatch.End" />.
+ /// Use a newline character (\n) to draw more than one line of text.
+ /// </remarks>
+ /// <param name="spriteBatch"></param>
+ /// <param name="font">A font for displaying text.</param>
+ /// <param name="text">The text message to display.</param>
+ /// <param name="position">The location (in screen coordinates) to draw the text.</param>
+ /// <param name="color">
+ /// The <see cref="Color" /> to tint a sprite. Use <see cref="Color.White" /> for full color with no
+ /// tinting.
+ /// </param>
+ /// <param name="layerDepth">
+ /// The depth of a layer. By default, 0 represents the front layer and 1 represents a back layer.
+ /// Use SpriteSortMode if you want sprites to be sorted during drawing.
+ /// </param>
+ /// <param name="clippingRectangle">Clips the boundaries of the text so that it's not drawn outside the clipping rectangle</param>
+ public static void DrawString(this SpriteBatch spriteBatch, BitmapFont font, string text, Vector2 position, Color color, float layerDepth, Rectangle? clippingRectangle = null)
+ {
+ DrawString(spriteBatch, font, text, position, color, rotation: 0, origin: Vector2.Zero, scale: Vector2.One, effect: SpriteEffects.None,
+ layerDepth: layerDepth, clippingRectangle: clippingRectangle);
+ }
+
+ /// <summary>
+ /// Adds a string to a batch of sprites for rendering using the specified font, text, position, color,
+ /// and width (in pixels) where to wrap the text at. The text is drawn on layer 0f.
+ /// </summary>
+ /// <param name="spriteBatch"></param>
+ /// <param name="font">A font for displaying text.</param>
+ /// <param name="text">The text message to display.</param>
+ /// <param name="position">The location (in screen coordinates) to draw the text.</param>
+ /// <param name="color">
+ /// The <see cref="Color" /> to tint a sprite. Use <see cref="Color.White" /> for full color with no
+ /// tinting.
+ /// </param>
+ /// <param name="clippingRectangle">Clips the boundaries of the text so that it's not drawn outside the clipping rectangle</param>
+ public static void DrawString(this SpriteBatch spriteBatch, BitmapFont font, string text, Vector2 position, Color color, Rectangle? clippingRectangle = null)
+ {
+ DrawString(spriteBatch, font, text, position, color, rotation: 0, origin: Vector2.Zero, scale: Vector2.One, effect: SpriteEffects.None,
+ layerDepth: 0, clippingRectangle: clippingRectangle);
+ }
+
+ public static void DrawString(this SpriteBatch spriteBatch, BitmapFont font, StringBuilder text, Vector2 position, Color color, Rectangle? clippingRectangle = null)
+ {
+ DrawString(spriteBatch, font, text, position, color, rotation: 0, origin: Vector2.Zero, scale: Vector2.One, effect: SpriteEffects.None,
+ layerDepth: 0, clippingRectangle: clippingRectangle);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontReader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontReader.cs
new file mode 100644
index 0000000..c6d69fc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontReader.cs
@@ -0,0 +1,65 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.Content;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.BitmapFonts
+{
+ public class BitmapFontReader : ContentTypeReader<BitmapFont>
+ {
+ protected override BitmapFont Read(ContentReader reader, BitmapFont existingInstance)
+ {
+ var textureAssetCount = reader.ReadInt32();
+ var assets = new List<string>();
+
+ for (var i = 0; i < textureAssetCount; i++)
+ {
+ var assetName = reader.ReadString();
+ assets.Add(assetName);
+ }
+
+ var textures = assets
+ .Select(textureName => reader.ContentManager.Load<Texture2D>(reader.GetRelativeAssetName(textureName)))
+ .ToArray();
+
+ var lineHeight = reader.ReadInt32();
+ var regionCount = reader.ReadInt32();
+ var regions = new BitmapFontRegion[regionCount];
+
+ for (var r = 0; r < regionCount; r++)
+ {
+ var character = reader.ReadInt32();
+ var textureIndex = reader.ReadInt32();
+ var x = reader.ReadInt32();
+ var y = reader.ReadInt32();
+ var width = reader.ReadInt32();
+ var height = reader.ReadInt32();
+ var xOffset = reader.ReadInt32();
+ var yOffset = reader.ReadInt32();
+ var xAdvance = reader.ReadInt32();
+ var textureRegion = new TextureRegion2D(textures[textureIndex], x, y, width, height);
+ regions[r] = new BitmapFontRegion(textureRegion, character, xOffset, yOffset, xAdvance);
+ }
+
+ var characterMap = regions.ToDictionary(r => r.Character);
+ var kerningsCount = reader.ReadInt32();
+
+ for (var k = 0; k < kerningsCount; k++)
+ {
+ var first = reader.ReadInt32();
+ var second = reader.ReadInt32();
+ var amount = reader.ReadInt32();
+
+ // Find region
+ if (!characterMap.TryGetValue(first, out var region))
+ continue;
+
+ region.Kernings[second] = amount;
+ }
+
+ return new BitmapFont(reader.AssetName, regions, lineHeight);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontRegion.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontRegion.cs
new file mode 100644
index 0000000..5254f48
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontRegion.cs
@@ -0,0 +1,33 @@
+using System;
+using MonoGame.Extended.TextureAtlases;
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.BitmapFonts
+{
+ public class BitmapFontRegion
+ {
+ public BitmapFontRegion(TextureRegion2D textureRegion, int character, int xOffset, int yOffset, int xAdvance)
+ {
+ TextureRegion = textureRegion;
+ Character = character;
+ XOffset = xOffset;
+ YOffset = yOffset;
+ XAdvance = xAdvance;
+ Kernings = new Dictionary<int, int>();
+ }
+
+ public int Character { get; }
+ public TextureRegion2D TextureRegion { get; }
+ public int XOffset { get; }
+ public int YOffset { get; }
+ public int XAdvance { get; }
+ public int Width => TextureRegion.Width;
+ public int Height => TextureRegion.Height;
+ public Dictionary<int, int> Kernings { get; }
+
+ public override string ToString()
+ {
+ return $"{Convert.ToChar(Character)} {TextureRegion}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Camera.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Camera.cs
new file mode 100644
index 0000000..617cb69
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Camera.cs
@@ -0,0 +1,32 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public abstract class Camera<T> where T : struct
+ {
+ public abstract T Position { get; set; }
+ public abstract float Rotation { get; set; }
+ public abstract float Zoom { get; set; }
+ public abstract float MinimumZoom { get; set; }
+ public abstract float MaximumZoom { get; set; }
+ public abstract RectangleF BoundingRectangle { get; }
+ public abstract T Origin { get; set; }
+ public abstract T Center { get; }
+
+ public abstract void Move(T direction);
+ public abstract void Rotate(float deltaRadians);
+ public abstract void ZoomIn(float deltaZoom);
+ public abstract void ZoomOut(float deltaZoom);
+ public abstract void LookAt(T position);
+
+ public abstract T WorldToScreen(T worldPosition);
+ public abstract T ScreenToWorld(T screenPosition);
+
+ public abstract Matrix GetViewMatrix();
+ public abstract Matrix GetInverseViewMatrix();
+
+ public abstract BoundingFrustum GetBoundingFrustum();
+ public abstract ContainmentType Contains(Vector2 vector2);
+ public abstract ContainmentType Contains(Rectangle rectangle);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Bag.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Bag.cs
new file mode 100644
index 0000000..868b6d7
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Bag.cs
@@ -0,0 +1,217 @@
+// Original code dervied from:
+// https://github.com/thelinuxlich/artemis_CSharp/blob/master/Artemis_XNA_INDEPENDENT/Utils/Bag.cs
+
+// --------------------------------------------------------------------------------------------------------------------
+// <copyright file="Bag.cs" company="GAMADU.COM">
+// Copyright © 2013 GAMADU.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.
+//
+// THIS SOFTWARE IS PROVIDED BY GAMADU.COM '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 GAMADU.COM 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.
+//
+// The views and conclusions contained in the software and documentation are those of the
+// authors and should not be interpreted as representing official policies, either expressed
+// or implied, of GAMADU.COM.
+// </copyright>
+// <summary>
+// Class Bag.
+// </summary>
+// --------------------------------------------------------------------------------------------------------------------
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Collections
+{
+ public class Bag<T> : IEnumerable<T>
+ {
+ private T[] _items;
+ private readonly bool _isPrimitive;
+
+ public int Capacity => _items.Length;
+ public bool IsEmpty => Count == 0;
+ public int Count { get; private set; }
+
+ public Bag(int capacity = 16)
+ {
+ _isPrimitive = typeof(T).IsPrimitive;
+ _items = new T[capacity];
+ }
+
+ public T this[int index]
+ {
+ get => index >= _items.Length ? default(T) : _items[index];
+ set
+ {
+ EnsureCapacity(index + 1);
+ if (index >= Count)
+ Count = index + 1;
+ _items[index] = value;
+ }
+ }
+
+ public void Add(T element)
+ {
+ EnsureCapacity(Count + 1);
+ _items[Count] = element;
+ ++Count;
+ }
+
+ public void AddRange(Bag<T> range)
+ {
+ for (int index = 0, j = range.Count; j > index; ++index)
+ Add(range[index]);
+ }
+
+ public void Clear()
+ {
+ if(Count == 0)
+ return;
+
+ // non-primitive types are cleared so the garbage collector can release them
+ if (!_isPrimitive)
+ Array.Clear(_items, 0, Count);
+
+ Count = 0;
+ }
+
+ public bool Contains(T element)
+ {
+ for (var index = Count - 1; index >= 0; --index)
+ {
+ if (element.Equals(_items[index]))
+ return true;
+ }
+
+ return false;
+ }
+
+ public T RemoveAt(int index)
+ {
+ var result = _items[index];
+ --Count;
+ _items[index] = _items[Count];
+ _items[Count] = default(T);
+ return result;
+ }
+
+ public bool Remove(T element)
+ {
+ for (var index = Count - 1; index >= 0; --index)
+ {
+ if (element.Equals(_items[index]))
+ {
+ --Count;
+ _items[index] = _items[Count];
+ _items[Count] = default(T);
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public bool RemoveAll(Bag<T> bag)
+ {
+ var isResult = false;
+
+ for (var index = bag.Count - 1; index >= 0; --index)
+ {
+ if (Remove(bag[index]))
+ isResult = true;
+ }
+
+ return isResult;
+ }
+
+ private void EnsureCapacity(int capacity)
+ {
+ if (capacity < _items.Length)
+ return;
+
+ var newCapacity = Math.Max((int)(_items.Length * 1.5), capacity);
+ var oldElements = _items;
+ _items = new T[newCapacity];
+ Array.Copy(oldElements, 0, _items, 0, oldElements.Length);
+ }
+
+ IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ /// <summary>
+ /// Get the <see cref="BagEnumerator"/> for this <see cref="Bag{T}"/>.
+ /// </summary>
+ /// <returns></returns>
+ /// <remarks>
+ /// Use this method preferentially over <see cref="IEnumerable.GetEnumerator"/> while enumerating via foreach
+ /// to avoid boxing the enumerator on every iteration, which can be expensive in high-performance environments.
+ /// </remarks>
+ public BagEnumerator GetEnumerator()
+ {
+ return new BagEnumerator(this);
+ }
+
+ /// <summary>
+ /// Enumerates a Bag.
+ /// </summary>
+ public struct BagEnumerator : IEnumerator<T>
+ {
+ private readonly Bag<T> _bag;
+ private volatile int _index;
+
+ /// <summary>
+ /// Creates a new <see cref="BagEnumerator"/> for this <see cref="Bag{T}"/>.
+ /// </summary>
+ /// <param name="bag"></param>
+ public BagEnumerator(Bag<T> bag)
+ {
+ _bag = bag;
+ _index = -1;
+ }
+
+ readonly T IEnumerator<T>.Current => _bag[_index];
+
+ readonly object IEnumerator.Current => _bag[_index];
+
+ /// <summary>
+ /// Gets the element in the <see cref="Bag{T}"/> at the current position of the enumerator.
+ /// </summary>
+ public readonly T Current => _bag[_index];
+
+ /// <inheritdoc/>
+ public bool MoveNext()
+ {
+ return ++_index < _bag.Count;
+ }
+
+ /// <inheritdoc/>
+ public readonly void Dispose()
+ {
+ }
+
+ /// <inheritdoc/>
+ public readonly void Reset()
+ {
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Deque.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Deque.cs
new file mode 100644
index 0000000..fd59066
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Deque.cs
@@ -0,0 +1,837 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+namespace MonoGame.Extended.Collections
+{
+ internal static class Deque
+ {
+ internal static readonly Func<int, int> DefaultResizeFunction = x => x * 2;
+ }
+
+ /// <summary>
+ /// Represents a collection of objects which elements can added to or removed either from the front or back; a
+ /// <a href="https://en.wikipedia.org/wiki/Double-ended_queue">double ended queue</a> (deque).
+ /// </summary>
+ /// <remarks>
+ /// <a href="https://en.wikipedia.org/wiki/Circular_buffer">circular array</a> is used as the internal data
+ /// structure for the <see cref="Deque{T}" />.
+ /// </remarks>
+ /// <typeparam name="T">The type of the elements in the deque.</typeparam>
+ public class Deque<T> : IList<T>
+ {
+ private const int _defaultCapacity = 4;
+ private static readonly T[] _emptyArray = new T[0];
+ private int _frontArrayIndex;
+ private T[] _items;
+ private Func<int, int> _resizeFunction = Deque.DefaultResizeFunction;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Deque{T}" /> class that is empty and has the default initial capacity.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// The capacity of a <see cref="Deque{T}" /> is the number of elements that the <see cref="Deque{T}" /> can
+ /// hold. As elements are added to a <see cref="Deque{T}" />, <see cref="Capacity" /> is automatically increased by
+ /// <see cref="ResizeFunction" /> as required by reallocating the internal array.
+ /// </para>
+ /// <para>
+ /// If the size of the collection can be estimated, using the <see cref="Deque{T}(int)" /> constructor and
+ /// specifying the initial capacity eliminates the need to perform a number of resizing operations while adding
+ /// elements to the <see cref="Deque{T}" />.
+ /// </para>
+ /// <para>
+ /// The capacity can be decreased by calling the <see cref="TrimExcess" /> method or by setting the
+ /// <see cref="Capacity" /> property explicitly. Decreasing, or increasing, the capacity reallocates memory and
+ /// copies all the
+ /// elements in the <see cref="Deque{T}" />.
+ /// </para>
+ /// <para>This constructor is an O(1) operation.</para>
+ /// </remarks>
+ public Deque()
+ {
+ _items = _emptyArray;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Deque{T}" /> class that contains elements copied from the specified
+ /// collection and has sufficient capacity to accommodate the number of elements copied.
+ /// </summary>
+ /// <param name="collection">The collection whose elements are copied to the new deque.</param>
+ /// <exception cref="ArgumentNullException"><paramref name="collection" /> is null.</exception>
+ /// <remarks>
+ /// <para>
+ /// The elements are copied onto the <see cref="Deque{T}" /> in the same order they are read by the enumerator of
+ /// <paramref name="collection" />.
+ /// </para>
+ /// <para>This constructor is an O(n) operation, where n is the number of elements in <paramref name="collection" />.</para>
+ /// </remarks>
+ public Deque(IEnumerable<T> collection)
+ {
+ if (collection == null)
+ throw new ArgumentNullException(nameof(collection));
+
+ var array = collection as T[] ?? collection.ToArray();
+ var count = array.Length;
+
+ if (count == 0)
+ _items = _emptyArray;
+ else
+ {
+ _items = new T[count];
+ array.CopyTo(_items, 0);
+ Count = count;
+ }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Deque{T}" /> class that is empty and has the specified initial
+ /// capacity.
+ /// </summary>
+ /// <param name="capacity">The number of elements that the new <see cref="Deque{T}" /> can initially store.</param>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="capacity" /> is less than 0.</exception>
+ /// <remarks>
+ /// <para>
+ /// The capacity of a <see cref="Deque{T}" /> is the number of elements that the <see cref="Deque{T}" /> can
+ /// hold. As elements are added to a <see cref="Deque{T}" />, <see cref="Capacity" /> is automatically increased by
+ /// <see cref="ResizeFunction" /> as required by reallocating the internal array.
+ /// </para>
+ /// <para>
+ /// If the size of the collection can be estimated, specifying the initial capacity eliminates the need to
+ /// perform a number of resizing operations while adding elements to the <see cref="Deque{T}" />.
+ /// </para>
+ /// <para>
+ /// The capacity can be decreased by calling the <see cref="TrimExcess" /> method or by setting the
+ /// <see cref="Capacity" /> property explicitly. Decreasing, or increasing, the capacity reallocates memory and
+ /// copies all the elements in the <see cref="Deque{T}" />.
+ /// </para>
+ /// <para>This constructor is an O(n) operation, where n is <paramref name="capacity" />.</para>
+ /// </remarks>
+ public Deque(int capacity)
+ {
+ if (capacity < 0)
+ throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity was less than zero.");
+
+ _items = capacity == 0 ? _emptyArray : new T[capacity];
+ }
+
+ /// <summary>
+ /// Gets or sets the resize function used to calculate and set <see cref="Capacity" /> when a greater capacity is
+ /// required.
+ /// </summary>
+ /// <returns>
+ /// The <see cref="Func{T, TResult}" /> used to calculate and set <see cref="Capacity" /> when a greater capacity
+ /// is required.
+ /// </returns>
+ /// <remarks>
+ /// The default resize function is twice the <see cref="Capacity" />. Setting
+ /// <see cref="ResizeFunction" /> to <c>null</c> will set it back to the default.
+ /// </remarks>
+ public Func<int, int> ResizeFunction
+ {
+ get => _resizeFunction;
+ set => _resizeFunction = value ?? Deque.DefaultResizeFunction;
+ }
+
+ /// <summary>
+ /// Gets or sets the total number of elements the internal data structure can hold without resizing.
+ /// </summary>
+ /// <returns>The number of elements that the <see cref="Deque{T}" /> can contain before resizing is required.</returns>
+ /// <exception cref="ArgumentOutOfRangeException">
+ /// <see cref="Capacity" /> cannot be set to a value less than <see cref="Count" />.
+ /// </exception>
+ /// <remarks>
+ /// Changing <see cref="Capacity" /> reallocates memory and copies all the
+ /// elements in the <see cref="Deque{T}" />.
+ /// </remarks>
+ public int Capacity
+ {
+ get => _items.Length;
+ set
+ {
+ if (value < Count)
+ throw new ArgumentOutOfRangeException(nameof(value), "capacity was less than the current size.");
+
+ if (value == Capacity)
+ return;
+
+ if (value == 0)
+ {
+ _items = _emptyArray;
+ return;
+ }
+
+ var newItems = new T[value];
+ CopyTo(newItems);
+
+ _frontArrayIndex = 0;
+ _items = null;
+ _items = newItems;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the element at the specified index.
+ /// </summary>
+ /// <param name="index">The zero-based index of the element to get or set.</param>
+ /// <returns>The element at the specified index.</returns>
+ /// <exception cref="ArgumentOutOfRangeException">
+ /// Index was out of range. Must be non-negative and less than <see cref="Count" />.
+ /// </exception>
+ /// <remarks>
+ /// <para></para>
+ /// <para>
+ /// Use <c>0</c> for the <paramref name="index" /> to get or set the element at the beginning of the
+ /// <see cref="Deque{T}" />, and use <c><see cref="Count" /> - 1</c> for the <paramref name="index" /> to get the
+ /// element at the end of the <see cref="Deque{T}" />.
+ /// </para>
+ /// </remarks>
+ public T this[int index]
+ {
+ get
+ {
+ var arrayIndex = GetArrayIndex(index);
+ if (arrayIndex == -1)
+ {
+ throw new ArgumentOutOfRangeException(nameof(index),
+ "Index was out of range. Must be non-negative and less than the size of the collection.");
+ }
+ return _items[arrayIndex];
+ }
+ set
+ {
+ var arrayIndex = GetArrayIndex(index);
+ if (arrayIndex == -1)
+ {
+ throw new ArgumentOutOfRangeException(nameof(index),
+ "Index was out of range. Must be non-negative and less than the size of the collection.");
+ }
+ _items[arrayIndex] = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets the number of elements contained in the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <returns>The number of elements contained in the <see cref="Deque{T}" />.</returns>
+ public int Count { get; private set; }
+
+ bool ICollection<T>.IsReadOnly => false;
+
+ /// <summary>
+ /// Returns an enumerator that iterates through the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <returns>An <see cref="IEnumerator{T}" /> that can be used to iterate through the <see cref="Deque{T}" />.</returns>
+ public IEnumerator<T> GetEnumerator()
+ {
+ if (Count == 0)
+ yield break;
+
+ if (Count <= _items.Length - _frontArrayIndex)
+ {
+ for (var i = _frontArrayIndex; i < _frontArrayIndex + Count; i++)
+ yield return _items[i];
+ }
+ else
+ {
+ for (var i = _frontArrayIndex; i < Capacity; i++)
+ yield return _items[i];
+ for (var i = 0; i < (_frontArrayIndex + Count) % Capacity; i++)
+ yield return _items[i];
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ void ICollection<T>.Add(T item)
+ {
+ AddToBack(item);
+ }
+
+ /// <summary>
+ /// Searches for the specified element and returns the zero-based index of the first occurrence within the entire
+ /// <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="item">
+ /// The element to locate in the <see cref="Deque{T}" />. The value can be <c>null</c> for reference
+ /// types.
+ /// </param>
+ /// <returns>
+ /// The zero-based index of the first occurrence of <paramref name="item" /> within the entire
+ /// <see cref="Deque{T}" />, if found; otherwise, <c>-1</c>.
+ /// </returns>
+ /// <remarks>
+ /// <para>
+ /// This method is an O(1) operation if <paramref name="item" /> is at the front or back of the
+ /// <see cref="Deque{T}" />; otherwise, this method is an O(n) operation where n is <see cref="Count" />.
+ /// </para>
+ /// </remarks>
+ public int IndexOf(T item)
+ {
+ var comparer = EqualityComparer<T>.Default;
+
+ if (Get(0, out var checkFrontBackItem) && comparer.Equals(checkFrontBackItem, item))
+ return 0;
+
+ var backIndex = Count - 1;
+
+ if (Get(backIndex, out checkFrontBackItem) && comparer.Equals(checkFrontBackItem, item))
+ return backIndex;
+
+ int index;
+
+ if (Count <= _items.Length - _frontArrayIndex)
+ index = Array.IndexOf(_items, item, _frontArrayIndex, Count);
+ else
+ {
+ index = Array.IndexOf(_items, item, _frontArrayIndex, _items.Length - _frontArrayIndex);
+ if (index < 0)
+ index = Array.IndexOf(_items, item, 0, _frontArrayIndex + Count - _items.Length);
+ }
+
+ var circularIndex = (index - _frontArrayIndex + _items.Length) % _items.Length;
+ return circularIndex;
+ }
+
+ void IList<T>.Insert(int index, T item)
+ {
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Removes the first occurrence of a specific element from the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="item">
+ /// The element to remove from the <see cref="Deque{T}" />. The value can be <c>null</c> for reference
+ /// types.
+ /// </param>
+ /// <returns>
+ /// <c>true</c> if <paramref name="item" /> was successfully removed; otherwise, false. This method also returns false
+ /// if <paramref name="item" /> is not found in the <see cref="Deque{T}" />.
+ /// </returns>
+ /// <remarks>
+ /// <para>
+ /// This method is an O(1) operation if <paramref name="item" /> is at the front or back of the
+ /// <see cref="Deque{T}" />; otherwise, this method is an O(n) operation where n is <see cref="Count" />.
+ /// </para>
+ /// </remarks>
+ public bool Remove(T item)
+ {
+ var index = IndexOf(item);
+ if (index == -1)
+ return false;
+
+ RemoveAt(index);
+ return true;
+ }
+
+ /// <summary>
+ /// Removes the element at the specified index of the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="index">The zero-based index of the element to remove.</param>
+ /// <exception cref="ArgumentOutOfRangeException">
+ /// <para><paramref name="index" /> is less than 0.</para>
+ /// <para>-or-</para>
+ /// <para><paramref name="index" /> is equal to or greater than <see cref="Count" />.</para>
+ /// </exception>
+ public void RemoveAt(int index)
+ {
+ if (index < 0)
+ throw new ArgumentOutOfRangeException(nameof(index), index, "Index was less than zero.");
+
+ if (index >= Count)
+ throw new ArgumentOutOfRangeException(nameof(index), index, "Index was equal or greater than TotalCount.");
+
+ if (index == 0)
+ {
+ RemoveFromFront();
+ }
+ else
+ {
+ if (index == Count - 1)
+ {
+ RemoveFromBack();
+ }
+ else
+ {
+ if (index < Count / 2)
+ {
+ var arrayIndex = GetArrayIndex(index);
+ // shift the array from 0 to before the index to remove by 1 to the right
+ // the element to remove is replaced by the copy
+ Array.Copy(_items, 0, _items, 1, arrayIndex);
+ // the first element in the arrya is now either a duplicate or it's default value
+ // to be safe set it to it's default value regardless of circumstance
+ _items[0] = default(T);
+ // if we shifted the front element, adjust the front index
+ if (_frontArrayIndex < arrayIndex)
+ _frontArrayIndex = (_frontArrayIndex + 1) % _items.Length;
+ // decrement the count so the back index is calculated correctly
+ Count--;
+ }
+ else
+ {
+ var arrayIndex = GetArrayIndex(index);
+ // shift the array from the center of the array to before the index to remove by 1 to the right
+ // the element to remove is replaced by the copy
+ var arrayCenterIndex = _items.Length / 2;
+ Array.Copy(_items, arrayCenterIndex, _items, arrayCenterIndex + 1, _items.Length - 1 - arrayIndex);
+ // the last element in the array is now either a duplicate or it's default value
+ // to be safe set it to it's default value regardless of circumstance
+ _items[_items.Length - 1] = default(T);
+ // if we shifted the front element, adjust the front index
+ if (_frontArrayIndex < arrayIndex)
+ _frontArrayIndex = (_frontArrayIndex + 1) % _items.Length;
+ // decrement the count so the back index is calculated correctly
+ Count--;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Removes all elements from the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// <see cref="Count" /> is set to <c>0</c>, and references to other objects from elements of the collection are
+ /// also released.
+ /// </para>
+ /// <para>
+ /// <see cref="Capacity" /> remains unchanged. To reset the capacity of the <see cref="Deque{T}" />, call the
+ /// <see cref="TrimExcess" /> method or set the <see cref="Capacity" /> property explictly. Decreasing, or
+ /// increasing, the capacity reallocates memory and copies all the elements in the <see cref="Deque{T}" />.
+ /// Trimming an empty <see cref="Deque{T}" /> sets <see cref="Capacity" /> to the default capacity.
+ /// </para>
+ /// <para>This method is an O(n) operation, where n is <see cref="Count" />.</para>
+ /// </remarks>
+ public void Clear()
+ {
+ // allow the garbage collector to reclaim the references
+
+ if (Count == 0)
+ return;
+
+ if (Count > _items.Length - _frontArrayIndex)
+ {
+ Array.Clear(_items, _frontArrayIndex, _items.Length - _frontArrayIndex);
+ Array.Clear(_items, 0, _frontArrayIndex + Count - _items.Length);
+ }
+ else
+ Array.Clear(_items, _frontArrayIndex, Count);
+ Count = 0;
+ _frontArrayIndex = 0;
+ }
+
+ /// <summary>
+ /// Determines whether an element is in the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="item">
+ /// The element to locate in the <see cref="Deque{T}" />. The value can be <c>null</c> for reference
+ /// types.
+ /// </param>
+ /// <returns><c>true</c> if <paramref name="item" /> is found in the <see cref="Deque{T}" />; otherwise, false.</returns>
+ /// <remarks>
+ /// <para>
+ /// This method determines equality by using the default equality comparer, as defined by the object's
+ /// implementation
+ /// of the <see cref="IEquatable{T}.Equals(T)" /> method for the type of values in the list.
+ /// </para>
+ /// <para>
+ /// This method performs a linear search; therefore, this method is an O(n) operation, where n is
+ /// <see cref="Count" />.
+ /// </para>
+ /// </remarks>
+ public bool Contains(T item)
+ {
+ return this.Contains(item, EqualityComparer<T>.Default);
+ }
+
+ /// <summary>
+ /// Copies the entire <see cref="Deque{T}" /> to a compatible one-dimensional array, starting at the specified index of
+ /// the target array.
+ /// </summary>
+ /// <param name="array">
+ /// The one-dimensional <see cref="Array" /> that is the destination of the elements copied from
+ /// <see cref="Deque{T}" />. The <see cref="Array" /> must have zero-based indexing.
+ /// </param>
+ /// <param name="arrayIndex">The zero-based index in <paramref name="array" /> at which copying begins.</param>
+ /// <exception cref="ArgumentNullException"><paramref name="array" /> is null.</exception>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="arrayIndex" /> is less than 0.</exception>
+ /// <exception cref="ArgumentException">
+ /// The number of elements in the source <see cref="Deque{T}" /> is greater than the
+ /// available space from <paramref name="arrayIndex" /> to the end of the destination <paramref name="array" />.
+ /// </exception>
+ /// <remarks>
+ /// This method uses <see cref="Array.Copy(Array, int, Array, int, int)" /> to copy the elements. The elements are
+ /// copied to the <see cref="Array" /> in the same order in which the enumerator iterates
+ /// through the <see cref="Deque{T}" />. This method is an O(n) operation, where n is <see cref="Count" />.
+ /// </remarks>
+ public void CopyTo(T[] array, int arrayIndex = 0)
+ {
+ if (array == null)
+ throw new ArgumentNullException(nameof(array));
+
+ if (array.Rank != 1)
+ throw new ArgumentException("Only single dimensional arrays are supported for the requested action.");
+
+ if (arrayIndex < 0)
+ throw new ArgumentOutOfRangeException(nameof(arrayIndex), "Index was less than the array's lower bound.");
+
+ if (arrayIndex >= array.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(arrayIndex),
+ "Index was greater than the array's upper bound.");
+ }
+
+ if (array.Length - arrayIndex < Count)
+ throw new ArgumentException("Destination array was not long enough.");
+
+ if (Count == 0)
+ return;
+
+
+ try
+ {
+ var loopsAround = Count > _items.Length - _frontArrayIndex;
+ if (!loopsAround)
+ Array.Copy(_items, _frontArrayIndex, array, arrayIndex, Count);
+ else
+ {
+ Array.Copy(_items, _frontArrayIndex, array, arrayIndex, Capacity - _frontArrayIndex);
+ Array.Copy(_items, 0, array, arrayIndex + Capacity - _frontArrayIndex,
+ _frontArrayIndex + (Count - Capacity));
+ }
+ }
+ catch (ArrayTypeMismatchException)
+ {
+ throw new ArgumentException(
+ "Target array type is not compatible with the type of items in the collection.");
+ }
+ }
+
+ /// <summary>
+ /// Sets the capacity to the actual number of elements in the <see cref="Deque{T}" />, if that number is less than a
+ /// threshold value.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This method can be used to minimize the <see cref="Deque{T}" />'s memory overhead if no new elements will be
+ /// added. The cost of reallocating and copying the elements of a <see cref="Deque{T}" /> can be considerable.
+ /// However, the <see cref="TrimExcess" /> method does nothing if the <see cref="Count" /> is more than 90% of
+ /// <see cref="Capacity" />. This avoids incurring a large reallocation cost for a relatively small gain.
+ /// </para>
+ /// <para>
+ /// If <see cref="Count" /> is more than 90% of <see cref="Capacity" />, this method is an O(1) operation; O(n)
+ /// otherwise, where n is <see cref="Count" />.
+ /// </para>
+ /// <para>
+ /// To reset a <see cref="Deque{T}" /> to its initial state, call the <see cref="Clear" /> method before calling
+ /// the <see cref="TrimExcess" /> method. Trimming an empty <see cref="Deque{T}" /> sets <see cref="Capacity" /> to
+ /// the default capacity.
+ /// </para>
+ /// <para>The capacity can also be set using the <see cref="Capacity" /> property.</para>
+ /// </remarks>
+ public void TrimExcess()
+ {
+ if (Count > (int)(_items.Length * 0.9))
+ return;
+ Capacity = Count;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private int GetArrayIndex(int index)
+ {
+ if ((index < 0) || (index >= Count))
+ return -1;
+ return _items.Length != 0 ? (_frontArrayIndex + index) % _items.Length : 0;
+ }
+
+ private void EnsureCapacity(int minimumCapacity)
+ {
+ if (_items.Length >= minimumCapacity)
+ return;
+ var newCapacity = _defaultCapacity;
+ if (_items.Length > 0)
+ newCapacity = _resizeFunction(_items.Length);
+ newCapacity = Math.Max(newCapacity, minimumCapacity);
+ Capacity = newCapacity;
+ }
+
+ /// <summary>
+ /// Adds an element to the beginning of the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="item">The element to add to the <see cref="Deque{T}" />. The value can be <c>null</c>.</param>
+ /// <remarks>
+ /// <para>
+ /// As elements are added to a <see cref="Deque{T}" />, <see cref="Capacity" /> is automatically increased by
+ /// <see cref="ResizeFunction" /> as required by reallocating the internal circular array.
+ /// </para>
+ /// <para>
+ /// If <see cref="Count" /> is less than <see cref="Capacity" />, this method is an O(1) operation. Otherwise the
+ /// internal circular array needs to be resized to accommodate the new element and this method becomes an O(n)
+ /// operation, where n is <see cref="Count" />.
+ /// </para>
+ /// </remarks>
+ public void AddToFront(T item)
+ {
+ EnsureCapacity(Count + 1);
+ _frontArrayIndex = (_frontArrayIndex - 1 + _items.Length) % _items.Length;
+ _items[_frontArrayIndex] = item;
+ Count++;
+ }
+
+ /// <summary>
+ /// Adds an element to the end of the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="item">The element to add to the <see cref="Deque{T}" />. The value can be <c>null</c>.</param>
+ /// <remarks>
+ /// <para>
+ /// As elements are added to a <see cref="Deque{T}" />, <see cref="Capacity" /> is automatically increased by
+ /// <see cref="ResizeFunction" /> as required by reallocating the internal circular array.
+ /// </para>
+ /// <para>
+ /// If <see cref="Count" /> is less than <see cref="Capacity" />, this method is an O(1) operation. Otherwise the
+ /// internal circular array needs to be resized to accommodate the new element and this method becomes an O(n)
+ /// operation, where n is <see cref="Count" />.
+ /// </para>
+ /// </remarks>
+ public void AddToBack(T item)
+ {
+ EnsureCapacity(Count + 1);
+ var index = (_frontArrayIndex + Count++) % _items.Length;
+ _items[index] = item;
+ }
+
+ /// <summary>
+ /// Returns the element at the specified index of the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="index">The zero-based index of the element to get.</param>
+ /// <param name="item">
+ /// When this method returns, contains the element at the specified index of the
+ /// <see cref="Deque{T}" />, if <paramref name="index" /> was non-negative and less than <see cref="Count" />;
+ /// otherwise, the default value for the type of the value parameter. This parameter is passed uninitialized.
+ /// </param>
+ /// <returns>
+ /// <c>true</c> if <paramref name="item" /> was successfully retrieved at <paramref name="index" /> from the of the
+ /// <see cref="Deque{T}" />;
+ /// otherwise, <c>false</c> if <paramref name="index" /> was non-negative and less than <see cref="Count" />.
+ /// </returns>
+ public bool Get(int index, out T item)
+ {
+ var arrayIndex = GetArrayIndex(index);
+ if (arrayIndex == -1)
+ {
+ item = default(T);
+ return false;
+ }
+ item = _items[arrayIndex];
+ return true;
+ }
+
+ /// <summary>
+ /// Returns the element at the beginning of the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="item">
+ /// When this method returns, contains the element at the beginning of the <see cref="Deque{T}" />, if
+ /// <see cref="Deque{T}" /> was not empty; otherwise, the default value for the type of the value parameter. This
+ /// parameter is passed uninitialized.
+ /// </param>
+ /// <returns>
+ /// <c>true</c> if <paramref name="item" /> was successfully from the beginning of the <see cref="Deque{T}" />;
+ /// otherwise, <c>false</c> if the <see cref="Deque{T}" /> is empty.
+ /// </returns>
+ public bool GetFront(out T item)
+ {
+ return Get(0, out item);
+ }
+
+ /// <summary>
+ /// Returns the element at the end of the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="item">
+ /// When this method returns, contains the element at the end of the <see cref="Deque{T}" />, if
+ /// <see cref="Deque{T}" /> was not empty; otherwise, the default value for the type of the value parameter. This
+ /// parameter is passed uninitialized.
+ /// </param>
+ /// <returns>
+ /// <c>true</c> if <paramref name="item" /> was successfully from the end of the <see cref="Deque{T}" />;
+ /// otherwise, <c>false</c> if the <see cref="Deque{T}" /> is empty.
+ /// </returns>
+ public bool GetBack(out T item)
+ {
+ return Get(Count - 1, out item);
+ }
+
+ /// <summary>
+ /// Removes the element at the beginning of the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="item">
+ /// When this method returns, contains the element removed from the beginning of the
+ /// <see cref="Deque{T}" />, if the <see cref="Deque{T}" /> was not empty; otherwise, the default value for the type of
+ /// the value parameter. This parameter is passed uninitialized.
+ /// </param>
+ /// <returns>
+ /// <c>true</c> if <paramref name="item" /> was successfully removed from the beginning of the <see cref="Deque{T}" />;
+ /// otherwise, <c>false</c> if the <see cref="Deque{T}" /> is empty.
+ /// </returns>
+ /// <remarks>
+ /// <para>
+ /// This method is similar to the <see cref="GetFront" /> method, but <see cref="GetFront" /> does not
+ /// modify the <see cref="Deque{T}" />.
+ /// </para>
+ /// <para>
+ /// <c>null</c> can be added to the <see cref="Deque{T}" /> as a value. To distinguish between a null value and
+ /// the end of the <see cref="Deque{T}" />, check whether the return value of <see cref="RemoveFromFront(out T)" />
+ /// is
+ /// <c>false</c> or
+ /// <see cref="Count" /> is <c>0</c>.
+ /// </para>
+ /// <para>
+ /// This method is an O(1) operation.
+ /// </para>
+ /// </remarks>
+ public bool RemoveFromFront(out T item)
+ {
+ if (Count == 0)
+ {
+ item = default(T);
+ return false;
+ }
+
+ var index = _frontArrayIndex % _items.Length;
+ item = _items[index];
+ _items[index] = default(T);
+ _frontArrayIndex = (_frontArrayIndex + 1) % _items.Length;
+ Count--;
+ return true;
+ }
+
+ /// <summary>
+ /// Removes the element at the beginning of the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if the element was successfully removed from the beginning of the <see cref="Deque{T}" />;
+ /// otherwise, <c>false</c> if the <see cref="Deque{T}" /> is empty.
+ /// </returns>
+ /// <remarks>
+ /// <para>
+ /// This method is similar to the <see cref="GetFront" /> method, but <see cref="GetFront" /> does not
+ /// modify the <see cref="Deque{T}" />.
+ /// </para>
+ /// <para>
+ /// <c>null</c> can be added to the <see cref="Deque{T}" /> as a value. To distinguish between a null value and
+ /// the end of the <see cref="Deque{T}" />, check whether the return value of <see cref="RemoveFromFront()" /> is
+ /// <c>false</c> or
+ /// <see cref="Count" /> is <c>0</c>.
+ /// </para>
+ /// <para>
+ /// This method is an O(1) operation.
+ /// </para>
+ /// </remarks>
+ public bool RemoveFromFront()
+ {
+ if (Count == 0)
+ return false;
+
+ var index = _frontArrayIndex % _items.Length;
+ _items[index] = default(T);
+ _frontArrayIndex = (_frontArrayIndex + 1) % _items.Length;
+ Count--;
+ return true;
+ }
+
+ /// <summary>
+ /// Removes the element at the end of the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <param name="item">
+ /// When this method returns, contains the element removed from the end of the
+ /// <see cref="Deque{T}" />, if the <see cref="Deque{T}" /> was not empty; otherwise, the default value for the type of
+ /// the value parameter. This parameter is passed uninitialized.
+ /// </param>
+ /// <returns>
+ /// <c>true</c> if <paramref name="item" /> was successfully removed from the end of the <see cref="Deque{T}" />;
+ /// otherwise, <c>false</c> if the <see cref="Deque{T}" /> is empty.
+ /// </returns>
+ /// <remarks>
+ /// <para>
+ /// This method is similar to the <see cref="GetBack" /> method, but <see cref="GetBack" /> does not
+ /// modify the <see cref="Deque{T}" />.
+ /// </para>
+ /// <para>
+ /// <c>null</c> can be added to the <see cref="Deque{T}" /> as a value. To distinguish between a null value and
+ /// the end of the <see cref="Deque{T}" />, check whether the return value of <see cref="RemoveFromBack(out T)" />
+ /// is
+ /// <c>false</c> or
+ /// <see cref="Count" /> is <c>0</c>.
+ /// </para>
+ /// <para>
+ /// This method is an O(1) operation.
+ /// </para>
+ /// </remarks>
+ public bool RemoveFromBack(out T item)
+ {
+ if (Count == 0)
+ {
+ item = default(T);
+ return false;
+ }
+
+ var circularBackIndex = (_frontArrayIndex + (Count - 1)) % _items.Length;
+ item = _items[circularBackIndex];
+ _items[circularBackIndex] = default(T);
+ Count--;
+ return true;
+ }
+
+ /// <summary>
+ /// Removes the element at the end of the <see cref="Deque{T}" />.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if the element was successfully removed from the end of the <see cref="Deque{T}" />;
+ /// otherwise, <c>false</c> if the <see cref="Deque{T}" /> is empty.
+ /// </returns>
+ /// <remarks>
+ /// <para>
+ /// This method is similar to the <see cref="GetBack" /> method, but <see cref="GetBack" /> does not
+ /// modify the <see cref="Deque{T}" />.
+ /// </para>
+ /// <para>
+ /// <c>null</c> can be added to the <see cref="Deque{T}" /> as a value. To distinguish between a null value and
+ /// the end of the <see cref="Deque{T}" />, check whether the return value of <see cref="RemoveFromBack()" /> is
+ /// <c>false</c> or
+ /// <see cref="Count" /> is <c>0</c>.
+ /// </para>
+ /// <para>
+ /// This method is an O(1) operation.
+ /// </para>
+ /// </remarks>
+ public bool RemoveFromBack()
+ {
+ if (Count == 0)
+ return false;
+
+ var circularBackIndex = (_frontArrayIndex + (Count - 1)) % _items.Length;
+ _items[circularBackIndex] = default(T);
+ Count--;
+ return true;
+ }
+
+ /// <summary>
+ /// Removes and returns the last item.
+ /// </summary>
+ /// <returns>The item that was removed</returns>
+ public T Pop()
+ {
+ if (RemoveFromBack(out var item))
+ return item;
+
+ throw new InvalidOperationException();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/DictionaryExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/DictionaryExtensions.cs
new file mode 100644
index 0000000..ee581b7
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/DictionaryExtensions.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Collections
+{
+ public static class DictionaryExtensions
+ {
+ public static TValue GetValueOrDefault<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key)
+ {
+ return GetValueOrDefault(dictionary, key, default(TValue));
+ }
+
+ public static TValue GetValueOrDefault<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key, TValue defaultValue)
+ {
+ TValue value;
+ return dictionary.TryGetValue(key, out value) ? value : defaultValue;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IObservableCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IObservableCollection.cs
new file mode 100644
index 0000000..bb4fe16
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IObservableCollection.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace MonoGame.Extended.Collections
+{
+ /// <summary>Interface for collections that can be observed</summary>
+ /// <typeparam name="T">Type of items managed in the collection</typeparam>
+ public interface IObservableCollection<T>
+ {
+ /// <summary>Raised when an item has been added to the collection</summary>
+ event EventHandler<ItemEventArgs<T>> ItemAdded;
+
+ /// <summary>Raised when an item is removed from the collection</summary>
+ event EventHandler<ItemEventArgs<T>> ItemRemoved;
+
+ /// <summary>Raised when the collection is about to be cleared</summary>
+ /// <remarks>
+ /// This could be covered by calling ItemRemoved for each item currently
+ /// contained in the collection, but it is often simpler and more efficient
+ /// to process the clearing of the entire collection as a special operation.
+ /// </remarks>
+ event EventHandler Clearing;
+
+ /// <summary>Raised when the collection has been cleared of its items</summary>
+ event EventHandler Cleared;
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IPoolable.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IPoolable.cs
new file mode 100644
index 0000000..cba24b8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IPoolable.cs
@@ -0,0 +1,12 @@
+namespace MonoGame.Extended.Collections
+{
+ public delegate void ReturnToPoolDelegate(IPoolable poolable);
+
+ public interface IPoolable
+ {
+ IPoolable NextNode { get; set; }
+ IPoolable PreviousNode { get; set; }
+ void Initialize(ReturnToPoolDelegate returnDelegate);
+ void Return();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ItemEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ItemEventArgs.cs
new file mode 100644
index 0000000..0da853b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ItemEventArgs.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace MonoGame.Extended.Collections
+{
+ /// <summary>
+ /// Arguments class for collections wanting to hand over an item in an event
+ /// </summary>
+ public class ItemEventArgs<T> : EventArgs
+ {
+ /// <summary>Initializes a new event arguments supplier</summary>
+ /// <param name="item">Item to be supplied to the event handler</param>
+ public ItemEventArgs(T item)
+ {
+ Item = item;
+ }
+
+ /// <summary>Obtains the collection item the event arguments are carrying</summary>
+ public T Item { get; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/KeyedCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/KeyedCollection.cs
new file mode 100644
index 0000000..e4ebeff
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/KeyedCollection.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Collections
+{
+ public class KeyedCollection<TKey, TValue> : ICollection<TValue>
+ {
+ private readonly Func<TValue, TKey> _getKey;
+ private readonly Dictionary<TKey, TValue> _dictionary = new Dictionary<TKey, TValue>();
+
+ public KeyedCollection(Func<TValue, TKey> getKey)
+ {
+ _getKey = getKey;
+ }
+
+ public TValue this[TKey key] => _dictionary[key];
+ public ICollection<TKey> Keys => _dictionary.Keys;
+ public ICollection<TValue> Values => _dictionary.Values;
+ public int Count => _dictionary.Count;
+ public bool IsReadOnly => false;
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public IEnumerator<TValue> GetEnumerator()
+ {
+ return _dictionary.Values.GetEnumerator();
+ }
+
+ public void Add(TValue item)
+ {
+ _dictionary.Add(_getKey(item), item);
+ }
+
+ public void Clear()
+ {
+ _dictionary.Clear();
+ }
+
+ public bool Contains(TValue item)
+ {
+ return _dictionary.ContainsKey(_getKey(item));
+ }
+
+ public void CopyTo(TValue[] array, int arrayIndex)
+ {
+ throw new NotSupportedException();
+ }
+
+ public bool Remove(TValue item)
+ {
+ return _dictionary.Remove(_getKey(item));
+ }
+
+ public bool ContainsKey(TKey key)
+ {
+ return _dictionary.ContainsKey(key);
+ }
+
+ public bool TryGetValue(TKey key, out TValue value)
+ {
+ return _dictionary.TryGetValue(key, out value);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ListExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ListExtensions.cs
new file mode 100644
index 0000000..9452a8e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ListExtensions.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Collections
+{
+ public static class ListExtensions
+ {
+ public static IList<T> Shuffle<T>(this IList<T> list, Random random)
+ {
+ var n = list.Count;
+ while (n > 1)
+ {
+ n--;
+ var k = random.Next(n + 1);
+ var value = list[k];
+ list[k] = list[n];
+ list[n] = value;
+ }
+ return list;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObjectPool.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObjectPool.cs
new file mode 100644
index 0000000..066fa96
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObjectPool.cs
@@ -0,0 +1,157 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace MonoGame.Extended.Collections
+{
+ public enum ObjectPoolIsFullPolicy
+ {
+ ReturnNull,
+ IncreaseSize,
+ KillExisting,
+ }
+
+ public class ObjectPool<T> : IEnumerable<T>
+ where T : class, IPoolable
+ {
+ private readonly ReturnToPoolDelegate _returnToPoolDelegate;
+
+ private readonly Deque<T> _freeItems; // circular buffer for O(1) operations
+ private T _headNode; // linked list for iteration
+ private T _tailNode;
+
+ private readonly Func<T> _instantiationFunction;
+
+ public ObjectPoolIsFullPolicy IsFullPolicy { get; }
+ public int Capacity { get; private set; }
+ public int TotalCount { get; private set; }
+ public int AvailableCount => _freeItems.Count;
+ public int InUseCount => TotalCount - AvailableCount;
+
+ public event Action<T> ItemUsed;
+ public event Action<T> ItemReturned;
+
+ public ObjectPool(Func<T> instantiationFunc, int capacity = 16, ObjectPoolIsFullPolicy isFullPolicy = ObjectPoolIsFullPolicy.ReturnNull)
+ {
+ if (instantiationFunc == null)
+ throw new ArgumentNullException(nameof(instantiationFunc));
+
+ _returnToPoolDelegate = Return;
+
+ _instantiationFunction = instantiationFunc;
+ _freeItems = new Deque<T>(capacity);
+ IsFullPolicy = isFullPolicy;
+
+ Capacity = capacity;
+ }
+
+ public IEnumerator<T> GetEnumerator()
+ {
+ var node = _headNode;
+ while (node != null)
+ {
+ yield return node;
+ node = (T)node.NextNode;
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public T New()
+ {
+ if (!_freeItems.RemoveFromFront(out var poolable))
+ {
+ if (TotalCount <= Capacity)
+ {
+ poolable = CreateObject();
+ }
+ else
+ {
+ switch (IsFullPolicy)
+ {
+ case ObjectPoolIsFullPolicy.ReturnNull:
+ return null;
+ case ObjectPoolIsFullPolicy.IncreaseSize:
+ Capacity++;
+ poolable = CreateObject();
+ break;
+ case ObjectPoolIsFullPolicy.KillExisting:
+ if (_headNode == null)
+ return null;
+ var newHeadNode = (T)_headNode.NextNode;
+ _headNode.Return();
+ _freeItems.RemoveFromBack(out poolable);
+ _headNode = newHeadNode;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+ }
+
+ Use(poolable);
+ return poolable;
+ }
+
+ private T CreateObject()
+ {
+ TotalCount++;
+ var item = _instantiationFunction();
+ if (item == null)
+ throw new NullReferenceException($"The created pooled object of type '{typeof(T).Name}' is null.");
+ item.PreviousNode = _tailNode;
+ item.NextNode = null;
+ if (_headNode == null)
+ _headNode = item;
+ if (_tailNode != null)
+ _tailNode.NextNode = item;
+ _tailNode = item;
+ return item;
+ }
+
+ private void Return(IPoolable item)
+ {
+ Debug.Assert(item != null);
+
+ var poolable1 = (T) item;
+
+ var previousNode = (T)item.PreviousNode;
+ var nextNode = (T)item.NextNode;
+
+ if (previousNode != null)
+ previousNode.NextNode = nextNode;
+ if (nextNode != null)
+ nextNode.PreviousNode = previousNode;
+
+ if (item == _headNode)
+ _headNode = nextNode;
+ if (item == _tailNode)
+ _tailNode = previousNode;
+
+ if (_tailNode != null)
+ _tailNode.NextNode = null;
+
+ _freeItems.AddToBack(poolable1);
+
+ ItemReturned?.Invoke((T)item);
+ }
+
+ private void Use(T item)
+ {
+ item.Initialize(_returnToPoolDelegate);
+ item.NextNode = null;
+ if (item != _tailNode)
+ {
+ item.PreviousNode = _tailNode;
+ _tailNode.NextNode = item;
+ _tailNode = item;
+ }
+
+ ItemUsed?.Invoke(item);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObservableCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObservableCollection.cs
new file mode 100644
index 0000000..de827df
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObservableCollection.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace MonoGame.Extended.Collections
+{
+ public class ObservableCollection<T> : Collection<T>, IObservableCollection<T>
+ {
+ /// <summary>
+ /// Initializes a new instance of the ObservableCollection class that is empty.
+ /// </summary>
+ public ObservableCollection()
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the ObservableCollection class as a wrapper
+ /// for the specified list.
+ /// </summary>
+ /// <param name="list">The list that is wrapped by the new collection.</param>
+ /// <exception cref="System.ArgumentNullException">
+ /// List is null.
+ /// </exception>
+ public ObservableCollection(IList<T> list) : base(list)
+ {
+ }
+
+ /// <summary>Raised when an item has been added to the collection</summary>
+ public event EventHandler<ItemEventArgs<T>> ItemAdded;
+
+ /// <summary>Raised when an item is removed from the collection</summary>
+ public event EventHandler<ItemEventArgs<T>> ItemRemoved;
+
+ /// <summary>Raised when the collection is about to be cleared</summary>
+ /// <remarks>
+ /// This could be covered by calling ItemRemoved for each item currently
+ /// contained in the collection, but it is often simpler and more efficient
+ /// to process the clearing of the entire collection as a special operation.
+ /// </remarks>
+ public event EventHandler Clearing;
+
+ /// <summary>Raised when the collection has been cleared</summary>
+ public event EventHandler Cleared;
+
+ /// <summary>Removes all elements from the Collection</summary>
+ protected override void ClearItems()
+ {
+ OnClearing();
+ base.ClearItems();
+ OnCleared();
+ }
+
+ /// <summary>
+ /// Inserts an element into the ObservableCollection at the specified index
+ /// </summary>
+ /// <param name="index">
+ /// The object to insert. The value can be null for reference types.
+ /// </param>
+ /// <param name="item">The zero-based index at which item should be inserted</param>
+ protected override void InsertItem(int index, T item)
+ {
+ base.InsertItem(index, item);
+ OnAdded(item);
+ }
+
+ /// <summary>
+ /// Removes the element at the specified index of the ObservableCollection
+ /// </summary>
+ /// <param name="index">The zero-based index of the element to remove</param>
+ protected override void RemoveItem(int index)
+ {
+ var item = base[index];
+ base.RemoveItem(index);
+ OnRemoved(item);
+ }
+
+ /// <summary>Replaces the element at the specified index</summary>
+ /// <param name="index">
+ /// The new value for the element at the specified index. The value can be null
+ /// for reference types
+ /// </param>
+ /// <param name="item">The zero-based index of the element to replace</param>
+ protected override void SetItem(int index, T item)
+ {
+ var oldItem = base[index];
+ base.SetItem(index, item);
+ OnRemoved(oldItem);
+ OnAdded(item);
+ }
+
+ /// <summary>Fires the 'ItemAdded' event</summary>
+ /// <param name="item">Item that has been added to the collection</param>
+ protected virtual void OnAdded(T item)
+ {
+ ItemAdded?.Invoke(this, new ItemEventArgs<T>(item));
+ }
+
+ /// <summary>Fires the 'ItemRemoved' event</summary>
+ /// <param name="item">Item that has been removed from the collection</param>
+ protected virtual void OnRemoved(T item)
+ {
+ ItemRemoved?.Invoke(this, new ItemEventArgs<T>(item));
+ }
+
+ /// <summary>Fires the 'Clearing' event</summary>
+ protected virtual void OnClearing()
+ {
+ Clearing?.Invoke(this, EventArgs.Empty);
+ }
+
+ /// <summary>Fires the 'Cleared' event</summary>
+ protected virtual void OnCleared()
+ {
+ Cleared?.Invoke(this, EventArgs.Empty);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Pool.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Pool.cs
new file mode 100644
index 0000000..7c1546f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Pool.cs
@@ -0,0 +1,51 @@
+using System;
+
+namespace MonoGame.Extended.Collections
+{
+ public class Pool<T>
+ where T : class
+ {
+ private readonly Func<T> _createItem;
+ private readonly Action<T> _resetItem;
+ private readonly Deque<T> _freeItems;
+ private readonly int _maximum;
+
+ public Pool(Func<T> createItem, Action<T> resetItem, int capacity = 16, int maximum = int.MaxValue)
+ {
+ _createItem = createItem;
+ _resetItem = resetItem;
+ _maximum = maximum;
+ _freeItems = new Deque<T>(capacity);
+ }
+
+ public Pool(Func<T> createItem, int capacity = 16, int maximum = int.MaxValue)
+ : this(createItem, _ => { }, capacity, maximum)
+ {
+ }
+
+ public int AvailableCount => _freeItems.Count;
+
+ public T Obtain()
+ {
+ if (_freeItems.Count > 0)
+ return _freeItems.Pop();
+
+ return _createItem();
+ }
+
+ public void Free(T item)
+ {
+ if (item == null) throw new ArgumentNullException(nameof(item));
+
+ if (_freeItems.Count < _maximum)
+ _freeItems.AddToBack(item);
+
+ _resetItem(item);
+ }
+
+ public void Clear()
+ {
+ _freeItems.Clear();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorExtensions.cs
new file mode 100644
index 0000000..452ad37
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorExtensions.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Globalization;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// Provides additional methods for working with color
+ /// </summary>
+ public static class ColorExtensions
+ {
+ public static Color FromHex(string value)
+ {
+ var r = int.Parse(value.Substring(1, 2), NumberStyles.HexNumber);
+ var g = int.Parse(value.Substring(3, 2), NumberStyles.HexNumber);
+ var b = int.Parse(value.Substring(5, 2), NumberStyles.HexNumber);
+ var a = value.Length > 7 ? int.Parse(value.Substring(7, 2), NumberStyles.HexNumber) : 255;
+ return new Color(r, g, b, a);
+ }
+
+ public static Color ToRgb(this HslColor c)
+ {
+ var h = c.H;
+ var s = c.S;
+ var l = c.L;
+
+ if (s == 0f)
+ return new Color(l, l, l);
+
+ h = h/360f;
+ var max = l < 0.5f ? l*(1 + s) : l + s - l*s;
+ var min = 2f*l - max;
+
+ return new Color(
+ ComponentFromHue(min, max, h + 1f/3f),
+ ComponentFromHue(min, max, h),
+ ComponentFromHue(min, max, h - 1f/3f));
+ }
+
+ private static float ComponentFromHue(float m1, float m2, float h)
+ {
+ h = (h + 1f)%1f;
+ if (h*6f < 1)
+ return m1 + (m2 - m1)*6f*h;
+ if (h*2 < 1)
+ return m2;
+ if (h*3 < 2)
+ return m1 + (m2 - m1)*(2f/3f - h)*6f;
+ return m1;
+ }
+
+ public static HslColor ToHsl(this Color c)
+ {
+ var r = c.R/255f;
+ var b = c.B/255f;
+ var g = c.G/255f;
+
+ var max = Math.Max(Math.Max(r, g), b);
+ var min = Math.Min(Math.Min(r, g), b);
+ var chroma = max - min;
+ var sum = max + min;
+
+ var l = sum*0.5f;
+
+ if (chroma == 0)
+ return new HslColor(0f, 0f, l);
+
+ float h;
+
+ if (r == max)
+ h = (60*(g - b)/chroma + 360)%360;
+ else
+ {
+ if (g == max)
+ h = 60*(b - r)/chroma + 120f;
+ else
+ h = 60*(r - g)/chroma + 240f;
+ }
+
+ var s = l <= 0.5f ? chroma/sum : chroma/(2f - sum);
+
+ return new HslColor(h, s, l);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorHelper.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorHelper.cs
new file mode 100644
index 0000000..370af3d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorHelper.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public static class ColorHelper
+ {
+ //http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
+ public static Color FromHsl(float hue, float saturation, float lightness)
+ {
+ var hsl = new Vector4(hue, saturation, lightness, 1);
+ var color = new Vector4(0, 0, 0, hsl.W);
+
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if (hsl.Y == 0.0f)
+ color.X = color.Y = color.Z = hsl.Z;
+ else
+ {
+ var q = hsl.Z < 0.5f ? hsl.Z*(1.0f + hsl.Y) : hsl.Z + hsl.Y - hsl.Z*hsl.Y;
+ var p = 2.0f*hsl.Z - q;
+
+ color.X = HueToRgb(p, q, hsl.X + 1.0f/3.0f);
+ color.Y = HueToRgb(p, q, hsl.X);
+ color.Z = HueToRgb(p, q, hsl.X - 1.0f/3.0f);
+ }
+
+ return new Color(color);
+ }
+
+ private static float HueToRgb(float p, float q, float t)
+ {
+ if (t < 0.0f) t += 1.0f;
+ if (t > 1.0f) t -= 1.0f;
+ if (t < 1.0f/6.0f) return p + (q - p)*6.0f*t;
+ if (t < 1.0f/2.0f) return q;
+ if (t < 2.0f/3.0f) return p + (q - p)*(2.0f/3.0f - t)*6.0f;
+ return p;
+ }
+
+ public static Color FromHex(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ return Color.Transparent;
+ var startIndex = 0;
+ if (value.StartsWith("#"))
+ startIndex++;
+ var r = int.Parse(value.Substring(startIndex, 2), NumberStyles.HexNumber);
+ var g = int.Parse(value.Substring(startIndex + 2, 2), NumberStyles.HexNumber);
+ var b = int.Parse(value.Substring(startIndex + 4, 2), NumberStyles.HexNumber);
+ var a = value.Length > 6 + startIndex ? int.Parse(value.Substring(startIndex + 6, 2), NumberStyles.HexNumber) : 255;
+
+ return new Color(r, g, b, a);
+ }
+
+ public static string ToHex(Color color)
+ {
+ var rx = $"{color.R:x2}";
+ var gx = $"{color.G:x2}";
+ var bx = $"{color.B:x2}";
+ var ax = $"{color.A:x2}";
+ return $"#{rx}{gx}{bx}{ax}";
+ }
+
+ private static readonly Dictionary<string, Color> _colorsByName = typeof(Color)
+ .GetRuntimeProperties()
+ .Where(p => p.PropertyType == typeof(Color))
+ .ToDictionary(p => p.Name, p => (Color) p.GetValue(null), StringComparer.OrdinalIgnoreCase);
+
+ public static Color FromName(string name)
+ {
+ Color color;
+
+ if(_colorsByName.TryGetValue(name, out color))
+ return color;
+
+ throw new InvalidOperationException($"{name} is not a valid color");
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentManagerExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentManagerExtensions.cs
new file mode 100644
index 0000000..69e7730
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentManagerExtensions.cs
@@ -0,0 +1,51 @@
+using System.IO;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Content
+{
+ public interface IContentLoader<out T>
+ {
+ T Load(ContentManager contentManager, string path);
+ }
+
+ public interface IContentLoader
+ {
+ T Load<T>(ContentManager contentManager, string path);
+ }
+
+ public static class ContentManagerExtensions
+ {
+ public const string DirectorySeparatorChar = "/";
+
+ public static Stream OpenStream(this ContentManager contentManager, string path)
+ {
+ return TitleContainer.OpenStream(contentManager.RootDirectory + DirectorySeparatorChar + path);
+ }
+
+ public static GraphicsDevice GetGraphicsDevice(this ContentManager contentManager)
+ {
+ // http://konaju.com/?p=21
+ var serviceProvider = contentManager.ServiceProvider;
+ var graphicsDeviceService = (IGraphicsDeviceService) serviceProvider.GetService(typeof(IGraphicsDeviceService));
+ return graphicsDeviceService.GraphicsDevice;
+ }
+
+ /// <summary>
+ /// Loads the content using a custom content loader.
+ /// </summary>
+ public static T Load<T>(this ContentManager contentManager, string path, IContentLoader contentLoader)
+ {
+ return contentLoader.Load<T>(contentManager, path);
+ }
+
+ /// <summary>
+ /// Loads the content using a custom content loader.
+ /// </summary>
+ public static T Load<T>(this ContentManager contentManager, string path, IContentLoader<T> contentLoader)
+ {
+ return contentLoader.Load(contentManager, path);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentReaderExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentReaderExtensions.cs
new file mode 100644
index 0000000..f4c51a4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentReaderExtensions.cs
@@ -0,0 +1,44 @@
+using System;
+using System.IO;
+using System.Reflection;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Content
+{
+ public static class ContentReaderExtensions
+ {
+ private static readonly FieldInfo _contentReaderGraphicsDeviceFieldInfo = typeof(ContentReader).GetTypeInfo().GetDeclaredField("graphicsDevice");
+
+ public static GraphicsDevice GetGraphicsDevice(this ContentReader contentReader)
+ {
+ return (GraphicsDevice)_contentReaderGraphicsDeviceFieldInfo.GetValue(contentReader);
+ }
+
+ public static string RemoveExtension(string path)
+ {
+ return Path.ChangeExtension(path, null).TrimEnd('.');
+ }
+
+ public static string GetRelativeAssetName(this ContentReader contentReader, string relativeName)
+ {
+ var assetDirectory = Path.GetDirectoryName(contentReader.AssetName);
+ var assetName = RemoveExtension(Path.Combine(assetDirectory, relativeName).Replace('\\', '/'));
+
+ return ShortenRelativePath(assetName);
+ }
+
+ public static string ShortenRelativePath(string relativePath)
+ {
+ var ellipseIndex = relativePath.IndexOf("/../", StringComparison.Ordinal);
+ while (ellipseIndex != -1)
+ {
+ var lastDirectoryIndex = relativePath.LastIndexOf('/', ellipseIndex - 1) + 1;
+ relativePath = relativePath.Remove(lastDirectoryIndex, ellipseIndex + 4 - lastDirectoryIndex);
+ ellipseIndex = relativePath.IndexOf("/../", StringComparison.Ordinal);
+ }
+
+ return relativePath;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounter.cs
new file mode 100644
index 0000000..432f1ac
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounter.cs
@@ -0,0 +1,34 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public class FramesPerSecondCounter : IUpdate
+ {
+ private static readonly TimeSpan _oneSecondTimeSpan = new TimeSpan(0, 0, 1);
+ private int _framesCounter;
+ private TimeSpan _timer = _oneSecondTimeSpan;
+
+ public FramesPerSecondCounter()
+ {
+ }
+
+ public int FramesPerSecond { get; private set; }
+
+ public void Update(GameTime gameTime)
+ {
+ _timer += gameTime.ElapsedGameTime;
+ if (_timer <= _oneSecondTimeSpan)
+ return;
+
+ FramesPerSecond = _framesCounter;
+ _framesCounter = 0;
+ _timer -= _oneSecondTimeSpan;
+ }
+
+ public void Draw(GameTime gameTime)
+ {
+ _framesCounter++;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounterComponent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounterComponent.cs
new file mode 100644
index 0000000..ef36072
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounterComponent.cs
@@ -0,0 +1,27 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public class FramesPerSecondCounterComponent : DrawableGameComponent
+ {
+ private readonly FramesPerSecondCounter _fpsCounter;
+
+ public FramesPerSecondCounterComponent(Game game)
+ : base(game)
+ {
+ _fpsCounter = new FramesPerSecondCounter();
+ }
+
+ public int FramesPerSecond => _fpsCounter.FramesPerSecond;
+
+ public override void Update(GameTime gameTime)
+ {
+ _fpsCounter.Update(gameTime);
+ }
+
+ public override void Draw(GameTime gameTime)
+ {
+ _fpsCounter.Draw(gameTime);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/GameComponentCollectionExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/GameComponentCollectionExtensions.cs
new file mode 100644
index 0000000..c470a12
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/GameComponentCollectionExtensions.cs
@@ -0,0 +1,24 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public static class GameComponentCollectionExtensions
+ {
+ public static T Add<T>(this GameComponentCollection collection)
+ where T : IGameComponent, new()
+ {
+ var gameComponent = new T();
+ collection.Add(gameComponent);
+ return gameComponent;
+ }
+
+ public static T Add<T>(this GameComponentCollection collection, Func<T> createGameComponent)
+ where T : IGameComponent
+ {
+ var gameComponent = createGameComponent();
+ collection.Add(gameComponent);
+ return gameComponent;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/GameTimeExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/GameTimeExtensions.cs
new file mode 100644
index 0000000..01c8790
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/GameTimeExtensions.cs
@@ -0,0 +1,12 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public static class GameTimeExtensions
+ {
+ public static float GetElapsedSeconds(this GameTime gameTime)
+ {
+ return (float) gameTime.ElapsedGameTime.TotalSeconds;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/HslColor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/HslColor.cs
new file mode 100644
index 0000000..628073d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/HslColor.cs
@@ -0,0 +1,270 @@
+using System;
+using System.Globalization;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// An immutable data structure representing a 24bit color composed of separate hue, saturation and lightness channels.
+ /// </summary>
+ //[Serializable]
+ public struct HslColor : IEquatable<HslColor>, IComparable<HslColor>
+ {
+ /// <summary>
+ /// Gets the value of the hue channel in degrees.
+ /// </summary>
+ public readonly float H;
+
+ /// <summary>
+ /// Gets the value of the saturation channel.
+ /// </summary>
+ public readonly float S;
+
+ /// <summary>
+ /// Gets the value of the lightness channel.
+ /// </summary>
+ public readonly float L;
+
+ private static float NormalizeHue(float h)
+ {
+ if (h < 0) return h + 360*((int) (h/360) + 1);
+ return h%360;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HslColor" /> structure.
+ /// </summary>
+ /// <param name="h">The value of the hue channel.</param>
+ /// <param name="s">The value of the saturation channel.</param>
+ /// <param name="l">The value of the lightness channel.</param>
+ public HslColor(float h, float s, float l) : this()
+ {
+ // normalize the hue
+ H = NormalizeHue(h);
+ S = MathHelper.Clamp(s, 0f, 1f);
+ L = MathHelper.Clamp(l, 0f, 1f);
+ }
+
+ /// <summary>
+ /// Copies the individual channels of the color to the specified memory location.
+ /// </summary>
+ /// <param name="destination">The memory location to copy the axis to.</param>
+ public void CopyTo(out HslColor destination)
+ {
+ destination = new HslColor(H, S, L);
+ }
+
+ /// <summary>
+ /// Destructures the color, exposing the individual channels.
+ /// </summary>
+ public void Destructure(out float h, out float s, out float l)
+ {
+ h = H;
+ s = S;
+ l = L;
+ }
+
+ /// <summary>
+ /// Exposes the individual channels of the color to the specified matching function.
+ /// </summary>
+ /// <param name="callback">The function which matches the individual channels of the color.</param>
+ /// <exception cref="T:System.ArgumentNullException">
+ /// Thrown if the value passed to the <paramref name="callback" /> parameter is <c>null</c>.
+ /// </exception>
+ public void Match(Action<float, float, float> callback)
+ {
+ if (callback == null)
+ throw new ArgumentNullException(nameof(callback));
+
+ callback(H, S, L);
+ }
+
+ /// <summary>
+ /// Exposes the individual channels of the color to the specified mapping function and returns the
+ /// result;
+ /// </summary>
+ /// <typeparam name="T">The type being mapped to.</typeparam>
+ /// <param name="map">
+ /// A function which maps the color channels to an instance of <typeparamref name="T" />.
+ /// </param>
+ /// <returns>
+ /// The result of the <paramref name="map" /> function when passed the individual X and Y components.
+ /// </returns>
+ /// <exception cref="T:System.ArgumentNullException">
+ /// Thrown if the value passed to the <paramref name="map" /> parameter is <c>null</c>.
+ /// </exception>
+ public T Map<T>(Func<float, float, float, T> map)
+ {
+ if (map == null)
+ throw new ArgumentNullException(nameof(map));
+
+ return map(H, S, L);
+ }
+
+ public static HslColor operator +(HslColor a, HslColor b)
+ {
+ return new HslColor(a.H + b.H, a.S + b.S, a.L + b.L);
+ }
+
+ public static implicit operator HslColor(string value)
+ {
+ return Parse(value);
+ }
+
+ public int CompareTo(HslColor other)
+ {
+ // ReSharper disable ImpureMethodCallOnReadonlyValueField
+ return H.CompareTo(other.H)*100 + S.CompareTo(other.S)*10 + L.CompareTo(L);
+ // ReSharper restore ImpureMethodCallOnReadonlyValueField
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="System.Object" /> is equal to this instance.
+ /// </summary>
+ /// <param name="obj">The <see cref="System.Object" /> to compare with this instance.</param>
+ /// <returns>
+ /// <c>true</c> if the specified <see cref="System.Object" /> is equal to this instance; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ if (obj is HslColor)
+ return Equals((HslColor) obj);
+
+ return base.Equals(obj);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="HslColor" /> is equal to this instance.
+ /// </summary>
+ /// <param name="value">The <see cref="HslColor" /> to compare with this instance.</param>
+ /// <returns>
+ /// <c>true</c> if the specified <see cref="HslColor" /> is equal to this instance; otherwise, <c>false</c>.
+ /// </returns>
+ public bool Equals(HslColor value)
+ {
+ // ReSharper disable ImpureMethodCallOnReadonlyValueField
+ return H.Equals(value.H) && S.Equals(value.S) && L.Equals(value.L);
+ // ReSharper restore ImpureMethodCallOnReadonlyValueField
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>
+ /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ return H.GetHashCode() ^
+ S.GetHashCode() ^
+ L.GetHashCode();
+ }
+
+ /// <summary>
+ /// Returns a <see cref="System.String" /> that represents this instance.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="System.String" /> that represents this instance.
+ /// </returns>
+ public override string ToString()
+ {
+ return string.Format(CultureInfo.InvariantCulture, "H:{0:N1}° S:{1:N1} L:{2:N1}",
+ H, 100*S, 100*L);
+ }
+
+ public static HslColor Parse(string s)
+ {
+ var hsl = s.Split(',');
+ var hue = float.Parse(hsl[0].TrimEnd('°'), CultureInfo.InvariantCulture.NumberFormat);
+ var sat = float.Parse(hsl[1], CultureInfo.InvariantCulture.NumberFormat);
+ var lig = float.Parse(hsl[2], CultureInfo.InvariantCulture.NumberFormat);
+
+ return new HslColor(hue, sat, lig);
+ }
+
+ /// <summary>
+ /// Implements the operator ==.
+ /// </summary>
+ /// <param name="x">The lvalue.</param>
+ /// <param name="y">The rvalue.</param>
+ /// <returns>
+ /// <c>true</c> if the lvalue <see cref="HslColor" /> is equal to the rvalue; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(HslColor x, HslColor y)
+ {
+ return x.Equals(y);
+ }
+
+ /// <summary>
+ /// Implements the operator !=.
+ /// </summary>
+ /// <param name="x">The lvalue.</param>
+ /// <param name="y">The rvalue.</param>
+ /// <returns>
+ /// <c>true</c> if the lvalue <see cref="HslColor" /> is not equal to the rvalue; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(HslColor x, HslColor y)
+ {
+ return !x.Equals(y);
+ }
+
+ public static HslColor operator -(HslColor a, HslColor b)
+ {
+ return new HslColor(a.H - b.H, a.S - b.S, a.L - b.L);
+ }
+
+ public static HslColor Lerp(HslColor c1, HslColor c2, float t)
+ {
+ // loop around if c2.H < c1.H
+ var h2 = c2.H >= c1.H ? c2.H : c2.H + 360;
+ return new HslColor(
+ c1.H + t*(h2 - c1.H),
+ c1.S + t*(c2.S - c1.S),
+ c1.L + t*(c2.L - c1.L));
+ }
+
+ public static HslColor FromRgb(Color color)
+ {
+ // derived from http://www.geekymonkey.com/Programming/CSharp/RGB2HSL_HSL2RGB.htm
+ var r = color.R / 255f;
+ var g = color.G / 255f;
+ var b = color.B / 255f;
+ var h = 0f; // default to black
+ var s = 0f;
+ var l = 0f;
+ var v = Math.Max(r, g);
+ v = Math.Max(v, b);
+
+ var m = Math.Min(r, g);
+ m = Math.Min(m, b);
+ l = (m + v) / 2.0f;
+
+ if (l <= 0.0)
+ return new HslColor(h, s, l);
+
+ var vm = v - m;
+ s = vm;
+
+ if (s > 0.0)
+ s /= l <= 0.5f ? v + m : 2.0f - v - m;
+ else
+ return new HslColor(h, s, l);
+
+ var r2 = (v - r) / vm;
+ var g2 = (v - g) / vm;
+ var b2 = (v - b) / vm;
+
+ if (Math.Abs(r - v) < float.Epsilon)
+ h = Math.Abs(g - m) < float.Epsilon ? 5.0f + b2 : 1.0f - g2;
+ else if (Math.Abs(g - v) < float.Epsilon)
+ h = Math.Abs(b - m) < float.Epsilon ? 1.0f + r2 : 3.0f - b2;
+ else
+ h = Math.Abs(r - m) < float.Epsilon ? 3.0f + g2 : 5.0f - r2;
+
+ h *= 60;
+ h = NormalizeHue(h);
+
+ return new HslColor(h, s, l);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/IColorable.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IColorable.cs
new file mode 100644
index 0000000..ad3c35c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IColorable.cs
@@ -0,0 +1,9 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public interface IColorable
+ {
+ Color Color { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/IEquatableByRef.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IEquatableByRef.cs
new file mode 100644
index 0000000..678e510
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IEquatableByRef.cs
@@ -0,0 +1,21 @@
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// Defines a generalized method that a value type or class implements to create a type-specific method for
+ /// determining equality of instances by reference.
+ /// </summary>
+ /// <typeparam name="T">The type of values or objects to compare.</typeparam>
+ public interface IEquatableByRef<T>
+ {
+ /// <summary>
+ /// Indicates whether the current value or object is equal to another value or object of the same type by
+ /// reference.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if the current value or object is equal to the <paramref name="other" /> parameter; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ /// <param name="other">A value or object to compare with this value or object.</param>
+ bool Equals(ref T other);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/IMovable.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IMovable.cs
new file mode 100644
index 0000000..a851088
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IMovable.cs
@@ -0,0 +1,9 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public interface IMovable
+ {
+ Vector2 Position { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/IRectangular.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IRectangular.cs
new file mode 100644
index 0000000..a8b1eab
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IRectangular.cs
@@ -0,0 +1,14 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public interface IRectangular
+ {
+ Rectangle BoundingRectangle { get; }
+ }
+
+ public interface IRectangularF
+ {
+ RectangleF BoundingRectangle { get; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/IRotatable.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IRotatable.cs
new file mode 100644
index 0000000..7a7d622
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IRotatable.cs
@@ -0,0 +1,7 @@
+namespace MonoGame.Extended
+{
+ public interface IRotatable
+ {
+ float Rotation { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/IScalable.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IScalable.cs
new file mode 100644
index 0000000..70a527b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IScalable.cs
@@ -0,0 +1,9 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public interface IScalable
+ {
+ Vector2 Scale { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ISizable.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ISizable.cs
new file mode 100644
index 0000000..5b55058
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ISizable.cs
@@ -0,0 +1,8 @@
+
+namespace MonoGame.Extended
+{
+ public interface ISizable
+ {
+ Size2 Size { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/IUpdate.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IUpdate.cs
new file mode 100644
index 0000000..6240e04
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IUpdate.cs
@@ -0,0 +1,9 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public interface IUpdate
+ {
+ void Update(GameTime gameTime);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Angle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Angle.cs
new file mode 100644
index 0000000..19f4855
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Angle.cs
@@ -0,0 +1,224 @@
+/*
+* -----------------------------------------------------------------------------
+* Original code from SlimMath project. http://code.google.com/p/slimmath/
+* Greetings to SlimDX Group. Original code published with the following license:
+* -----------------------------------------------------------------------------
+*
+* Copyright (c) 2007-2010 SlimDX Group
+*
+* Permission is hereby granted, free of charge, to any person obtaining a copy
+* of this software and associated documentation files (the "Software"), to deal
+* in the Software without restriction, including without limitation the rights
+* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+* copies of the Software, and to permit persons to whom the Software is
+* furnished to do so, subject to the following conditions:
+*
+* The above copyright notice and this permission notice shall be included in
+* all copies or substantial portions of the Software.
+*
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+* THE SOFTWARE.
+*/
+
+using System;
+using System.Diagnostics;
+using System.Runtime.Serialization;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public enum AngleType : byte
+ {
+ Radian = 0,
+ Degree,
+ Revolution, //or Turn / cycle
+ Gradian // or Gon
+ }
+
+ [DataContract]
+ [DebuggerDisplay("{ToString(),nq}")]
+ public struct Angle : IComparable<Angle>, IEquatable<Angle>
+ {
+ private const float _tau = (float) (Math.PI*2.0);
+ private const float _tauInv = (float) (0.5/Math.PI);
+ private const float _degreeRadian = (float) (Math.PI/180.0);
+ private const float _radianDegree = (float) (180.0/Math.PI);
+ private const float _gradianRadian = (float) (Math.PI/200.0);
+ private const float _radianGradian = (float) (200.0/Math.PI);
+
+ [DataMember]
+ public float Radians { get; set; }
+
+ public float Degrees
+ {
+ get => Radians*_radianDegree;
+ set => Radians = value*_degreeRadian;
+ }
+
+ public float Gradians
+ {
+ get => Radians*_radianGradian;
+ set => Radians = value*_gradianRadian;
+ }
+
+ public float Revolutions
+ {
+ get => Radians*_tauInv;
+ set => Radians = value*_tau;
+ }
+
+ public Angle(float value, AngleType angleType = AngleType.Radian)
+ {
+ switch (angleType)
+ {
+ default:
+ Radians = 0f;
+ break;
+ case AngleType.Radian:
+ Radians = value;
+ break;
+ case AngleType.Degree:
+ Radians = value*_degreeRadian;
+ break;
+ case AngleType.Revolution:
+ Radians = value*_tau;
+ break;
+ case AngleType.Gradian:
+ Radians = value*_gradianRadian;
+ break;
+ }
+ }
+
+ public float GetValue(AngleType angleType)
+ {
+ switch (angleType)
+ {
+ default:
+ return 0f;
+ case AngleType.Radian:
+ return Radians;
+ case AngleType.Degree:
+ return Degrees;
+ case AngleType.Revolution:
+ return Revolutions;
+ case AngleType.Gradian:
+ return Gradians;
+ }
+ }
+
+ public void Wrap()
+ {
+ var angle = Radians%_tau;
+ if (angle <= Math.PI) angle += _tau;
+ if (angle > Math.PI) angle -= _tau;
+ Radians = angle;
+ }
+
+ public void WrapPositive()
+ {
+ Radians %= _tau;
+ if (Radians < 0d) Radians += _tau;
+ Radians = Radians;
+ }
+
+ public static Angle FromVector(Vector2 vector)
+ {
+ return new Angle((float) Math.Atan2(-vector.Y, vector.X));
+ }
+
+ public Vector2 ToUnitVector() => ToVector(1);
+
+ public Vector2 ToVector(float length)
+ {
+ return new Vector2(length*(float) Math.Cos(Radians), -length*(float) Math.Sin(Radians));
+ }
+
+ public static bool IsBetween(Angle value, Angle min, Angle end)
+ {
+ return end < min
+ ? (value >= min) || (value <= end)
+ : (value >= min) && (value <= end);
+ }
+
+ public int CompareTo(Angle other)
+ {
+ WrapPositive();
+ other.WrapPositive();
+ return Radians.CompareTo(other.Radians);
+ }
+
+ public bool Equals(Angle other)
+ {
+ WrapPositive();
+ other.WrapPositive();
+ return Radians.Equals(other.Radians);
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ return obj is Angle a && Equals(a);
+ }
+
+ public override int GetHashCode()
+ {
+ // ReSharper disable once NonReadonlyMemberInGetHashCode
+ return Radians.GetHashCode();
+ }
+
+ public static implicit operator float(Angle angle)
+ {
+ return angle.Radians;
+ }
+
+ public static explicit operator Angle(float angle)
+ {
+ return new Angle(angle);
+ }
+
+ public static Angle operator -(Angle angle)
+ {
+ return new Angle(-angle.Radians);
+ }
+
+ public static bool operator ==(Angle a, Angle b)
+ {
+ return a.Equals(b);
+ }
+
+ public static bool operator !=(Angle a, Angle b)
+ {
+ return !a.Equals(b);
+ }
+
+ public static Angle operator -(Angle left, Angle right)
+ {
+ return new Angle(left.Radians - right.Radians);
+ }
+
+ public static Angle operator *(Angle left, float right)
+ {
+ return new Angle(left.Radians*right);
+ }
+
+ public static Angle operator *(float left, Angle right)
+ {
+ return new Angle(right.Radians*left);
+ }
+
+ public static Angle operator +(Angle left, Angle right)
+ {
+ return new Angle(left.Radians + right.Radians);
+ }
+
+ public override string ToString()
+ {
+ return $"{Radians} Radians";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/BoundingRectangle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/BoundingRectangle.cs
new file mode 100644
index 0000000..324ebb2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/BoundingRectangle.cs
@@ -0,0 +1,579 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.2; Bounding Volumes - Axis-aligned Bounding Boxes (AABBs). pg 77
+
+ /// <summary>
+ /// An axis-aligned, four sided, two dimensional box defined by a centre <see cref="Point2" /> and a radii
+ /// <see cref="Vector2" />.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// An <see cref="BoundingRectangle" /> is categorized by having its faces oriented in such a way that its
+ /// face normals are at all times parallel with the axes of the given coordinate system.
+ /// </para>
+ /// <para>
+ /// The <see cref="BoundingRectangle" /> of a rotated <see cref="BoundingRectangle" /> will be equivalent or larger
+ /// in size
+ /// than the original depending on the angle of rotation.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="IEquatable{T}" />
+ /// <seealso cref="IEquatableByRef{T}" />
+ [DebuggerDisplay("{" + nameof(DebugDisplayString) + ",nq}")]
+ public struct BoundingRectangle : IEquatable<BoundingRectangle>,
+ IEquatableByRef<BoundingRectangle>
+ {
+ /// <summary>
+ /// The <see cref="BoundingRectangle" /> with <see cref="Center" /> <see cref="Point2.Zero"/> and
+ /// <see cref="HalfExtents" /> set to <see cref="Vector2.Zero"/>.
+ /// </summary>
+ public static readonly BoundingRectangle Empty = new BoundingRectangle();
+
+ /// <summary>
+ /// The centre position of this <see cref="BoundingRectangle" />.
+ /// </summary>
+ public Point2 Center;
+
+ /// <summary>
+ /// The distance from the <see cref="Center" /> point along both axes to any point on the boundary of this
+ /// <see cref="BoundingRectangle" />.
+ /// </summary>
+ public Vector2 HalfExtents;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BoundingRectangle" /> structure from the specified centre
+ /// <see cref="Point2" /> and the radii <see cref="Size2" />.
+ /// </summary>
+ /// <param name="center">The centre <see cref="Point2" />.</param>
+ /// <param name="halfExtents">The radii <see cref="Vector2" />.</param>
+ public BoundingRectangle(Point2 center, Size2 halfExtents)
+ {
+ Center = center;
+ HalfExtents = halfExtents;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> from a minimum <see cref="Point2" /> and maximum
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="minimum">The minimum point.</param>
+ /// <param name="maximum">The maximum point.</param>
+ /// <param name="result">The resulting bounding rectangle.</param>
+ public static void CreateFrom(Point2 minimum, Point2 maximum, out BoundingRectangle result)
+ {
+ result.Center = new Point2((maximum.X + minimum.X) * 0.5f, (maximum.Y + minimum.Y) * 0.5f);
+ result.HalfExtents = new Vector2((maximum.X - minimum.X) * 0.5f, (maximum.Y - minimum.Y) * 0.5f);
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> from a minimum <see cref="Point2" /> and maximum
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="minimum">The minimum point.</param>
+ /// <param name="maximum">The maximum point.</param>
+ /// <returns>The resulting <see cref="BoundingRectangle" />.</returns>
+ public static BoundingRectangle CreateFrom(Point2 minimum, Point2 maximum)
+ {
+ BoundingRectangle result;
+ CreateFrom(minimum, maximum, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> from a list of <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="points">The points.</param>
+ /// <param name="result">The resulting bounding rectangle.</param>
+ public static void CreateFrom(IReadOnlyList<Point2> points, out BoundingRectangle result)
+ {
+ Point2 minimum;
+ Point2 maximum;
+ PrimitivesHelper.CreateRectangleFromPoints(points, out minimum, out maximum);
+ CreateFrom(minimum, maximum, out result);
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> from a list of <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="points">The points.</param>
+ /// <returns>The resulting <see cref="BoundingRectangle" />.</returns>
+ public static BoundingRectangle CreateFrom(IReadOnlyList<Point2> points)
+ {
+ BoundingRectangle result;
+ CreateFrom(points, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> from the specified <see cref="BoundingRectangle" /> transformed by
+ /// the
+ /// specified <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <param name="transformMatrix">The transform matrix.</param>
+ /// <param name="result">The resulting bounding rectangle.</param>
+ /// <returns>
+ /// The <see cref="BoundingRectangle" /> from the <paramref name="boundingRectangle" /> transformed by the
+ /// <paramref name="transformMatrix" />.
+ /// </returns>
+ /// <remarks>
+ /// <para>
+ /// If a transformed <see cref="BoundingRectangle" /> is used for <paramref name="boundingRectangle" /> then the
+ /// resulting <see cref="BoundingRectangle" /> will have the compounded transformation, which most likely is
+ /// not desired.
+ /// </para>
+ /// </remarks>
+ public static void Transform(ref BoundingRectangle boundingRectangle,
+ ref Matrix2 transformMatrix, out BoundingRectangle result)
+ {
+ PrimitivesHelper.TransformRectangle(ref boundingRectangle.Center, ref boundingRectangle.HalfExtents, ref transformMatrix);
+ result.Center = boundingRectangle.Center;
+ result.HalfExtents = boundingRectangle.HalfExtents;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> from the specified <see cref="BoundingRectangle" /> transformed by
+ /// the
+ /// specified <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <param name="transformMatrix">The transform matrix.</param>
+ /// <returns>
+ /// The <see cref="BoundingRectangle" /> from the <paramref name="boundingRectangle" /> transformed by the
+ /// <paramref name="transformMatrix" />.
+ /// </returns>
+ /// <remarks>
+ /// <para>
+ /// If a transformed <see cref="BoundingRectangle" /> is used for <paramref name="boundingRectangle" /> then the
+ /// resulting <see cref="BoundingRectangle" /> will have the compounded transformation, which most likely is
+ /// not desired.
+ /// </para>
+ /// </remarks>
+ public static BoundingRectangle Transform(BoundingRectangle boundingRectangle,
+ ref Matrix2 transformMatrix)
+ {
+ BoundingRectangle result;
+ Transform(ref boundingRectangle, ref transformMatrix, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> that contains the two specified
+ /// <see cref="BoundingRectangle" /> structures.
+ /// </summary>
+ /// <param name="first">The first bounding rectangle.</param>
+ /// <param name="second">The second bounding rectangle.</param>
+ /// <param name="result">The resulting bounding rectangle that contains both the <paramref name="first" /> and the
+ /// <paramref name="second" />.</param>
+ public static void Union(ref BoundingRectangle first, ref BoundingRectangle second, out BoundingRectangle result)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 6.5; Bounding Volume Hierarchies - Merging Bounding Volumes. pg 267
+
+ var firstMinimum = first.Center - first.HalfExtents;
+ var firstMaximum = first.Center + first.HalfExtents;
+ var secondMinimum = second.Center - second.HalfExtents;
+ var secondMaximum = second.Center + second.HalfExtents;
+
+ var minimum = Point2.Minimum(firstMinimum, secondMinimum);
+ var maximum = Point2.Maximum(firstMaximum, secondMaximum);
+
+ result = CreateFrom(minimum, maximum);
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> that contains the two specified
+ /// <see cref="BoundingRectangle" /> structures.
+ /// </summary>
+ /// <param name="first">The first bounding rectangle.</param>
+ /// <param name="second">The second bounding rectangle.</param>
+ /// <returns>
+ /// A <see cref="BoundingRectangle" /> that contains both the <paramref name="first" /> and the
+ /// <paramref name="second" />.
+ /// </returns>
+ public static BoundingRectangle Union(BoundingRectangle first, BoundingRectangle second)
+ {
+ BoundingRectangle result;
+ Union(ref first, ref second, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> that contains both the specified
+ /// <see cref="BoundingRectangle" /> and this <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <returns>
+ /// A <see cref="BoundingRectangle" /> that contains both the <paramref name="boundingRectangle" /> and
+ /// this
+ /// <see cref="BoundingRectangle" />.
+ /// </returns>
+ public BoundingRectangle Union(BoundingRectangle boundingRectangle)
+ {
+ return Union(this, boundingRectangle);
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> that is in common between the two specified
+ /// <see cref="BoundingRectangle" /> structures.
+ /// </summary>
+ /// <param name="first">The first bounding rectangle.</param>
+ /// <param name="second">The second bounding rectangle.</param>
+ /// <param name="result">The resulting bounding rectangle that is in common between both the <paramref name="first" /> and
+ /// the <paramref name="second" />, if they intersect; otherwise, <see cref="Empty"/>.</param>
+ public static void Intersection(ref BoundingRectangle first,
+ ref BoundingRectangle second, out BoundingRectangle result)
+ {
+ var firstMinimum = first.Center - first.HalfExtents;
+ var firstMaximum = first.Center + first.HalfExtents;
+ var secondMinimum = second.Center - second.HalfExtents;
+ var secondMaximum = second.Center + second.HalfExtents;
+
+ var minimum = Point2.Maximum(firstMinimum, secondMinimum);
+ var maximum = Point2.Minimum(firstMaximum, secondMaximum);
+
+ if ((maximum.X < minimum.X) || (maximum.Y < minimum.Y))
+ result = new BoundingRectangle();
+ else
+ result = CreateFrom(minimum, maximum);
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> that is in common between the two specified
+ /// <see cref="BoundingRectangle" /> structures.
+ /// </summary>
+ /// <param name="first">The first bounding rectangle.</param>
+ /// <param name="second">The second bounding rectangle.</param>
+ /// <returns>
+ /// A <see cref="BoundingRectangle" /> that is in common between both the <paramref name="first" /> and
+ /// the <paramref name="second" />, if they intersect; otherwise, <see cref="Empty"/>.
+ /// </returns>
+ public static BoundingRectangle Intersection(BoundingRectangle first,
+ BoundingRectangle second)
+ {
+ BoundingRectangle result;
+ Intersection(ref first, ref second, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="BoundingRectangle" /> that is in common between the specified
+ /// <see cref="BoundingRectangle" /> and this <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <returns>
+ /// A <see cref="BoundingRectangle" /> that is in common between both the <paramref name="boundingRectangle" /> and
+ /// this <see cref="BoundingRectangle"/>, if they intersect; otherwise, <see cref="Empty"/>.
+ /// </returns>
+ public BoundingRectangle Intersection(BoundingRectangle boundingRectangle)
+ {
+ BoundingRectangle result;
+ Intersection(ref this, ref boundingRectangle, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Determines whether the two specified <see cref="BoundingRectangle" /> structures intersect.
+ /// </summary>
+ /// <param name="first">The first bounding rectangle.</param>
+ /// <param name="second">The second bounding rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="first" /> intersects with the <see cref="second" />; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool Intersects(ref BoundingRectangle first, ref BoundingRectangle second)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.2; Bounding Volumes - Axis-aligned Bounding Boxes (AABBs). pg 80
+
+ var distance = first.Center - second.Center;
+ var radii = first.HalfExtents + second.HalfExtents;
+ return Math.Abs(distance.X) <= radii.X && Math.Abs(distance.Y) <= radii.Y;
+ }
+
+ /// <summary>
+ /// Determines whether the two specified <see cref="BoundingRectangle" /> structures intersect.
+ /// </summary>
+ /// <param name="first">The first bounding rectangle.</param>
+ /// <param name="second">The second bounding rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="first" /> intersects with the <see cref="second" />; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool Intersects(BoundingRectangle first, BoundingRectangle second)
+ {
+ return Intersects(ref first, ref second);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="BoundingRectangle" /> intersects with this
+ /// <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="boundingRectangle" /> intersects with this
+ /// <see cref="BoundingRectangle" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Intersects(ref BoundingRectangle boundingRectangle)
+ {
+ return Intersects(ref this, ref boundingRectangle);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="BoundingRectangle" /> intersects with this
+ /// <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="boundingRectangle" /> intersects with this
+ /// <see cref="BoundingRectangle" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Intersects(BoundingRectangle boundingRectangle)
+ {
+ return Intersects(ref this, ref boundingRectangle);
+ }
+
+ /// <summary>
+ /// Updates this <see cref="BoundingRectangle" /> from a list of <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="points">The points.</param>
+ public void UpdateFromPoints(IReadOnlyList<Point2> points)
+ {
+ var boundingRectangle = CreateFrom(points);
+ Center = boundingRectangle.Center;
+ HalfExtents = boundingRectangle.HalfExtents;
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="BoundingRectangle" /> contains the specified
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="boundingRectangle" /> contains the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public static bool Contains(ref BoundingRectangle boundingRectangle, ref Point2 point)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.2; Bounding Volumes - Axis-aligned Bounding Boxes (AABBs). pg 78
+
+ var distance = boundingRectangle.Center - point;
+ var radii = boundingRectangle.HalfExtents;
+
+ return (Math.Abs(distance.X) <= radii.X) && (Math.Abs(distance.Y) <= radii.Y);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="BoundingRectangle" /> contains the specified
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="boundingRectangle" /> contains the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public static bool Contains(BoundingRectangle boundingRectangle, Point2 point)
+ {
+ return Contains(ref boundingRectangle, ref point);
+ }
+
+ /// <summary>
+ /// Determines whether this <see cref="BoundingRectangle" /> contains the specified <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="BoundingRectangle" /> contains the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Contains(Point2 point)
+ {
+ return Contains(this, point);
+ }
+
+ /// <summary>
+ /// Computes the squared distance from this <see cref="BoundingRectangle"/> to a <see cref="Point2"/>.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>The squared distance from this <see cref="BoundingRectangle"/> to the <paramref name="point"/>.</returns>
+ public float SquaredDistanceTo(Point2 point)
+ {
+ return PrimitivesHelper.SquaredDistanceToPointFromRectangle(Center - HalfExtents, Center + HalfExtents, point);
+ }
+
+ /// <summary>
+ /// Computes the closest <see cref="Point2" /> on this <see cref="BoundingRectangle" /> to a specified
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>The closest <see cref="Point2" /> on this <see cref="BoundingRectangle" /> to the <paramref name="point" />.</returns>
+ public Point2 ClosestPointTo(Point2 point)
+ {
+ Point2 result;
+ PrimitivesHelper.ClosestPointToPointFromRectangle(Center - HalfExtents, Center + HalfExtents, point, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="BoundingRectangle" /> structures. The result specifies whether the values of the
+ /// <see cref="Center" /> and <see cref="HalfExtents" /> fields of the two <see cref="BoundingRectangle" /> structures
+ /// are equal.
+ /// </summary>
+ /// <param name="first">The first bounding rectangle.</param>
+ /// <param name="second">The second bounding rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Center" /> and <see cref="HalfExtents" /> fields of the two
+ /// <see cref="BoundingRectangle" /> structures are equal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(BoundingRectangle first, BoundingRectangle second)
+ {
+ return first.Equals(ref second);
+ }
+
+ /// <summary>
+ /// Compares two <see cref="BoundingRectangle" /> structures. The result specifies whether the values of the
+ /// <see cref="Center" /> and <see cref="HalfExtents" /> fields of the two <see cref="BoundingRectangle" /> structures
+ /// are unequal.
+ /// </summary>
+ /// <param name="first">The first bounding rectangle.</param>
+ /// <param name="second">The second bounding rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Center" /> and <see cref="HalfExtents" /> fields of the two
+ /// <see cref="BoundingRectangle" /> structures are unequal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(BoundingRectangle first, BoundingRectangle second)
+ {
+ return !(first == second);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="BoundingRectangle" /> is equal to another
+ /// <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="BoundingRectangle" /> is equal to the <paramref name="boundingRectangle" />;
+ /// otherwise, <c>false</c>.
+ /// </returns>
+ public bool Equals(BoundingRectangle boundingRectangle)
+ {
+ return Equals(ref boundingRectangle);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="BoundingRectangle" /> is equal to another <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="BoundingRectangle" /> is equal to the <paramref name="boundingRectangle" />;
+ /// otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(ref BoundingRectangle boundingRectangle)
+ {
+ return (boundingRectangle.Center == Center) && (boundingRectangle.HalfExtents == HalfExtents);
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="BoundingRectangle" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object to make the comparison with.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="BoundingRectangle" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ if (obj is BoundingRectangle)
+ return Equals((BoundingRectangle)obj);
+ return false;
+ }
+
+ /// <summary>
+ /// Returns a hash code of this <see cref="BoundingRectangle" /> suitable for use in hashing algorithms and data
+ /// structures like a hash table.
+ /// </summary>
+ /// <returns>
+ /// A hash code of this <see cref="BoundingRectangle" />.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (Center.GetHashCode() * 397) ^ HalfExtents.GetHashCode();
+ }
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Rectangle" /> to a <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// The resulting <see cref="BoundingRectangle" />.
+ /// </returns>
+ public static implicit operator BoundingRectangle(Rectangle rectangle)
+ {
+ var radii = new Size2(rectangle.Width * 0.5f, rectangle.Height * 0.5f);
+ var centre = new Point2(rectangle.X + radii.Width, rectangle.Y + radii.Height);
+ return new BoundingRectangle(centre, radii);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="BoundingRectangle" /> to a <see cref="Rectangle" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <returns>
+ /// The resulting <see cref="Rectangle" />.
+ /// </returns>
+ public static implicit operator Rectangle(BoundingRectangle boundingRectangle)
+ {
+ var minimum = boundingRectangle.Center - boundingRectangle.HalfExtents;
+ return new Rectangle((int)minimum.X, (int)minimum.Y, (int)boundingRectangle.HalfExtents.X * 2,
+ (int)boundingRectangle.HalfExtents.Y * 2);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="RectangleF" /> to a <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// The resulting <see cref="BoundingRectangle" />.
+ /// </returns>
+ public static implicit operator BoundingRectangle(RectangleF rectangle)
+ {
+ var radii = new Size2(rectangle.Width * 0.5f, rectangle.Height * 0.5f);
+ var centre = new Point2(rectangle.X + radii.Width, rectangle.Y + radii.Height);
+ return new BoundingRectangle(centre, radii);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="BoundingRectangle" /> to a <see cref="RectangleF" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <returns>
+ /// The resulting <see cref="Rectangle" />.
+ /// </returns>
+ public static implicit operator RectangleF(BoundingRectangle boundingRectangle)
+ {
+ var minimum = boundingRectangle.Center - boundingRectangle.HalfExtents;
+ return new RectangleF(minimum.X, minimum.Y, boundingRectangle.HalfExtents.X * 2,
+ boundingRectangle.HalfExtents.Y * 2);
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="BoundingRectangle" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"Centre: {Center}, Radii: {HalfExtents}";
+ }
+
+ internal string DebugDisplayString => ToString();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/CircleF.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/CircleF.cs
new file mode 100644
index 0000000..70f9307
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/CircleF.cs
@@ -0,0 +1,519 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.3; Bounding Volumes - Spheres. pg 88
+
+ /// <summary>
+ /// A two dimensional circle defined by a centre <see cref="Point2" /> and a radius <see cref="float" />.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// An <see cref="CircleF" /> is categorized by the set of all points in a plane that are at equal distance from
+ /// the
+ /// centre.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="IEquatable{T}" />
+ /// <seealso cref="IEquatableByRef{T}" />
+ [DataContract]
+ public struct CircleF : IEquatable<CircleF>, IEquatableByRef<CircleF>, IShapeF
+ {
+ /// <summary>
+ /// The centre position of this <see cref="CircleF" />.
+ /// </summary>
+ [DataMember] public Point2 Center;
+
+ /// <summary>
+ /// The distance from the <see cref="Center" /> point to any point on the boundary of this <see cref="CircleF" />.
+ /// </summary>
+ [DataMember] public float Radius;
+
+ /// <summary>
+ /// Gets or sets the position of the circle.
+ /// </summary>
+ public Point2 Position
+ {
+ get => Center;
+ set => Center = value;
+ }
+
+ public RectangleF BoundingRectangle
+ {
+ get
+ {
+ var minX = Center.X - Radius;
+ var minY = Center.Y - Radius;
+ return new RectangleF(minX, minY, Diameter, Diameter);
+ }
+ }
+
+ /// <summary>
+ /// Gets the distance from a point to the opposite point, both on the boundary of this <see cref="CircleF" />.
+ /// </summary>
+ public float Diameter => 2 * Radius;
+
+ /// <summary>
+ /// Gets the distance around the boundary of this <see cref="CircleF" />.
+ /// </summary>
+ public float Circumference => 2 * MathHelper.Pi * Radius;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CircleF" /> structure from the specified centre
+ /// <see cref="Point2" /> and the radius <see cref="float" />.
+ /// </summary>
+ /// <param name="center">The centre point.</param>
+ /// <param name="radius">The radius.</param>
+ public CircleF(Point2 center, float radius)
+ {
+ Center = center;
+ Radius = radius;
+ }
+
+ /// <summary>
+ /// Computes the bounding <see cref="CircleF" /> from a minimum <see cref="Point2" /> and maximum
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="minimum">The minimum point.</param>
+ /// <param name="maximum">The maximum point.</param>
+ /// <param name="result">The resulting circle.</param>
+ public static void CreateFrom(Point2 minimum, Point2 maximum, out CircleF result)
+ {
+ result.Center = new Point2((maximum.X + minimum.X) * 0.5f, (maximum.Y + minimum.Y) * 0.5f);
+ var distanceVector = maximum - minimum;
+ result.Radius = distanceVector.X > distanceVector.Y ? distanceVector.X * 0.5f : distanceVector.Y * 0.5f;
+ }
+
+ /// <summary>
+ /// Computes the bounding <see cref="CircleF" /> from a minimum <see cref="Point2" /> and maximum
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="minimum">The minimum point.</param>
+ /// <param name="maximum">The maximum point.</param>
+ /// <returns>An <see cref="CircleF" />.</returns>
+ public static CircleF CreateFrom(Point2 minimum, Point2 maximum)
+ {
+ CircleF result;
+ CreateFrom(minimum, maximum, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the bounding <see cref="CircleF" /> from a list of <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="points">The points.</param>
+ /// <param name="result">The resulting circle.</param>
+ public static void CreateFrom(IReadOnlyList<Point2> points, out CircleF result)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.3; Bounding Volumes - Spheres. pg 89-90
+
+ if (points == null || points.Count == 0)
+ {
+ result = default(CircleF);
+ return;
+ }
+
+ var minimum = new Point2(float.MaxValue, float.MaxValue);
+ var maximum = new Point2(float.MinValue, float.MinValue);
+
+ // ReSharper disable once ForCanBeConvertedToForeach
+ for (var index = points.Count - 1; index >= 0; --index)
+ {
+ var point = points[index];
+ minimum = Point2.Minimum(minimum, point);
+ maximum = Point2.Maximum(maximum, point);
+ }
+
+ CreateFrom(minimum, maximum, out result);
+ }
+
+ /// <summary>
+ /// Computes the bounding <see cref="CircleF" /> from a list of <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="points">The points.</param>
+ /// <returns>An <see cref="CircleF" />.</returns>
+ public static CircleF CreateFrom(IReadOnlyList<Point2> points)
+ {
+ CircleF result;
+ CreateFrom(points, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Determines whether the two specified <see cref="CircleF" /> structures intersect.
+ /// </summary>
+ /// <param name="first">The first circle.</param>
+ /// <param name="second">The second circle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="first" /> intersects with the <see cref="second" />; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool Intersects(ref CircleF first, ref CircleF second)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.3; Bounding Volumes - Spheres. pg 88
+
+ // Calculate squared distance between centers
+ var distanceVector = first.Center - second.Center;
+ var distanceSquared = distanceVector.Dot(distanceVector);
+ var radiusSum = first.Radius + second.Radius;
+ return distanceSquared <= radiusSum * radiusSum;
+ }
+
+ /// <summary>
+ /// Determines whether the two specified <see cref="CircleF" /> structures intersect.
+ /// </summary>
+ /// <param name="first">The first circle.</param>
+ /// <param name="second">The second circle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="first" /> intersects with the <see cref="second" />; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool Intersects(CircleF first, CircleF second)
+ {
+ return Intersects(ref first, ref second);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="CircleF" /> intersects with this <see cref="CircleF" />.
+ /// </summary>
+ /// <param name="circle">The circle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="circle" /> intersects with this <see cref="CircleF" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Intersects(ref CircleF circle)
+ {
+ return Intersects(ref this, ref circle);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="CircleF" /> intersects with this <see cref="CircleF" />.
+ /// </summary>
+ /// <param name="circle">The circle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="circle" /> intersects with this <see cref="CircleF" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Intersects(CircleF circle)
+ {
+ return Intersects(ref this, ref circle);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="CircleF" /> and <see cref="BoundingRectangle" /> structures intersect.
+ /// </summary>
+ /// <param name="circle">The circle.</param>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="circle" /> intersects with the <see cref="rectangle" />; otherwise, <c>false</c>
+ /// .
+ /// </returns>
+ public static bool Intersects(ref CircleF circle, ref BoundingRectangle rectangle)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.25; Basic Primitives Test - Testing Sphere Against AABB. pg 165-166
+
+ // Compute squared distance between sphere center and AABB boundary
+ var distanceSquared = rectangle.SquaredDistanceTo(circle.Center);
+ // Circle and AABB intersect if the (squared) distance between the AABB's boundary and the circle is less than the (squared) circle's radius
+ return distanceSquared <= circle.Radius * circle.Radius;
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="CircleF" /> and <see cref="BoundingRectangle" /> structures intersect.
+ /// </summary>
+ /// <param name="circle">The circle.</param>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="circle" /> intersects with the <see cref="rectangle" />; otherwise, <c>false</c>
+ /// .
+ /// </returns>
+ public static bool Intersects(CircleF circle, BoundingRectangle rectangle)
+ {
+ return Intersects(ref circle, ref rectangle);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="CircleF" /> intersects with this <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="rectangle" /> intersects with this <see cref="CircleF" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Intersects(ref BoundingRectangle rectangle)
+ {
+ return Intersects(ref this, ref rectangle);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="CircleF" /> intersects with this <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="rectangle" /> intersects with this <see cref="CircleF" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Intersects(BoundingRectangle rectangle)
+ {
+ return Intersects(ref this, ref rectangle);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="CircleF" /> contains the specified
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="circle">The circle.</param>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="circle" /> contains the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public static bool Contains(ref CircleF circle, Point2 point)
+ {
+ var dx = circle.Center.X - point.X;
+ var dy = circle.Center.Y - point.Y;
+ var d2 = dx * dx + dy * dy;
+ var r2 = circle.Radius * circle.Radius;
+ return d2 <= r2;
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="CircleF" /> contains the specified
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="circle">The circle.</param>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="circle" /> contains the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public static bool Contains(CircleF circle, Point2 point)
+ {
+ return Contains(ref circle, point);
+ }
+
+ /// <summary>
+ /// Determines whether this <see cref="CircleF" /> contains the specified <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="BoundingRectangle" /> contains the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Contains(Point2 point)
+ {
+ return Contains(ref this, point);
+ }
+
+ /// <summary>
+ /// Computes the closest <see cref="Point2" /> on this <see cref="CircleF" /> to a specified
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>The closest <see cref="Point2" /> on this <see cref="CircleF" /> to the <paramref name="point" />.</returns>
+ public Point2 ClosestPointTo(Point2 point)
+ {
+ var distanceVector = point - Center;
+ var lengthSquared = distanceVector.Dot(distanceVector);
+ if (lengthSquared <= Radius * Radius)
+ return point;
+ distanceVector.Normalize();
+ return Center + Radius * distanceVector;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="Point2" /> on the boundary of of this <see cref="CircleF" /> using the specified angle.
+ /// </summary>
+ /// <param name="angle">The angle in radians.</param>
+ /// <returns>The <see cref="Point2" /> on the boundary of this <see cref="CircleF" /> using <paramref name="angle" />.</returns>
+ public Point2 BoundaryPointAt(float angle)
+ {
+ var direction = new Vector2((float) Math.Cos(angle), (float) Math.Sin(angle));
+ return Center + Radius * direction;
+ }
+
+ [Obsolete("Circle.GetPointAlongEdge() may be removed in the future. Use BoundaryPointAt() instead.")]
+ public Point2 GetPointAlongEdge(float angle)
+ {
+ return Center + new Vector2(Radius * (float) Math.Cos(angle), Radius * (float) Math.Sin(angle));
+ }
+
+ /// <summary>
+ /// Compares two <see cref="CircleF" /> structures. The result specifies whether the values of the
+ /// <see cref="Center" /> and <see cref="Radius" /> fields of the two <see cref="CircleF" /> structures
+ /// are equal.
+ /// </summary>
+ /// <param name="first">The first circle.</param>
+ /// <param name="second">The second circle.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Center" /> and <see cref="Radius" /> fields of the two
+ /// <see cref="BoundingRectangle" /> structures are equal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(CircleF first, CircleF second)
+ {
+ return first.Equals(ref second);
+ }
+
+ /// <summary>
+ /// Compares two <see cref="CircleF" /> structures. The result specifies whether the values of the
+ /// <see cref="Center" /> and <see cref="Radius" /> fields of the two <see cref="CircleF" /> structures
+ /// are unequal.
+ /// </summary>
+ /// <param name="first">The first circle.</param>
+ /// <param name="second">The second circle.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Center" /> and <see cref="Radius" /> fields of the two
+ /// <see cref="CircleF" /> structures are unequal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(CircleF first, CircleF second)
+ {
+ return !(first == second);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="CircleF" /> is equal to another <see cref="CircleF" />.
+ /// </summary>
+ /// <param name="circle">The circle.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="CircleF" /> is equal to the <paramref name="circle" />; otherwise, <c>false</c>.
+ /// </returns>
+ public bool Equals(CircleF circle)
+ {
+ return Equals(ref circle);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="CircleF" /> is equal to another <see cref="CircleF" />.
+ /// </summary>
+ /// <param name="circle">The bounding rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="CircleF" /> is equal to the <paramref name="circle" />;
+ /// otherwise,<c>false</c>.
+ /// </returns>
+ public bool Equals(ref CircleF circle)
+ {
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ return circle.Center == Center && circle.Radius == Radius;
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="CircleF" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object to make the comparison with.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="CircleF" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ return obj is CircleF && Equals((CircleF) obj);
+ }
+
+ /// <summary>
+ /// Returns a hash code of this <see cref="CircleF" /> suitable for use in hashing algorithms and data
+ /// structures like a hash table.
+ /// </summary>
+ /// <returns>
+ /// A hash code of this <see cref="CircleF" />.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (Center.GetHashCode() * 397) ^ Radius.GetHashCode();
+ }
+ }
+
+ /// <summary>
+ /// Performs an explicit conversion from a <see cref="CircleF" /> to a <see cref="Rectangle" />.
+ /// </summary>
+ /// <param name="circle">The circle.</param>
+ /// <returns>
+ /// The resulting <see cref="Rectangle" />.
+ /// </returns>
+ public static explicit operator Rectangle(CircleF circle)
+ {
+ var diameter = (int) circle.Diameter;
+ return new Rectangle((int) (circle.Center.X - circle.Radius), (int) (circle.Center.Y - circle.Radius),
+ diameter, diameter);
+ }
+
+ /// <summary>
+ /// Performs a conversion from a specified <see cref="CircleF" /> to a <see cref="Rectangle" />.
+ /// </summary>
+ /// <returns>
+ /// The resulting <see cref="Rectangle" />.
+ /// </returns>
+ public Rectangle ToRectangle()
+ {
+ return (Rectangle)this;
+ }
+
+ /// <summary>
+ /// Performs an explicit conversion from a <see cref="Rectangle" /> to a <see cref="CircleF" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// The resulting <see cref="CircleF" />.
+ /// </returns>
+ public static explicit operator CircleF(Rectangle rectangle)
+ {
+ var halfWidth = rectangle.Width / 2;
+ var halfHeight = rectangle.Height / 2;
+ return new CircleF(new Point2(rectangle.X + halfWidth, rectangle.Y + halfHeight),
+ halfWidth > halfHeight ? halfWidth : halfHeight);
+ }
+
+ /// <summary>
+ /// Performs an explicit conversion from a <see cref="CircleF" /> to a <see cref="RectangleF" />.
+ /// </summary>
+ /// <param name="circle">The circle.</param>
+ /// <returns>
+ /// The resulting <see cref="RectangleF" />.
+ /// </returns>
+ public static explicit operator RectangleF(CircleF circle)
+ {
+ var diameter = circle.Diameter;
+ return new RectangleF(circle.Center.X - circle.Radius, circle.Center.Y - circle.Radius, diameter, diameter);
+ }
+
+ /// <summary>
+ /// Performs a conversion from a specified <see cref="CircleF" /> to a <see cref="RectangleF" />.
+ /// </summary>
+ /// <returns>
+ /// The resulting <see cref="RectangleF" />.
+ /// </returns>
+ public RectangleF ToRectangleF()
+ {
+ return (RectangleF)this;
+ }
+
+ /// <summary>
+ /// Performs an explicit conversion from a <see cref="RectangleF" /> to a <see cref="CircleF" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// The resulting <see cref="CircleF" />.
+ /// </returns>
+ public static explicit operator CircleF(RectangleF rectangle)
+ {
+ var halfWidth = rectangle.Width * 0.5f;
+ var halfHeight = rectangle.Height * 0.5f;
+ return new CircleF(new Point2(rectangle.X + halfWidth, rectangle.Y + halfHeight),
+ halfWidth > halfHeight ? halfWidth : halfHeight);
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="CircleF" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="CircleF" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"Centre: {Center}, Radius: {Radius}";
+ }
+
+ internal string DebugDisplayString => ToString();
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/EllipseF.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/EllipseF.cs
new file mode 100644
index 0000000..684a770
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/EllipseF.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Runtime.Serialization;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ [DataContract]
+ public struct EllipseF : IEquatable<EllipseF>, IEquatableByRef<EllipseF>, IShapeF
+ {
+ [DataMember] public Vector2 Center { get; set; }
+ [DataMember] public float RadiusX { get; set; }
+ [DataMember] public float RadiusY { get; set; }
+
+ public Point2 Position
+ {
+ get => Center;
+ set => Center = value;
+ }
+
+ public EllipseF(Vector2 center, float radiusX, float radiusY)
+ {
+ Center = center;
+ RadiusX = radiusX;
+ RadiusY = radiusY;
+ }
+
+ public float Left => Center.X - RadiusX;
+ public float Top => Center.Y - RadiusY;
+ public float Right => Center.X + RadiusX;
+ public float Bottom => Center.Y + RadiusY;
+
+ public RectangleF BoundingRectangle
+ {
+ get
+ {
+ var minX = Left;
+ var minY = Top;
+ var maxX = Right;
+ var maxY = Bottom;
+ return new RectangleF(minX, minY, maxX - minX, maxY - minY);
+ }
+ }
+
+ public bool Contains(float x, float y)
+ {
+ float xCalc = (float) (Math.Pow(x - Center.X, 2) / Math.Pow(RadiusX, 2));
+ float yCalc = (float) (Math.Pow(y - Center.Y, 2) / Math.Pow(RadiusY, 2));
+
+ return xCalc + yCalc <= 1;
+ }
+
+ public bool Contains(Vector2 point)
+ {
+ return Contains(point.X, point.Y);
+ }
+
+ public bool Equals(EllipseF ellispse)
+ {
+ return Equals(ref ellispse);
+ }
+
+ public bool Equals(ref EllipseF ellispse)
+ {
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ return ellispse.Center == Center
+ && ellispse.RadiusX == RadiusX
+ && ellispse.RadiusY == RadiusY;
+ }
+
+ public override bool Equals(object obj)
+ {
+ return obj is EllipseF && Equals((EllipseF)obj);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = Center.GetHashCode();
+ hashCode = (hashCode * 397) ^ RadiusX.GetHashCode();
+ hashCode = (hashCode * 397) ^ RadiusY.GetHashCode();
+ return hashCode;
+ }
+ }
+
+ public override string ToString()
+ {
+ return $"Centre: {Center}, RadiusX: {RadiusX}, RadiusY: {RadiusY}";
+ }
+
+ public static bool operator ==(EllipseF first, EllipseF second)
+ {
+ return first.Equals(ref second);
+ }
+
+ public static bool operator !=(EllipseF first, EllipseF second)
+ {
+ return !(first == second);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FastRandom.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FastRandom.cs
new file mode 100644
index 0000000..472518b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FastRandom.cs
@@ -0,0 +1,127 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// A random number generator that uses a fast algorithm to generate random values.
+ /// The speed comes at the price of true 'randomness' though, there are noticeable
+ /// patterns & it compares quite unfavourably to other algorithms in that respect.
+ /// It's a good choice in situations where speed is more desirable than a
+ /// good random distribution, and a poor choice when random distribution is important.
+ /// </summary>
+ public class FastRandom
+ {
+ private int _state;
+
+ public FastRandom()
+ : this(1)
+ {
+ }
+
+ public FastRandom(int seed)
+ {
+ if (seed < 1)
+ throw new ArgumentOutOfRangeException(nameof(seed), "seed must be greater than zero");
+
+ _state = seed;
+ }
+
+ /// <summary>
+ /// Gets the next random integer value.
+ /// </summary>
+ /// <returns>A random positive integer.</returns>
+ public int Next()
+ {
+ _state = 214013*_state + 2531011;
+ return (_state >> 16) & 0x7FFF;
+ }
+
+ /// <summary>
+ /// Gets the next random integer value which is greater than zero and less than or equal to
+ /// the specified maxmimum value.
+ /// </summary>
+ /// <param name="max">The maximum random integer value to return.</param>
+ /// <returns>A random integer value between zero and the specified maximum value.</returns>
+ public int Next(int max)
+ {
+ return (int) (max*NextSingle() + 0.5f);
+ }
+
+ /// <summary>
+ /// Gets the next random integer between the specified minimum and maximum values.
+ /// </summary>
+ /// <param name="min">The inclusive minimum value.</param>
+ /// <param name="max">The inclusive maximum value.</param>
+ public int Next(int min, int max)
+ {
+ return (int) ((max - min)*NextSingle() + 0.5f) + min;
+ }
+
+ /// <summary>
+ /// Gets the next random integer between the specified range of values.
+ /// </summary>
+ /// <param name="range">A range representing the inclusive minimum and maximum values.</param>
+ /// <returns>A random integer between the specified minumum and maximum values.</returns>
+ public int Next(Range<int> range)
+ {
+ return Next(range.Min, range.Max);
+ }
+
+ /// <summary>
+ /// Gets the next random single value.
+ /// </summary>
+ /// <returns>A random single value between 0 and 1.</returns>
+ public float NextSingle()
+ {
+ return Next()/(float) short.MaxValue;
+ }
+
+ /// <summary>
+ /// Gets the next random single value which is greater than zero and less than or equal to
+ /// the specified maxmimum value.
+ /// </summary>
+ /// <param name="max">The maximum random single value to return.</param>
+ /// <returns>A random single value between zero and the specified maximum value.</returns>
+ public float NextSingle(float max)
+ {
+ return max*NextSingle();
+ }
+
+ /// <summary>
+ /// Gets the next random single value between the specified minimum and maximum values.
+ /// </summary>
+ /// <param name="min">The inclusive minimum value.</param>
+ /// <param name="max">The inclusive maximum value.</param>
+ /// <returns>A random single value between the specified minimum and maximum values.</returns>
+ public float NextSingle(float min, float max)
+ {
+ return (max - min)*NextSingle() + min;
+ }
+
+ /// <summary>
+ /// Gets the next random single value between the specified range of values.
+ /// </summary>
+ /// <param name="range">A range representing the inclusive minimum and maximum values.</param>
+ /// <returns>A random single value between the specified minimum and maximum values.</returns>
+ public float NextSingle(Range<float> range)
+ {
+ return NextSingle(range.Min, range.Max);
+ }
+
+ /// <summary>
+ /// Gets the next random angle value.
+ /// </summary>
+ /// <returns>A random angle value.</returns>
+ public float NextAngle()
+ {
+ return NextSingle(-MathHelper.Pi, MathHelper.Pi);
+ }
+
+ public void NextUnitVector(out Vector2 vector)
+ {
+ var angle = NextAngle();
+ vector = new Vector2((float) Math.Cos(angle), (float) Math.Sin(angle));
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FloatHelper.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FloatHelper.cs
new file mode 100644
index 0000000..a3792e6
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FloatHelper.cs
@@ -0,0 +1,15 @@
+using System.Runtime.CompilerServices;
+
+namespace MonoGame.Extended
+{
+ public static class FloatHelper
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Swap(ref float value1, ref float value2)
+ {
+ var temp = value1;
+ value1 = value2;
+ value2 = temp;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Matrix2.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Matrix2.cs
new file mode 100644
index 0000000..cb56710
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Matrix2.cs
@@ -0,0 +1,1038 @@
+using System;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using Microsoft.Xna.Framework;
+
+// ReSharper disable CompareOfFloatsByEqualityOperator
+
+namespace MonoGame.Extended
+{
+ // https://en.wikipedia.org/wiki/Matrix_(mathematics)
+ // "Immersive Linear Algebra"; Jacob Ström, Kalle Åström & Tomas Akenine-Möller; 2015-2016. Chapter 6: The Matrix. http://immersivemath.com/ila/ch06_matrices/ch06.html
+ // "Real-Time Collision Detection"; Christer Ericson; 2005. Chapter 3.1: A Math and Geometry Primer - Matrices. pg 23-34
+
+ // Original code was from Matrix2D.cs in the Nez Library: https://github.com/prime31/Nez/
+
+ /// <summary>
+ /// Defines a 3x3 matrix using floating point numbers which can store two dimensional translation, scale and rotation
+ /// information in a right-handed coordinate system.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// Matrices use a row vector layout in the XNA / MonoGame Framework but, in general, matrices can be either have
+ /// a row vector or column vector layout. Row vector matrices view vectors as a row from left to right, while
+ /// column vector matrices view vectors as a column from top to bottom. For example, the <see cref="Translation" />
+ /// corresponds to the fields <see cref="M31" /> and <see cref="M32" />.
+ /// </para>
+ /// <para>
+ /// The fields M13 and M23 always have a value of <code>0.0f</code>, and thus are removed from the
+ /// <see cref="Matrix2" /> to reduce its memory footprint. Same is true for the field M33, except it always has a
+ /// value of <code>1.0f</code>.
+ /// </para>
+ /// </remarks>
+ [DebuggerDisplay("{DebugDisplayString,nq}")]
+ public struct Matrix2 : IEquatable<Matrix2>, IEquatableByRef<Matrix2>
+ {
+ public float M11; // x scale, also used for rotation
+ public float M12; // used for rotation
+
+ public float M21; // used for rotation
+ public float M22; // y scale, also used for rotation
+
+ public float M31; // x translation
+ public float M32; // y translation
+
+ /// <summary>
+ /// Gets the identity matrix.
+ /// </summary>
+ /// <value>
+ /// The identity matrix.
+ /// </value>
+ public static Matrix2 Identity { get; } = new Matrix2(1f, 0f, 0f, 1f, 0f, 0f);
+
+ /// <summary>
+ /// Gets the translation.
+ /// </summary>
+ /// <value>
+ /// The translation.
+ /// </value>
+ /// <remarks>The <see cref="Translation" /> is equal to the vector <code>(M31, M32)</code>.</remarks>
+ public Vector2 Translation => new Vector2(M31, M32);
+
+ /// <summary>
+ /// Gets the rotation angle in radians.
+ /// </summary>
+ /// <value>
+ /// The rotation angle in radians.
+ /// </value>
+ /// <remarks>
+ /// The <see cref="Rotation" /> is equal to <code>Atan2(M21, M11)</code>.
+ /// </remarks>
+ public float Rotation => (float)Math.Atan2(M21, M11);
+
+ /// <summary>
+ /// Gets the scale.
+ /// </summary>
+ /// <value>
+ /// The scale.
+ /// </value>
+ /// <remarks>
+ /// The <see cref="Scale" /> is equal to the vector
+ /// <code>(Sqrt(M11 * M11 + M21 * M21), Sqrt(M12 * M12 + M22 * M22))</code>.
+ /// </remarks>
+ public Vector2 Scale
+ {
+ get
+ {
+ var scaleX = (float)Math.Sqrt(M11 * M11 + M21 * M21);
+ var scaleY = (float)Math.Sqrt(M12 * M12 + M22 * M22);
+ return new Vector2(scaleX, scaleY);
+ }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct.
+ /// </summary>
+ /// <param name="m11">The value to initialize <see cref="M11" /> to.</param>
+ /// <param name="m12">The value to initialize <see cref="M12" /> to.</param>
+ /// <param name="m21">The value to initialize <see cref="M21" /> to.</param>
+ /// <param name="m22">The value to initialize <see cref="M22" /> to.</param>
+ /// <param name="m31">The value to initialize <see cref="M31" /> to.</param>
+ /// <param name="m32">The value to initialize <see cref="M32" /> to.</param>
+ /// <remarks>
+ /// <para>
+ /// The fields M13 and M23 always have a value of <code>0.0f</code>, and thus are removed from the
+ /// <see cref="Matrix2" /> to reduce its memory footprint. Same is true for the field M33, except it always has a
+ /// value of <code>1.0f</code>.
+ /// </para>
+ /// </remarks>
+ public Matrix2(float m11, float m12, float m21, float m22, float m31, float m32)
+ {
+ M11 = m11;
+ M12 = m12;
+
+ M21 = m21;
+ M22 = m22;
+
+ M31 = m31;
+ M32 = m32;
+ }
+
+ /// <summary>
+ /// Transforms the specified <see cref="Vector2" /> by this <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="vector">The vector.</param>
+ /// <returns>The resulting <see cref="Vector2" />.</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Vector2 Transform(Vector2 vector)
+ {
+ Vector2 result;
+ Transform(vector, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Transforms the specified <see cref="Vector2" /> by this <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="vector">The vector.</param>
+ /// <param name="result">The resulting <see cref="Vector2" />.</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Transform(Vector2 vector, out Vector2 result)
+ {
+ result.X = vector.X * M11 + vector.Y * M21 + M31;
+ result.Y = vector.X * M12 + vector.Y * M22 + M32;
+ }
+
+ /// <summary>
+ /// Transforms the specified <see cref="Vector2" /> by this <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="x">The x value of the vector.</param>
+ /// <param name="y">The y value of the vector.</param>
+ /// <param name="result">The resulting <see cref="Vector2" />.</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Transform(float x, float y, out Vector2 result)
+ {
+ result.X = x * M11 + y * M21 + M31;
+ result.Y = x * M12 + y * M22 + M32;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Transform(float x, float y, ref Vector3 result)
+ {
+ result.X = x * M11 + y * M21 + M31;
+ result.Y = x * M12 + y * M22 + M32;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to translate, rotate, and scale a set of vertices in two dimensions.
+ /// </summary>
+ /// <param name="position">The amounts to translate by on the x and y axes.</param>
+ /// <param name="rotation">The amount, in radians, in which to rotate around the z-axis.</param>
+ /// <param name="scale">The amount to scale by on the x and y axes.</param>
+ /// <param name="origin">The point which to rotate and scale around.</param>
+ /// <param name="transformMatrix">The resulting <see cref="Matrix2" /></param>
+ public static void CreateFrom(Vector2 position, float rotation, Vector2? scale, Vector2? origin,
+ out Matrix2 transformMatrix)
+ {
+ transformMatrix = Identity;
+
+ if (origin.HasValue)
+ {
+ transformMatrix.M31 = -origin.Value.X;
+ transformMatrix.M32 = -origin.Value.Y;
+ }
+
+ if (scale.HasValue)
+ {
+ var scaleMatrix = CreateScale(scale.Value);
+ Multiply(ref transformMatrix, ref scaleMatrix, out transformMatrix);
+ }
+
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if (rotation != 0f)
+ {
+ var rotationMatrix = CreateRotationZ(-rotation);
+ Multiply(ref transformMatrix, ref rotationMatrix, out transformMatrix);
+ }
+
+ var translationMatrix = CreateTranslation(position);
+ Multiply(ref transformMatrix, ref translationMatrix, out transformMatrix);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to translate, rotate, and scale a set of vertices in two dimensions.
+ /// </summary>
+ /// <param name="position">The amounts to translate by on the x and y axes.</param>
+ /// <param name="rotation">The amount, in radians, in which to rotate around the z-axis.</param>
+ /// <param name="scale">The amount to scale by on the x and y axes.</param>
+ /// <param name="origin">The point which to rotate and scale around.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 CreateFrom(Vector2 position, float rotation, Vector2? scale = null, Vector2? origin = null)
+ {
+ var transformMatrix = Identity;
+
+ if (origin.HasValue)
+ {
+ transformMatrix.M31 = -origin.Value.X;
+ transformMatrix.M32 = -origin.Value.Y;
+ }
+
+ if (scale.HasValue)
+ {
+ var scaleMatrix = CreateScale(scale.Value);
+ transformMatrix = Multiply(transformMatrix, scaleMatrix);
+ }
+
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if (rotation != 0f)
+ {
+ var rotationMatrix = CreateRotationZ(-rotation);
+ transformMatrix = Multiply(transformMatrix, rotationMatrix);
+ }
+
+ var translationMatrix = CreateTranslation(position);
+ transformMatrix = Multiply(transformMatrix, translationMatrix);
+
+ return transformMatrix;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to rotate a set of vertices
+ /// around the z-axis.
+ /// </summary>
+ /// <param name="radians">The amount, in radians, in which to rotate around the z-axis.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 CreateRotationZ(float radians)
+ {
+ Matrix2 result;
+ CreateRotationZ(radians, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Matrix2" /> struct that can be used to rotate a set of vertices around the z-axis.
+ /// </summary>
+ /// <param name="radians">The amount, in radians, in which to rotate around the z-axis.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void CreateRotationZ(float radians, out Matrix2 result)
+ {
+ var val1 = (float)Math.Cos(radians);
+ var val2 = (float)Math.Sin(radians);
+
+ result = new Matrix2
+ {
+ M11 = val1,
+ M12 = val2,
+ M21 = -val2,
+ M22 = val1,
+ M31 = 0,
+ M32 = 0
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to scale a set vertices.
+ /// </summary>
+ /// <param name="scale">The amount to scale by on the x and y axes.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 CreateScale(float scale)
+ {
+ Matrix2 result;
+ CreateScale(scale, scale, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Matrix2" /> struct that can be used to scale a set vertices.
+ /// </summary>
+ /// <param name="scale">The amount to scale by on the x and y axes.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void CreateScale(float scale, out Matrix2 result)
+ {
+ CreateScale(scale, scale, out result);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to scale a set vertices.
+ /// </summary>
+ /// <param name="xScale">The amount to scale by on the x-axis.</param>
+ /// <param name="yScale">The amount to scale by on the y-axis.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 CreateScale(float xScale, float yScale)
+ {
+ Matrix2 result;
+ CreateScale(xScale, yScale, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Matrix2" /> struct that can be used to scale a set vertices.
+ /// </summary>
+ /// <param name="xScale">The amount to scale by on the x-axis.</param>
+ /// <param name="yScale">The amount to scale by on the y-axis.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void CreateScale(float xScale, float yScale, out Matrix2 result)
+ {
+ result = new Matrix2
+ {
+ M11 = xScale,
+ M12 = 0,
+ M21 = 0,
+ M22 = yScale,
+ M31 = 0,
+ M32 = 0
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to scale a set vertices.
+ /// </summary>
+ /// <param name="scale">The amounts to scale by on the x and y axes.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 CreateScale(Vector2 scale)
+ {
+ Matrix2 result;
+ CreateScale(ref scale, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Matrix2" /> struct that can be used to scale a set vertices.
+ /// </summary>
+ /// <param name="scale">The amounts to scale by on the x and y axes.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void CreateScale(ref Vector2 scale, out Matrix2 result)
+ {
+ result = new Matrix2
+ {
+ M11 = scale.X,
+ M12 = 0,
+ M21 = 0,
+ M22 = scale.Y,
+ M31 = 0,
+ M32 = 0
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to translate a set vertices.
+ /// </summary>
+ /// <param name="xPosition">The amount to translate by on the x-axis.</param>
+ /// <param name="yPosition">The amount to translate by on the y-axis.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 CreateTranslation(float xPosition, float yPosition)
+ {
+ Matrix2 result;
+ CreateTranslation(xPosition, yPosition, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Matrix2" /> struct that can be used to translate a set vertices.
+ /// </summary>
+ /// <param name="xPosition">The amount to translate by on the x-axis.</param>
+ /// <param name="yPosition">The amount to translate by on the y-axis.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void CreateTranslation(float xPosition, float yPosition, out Matrix2 result)
+ {
+ result = new Matrix2
+ {
+ M11 = 1,
+ M12 = 0,
+ M21 = 0,
+ M22 = 1,
+ M31 = xPosition,
+ M32 = yPosition
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to translate a set vertices.
+ /// </summary>
+ /// <param name="position">The amounts to translate by on the x and y axes.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 CreateTranslation(Vector2 position)
+ {
+ Matrix2 result;
+ CreateTranslation(ref position, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Matrix2" /> struct that can be used to translate a set vertices.
+ /// </summary>
+ /// <param name="position">The amounts to translate by on the x and y axes.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void CreateTranslation(ref Vector2 position, out Matrix2 result)
+ {
+ result = new Matrix2
+ {
+ M11 = 1,
+ M12 = 0,
+ M21 = 0,
+ M22 = 1,
+ M31 = position.X,
+ M32 = position.Y
+ };
+ }
+
+ /// <summary>
+ /// Calculates the determinant of the <see cref="Matrix2" />.
+ /// </summary>
+ /// <returns>The determinant of the <see cref="Matrix2" />.</returns>
+ public float Determinant()
+ {
+ return M11 * M22 - M12 * M21;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the summation of two <see cref="Matrix2" />
+ /// s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 Add(Matrix2 matrix1, Matrix2 matrix2)
+ {
+ matrix1.M11 += matrix2.M11;
+ matrix1.M12 += matrix2.M12;
+
+ matrix1.M21 += matrix2.M21;
+ matrix1.M22 += matrix2.M22;
+
+ matrix1.M31 += matrix2.M31;
+ matrix1.M32 += matrix2.M32;
+
+ return matrix1;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Matrix2" /> summation of two <see cref="Matrix2" />s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void Add(ref Matrix2 matrix1, ref Matrix2 matrix2, out Matrix2 result)
+ {
+ result = new Matrix2
+ {
+ M11 = matrix1.M11 + matrix2.M11,
+ M12 = matrix1.M12 + matrix2.M12,
+ M21 = matrix1.M21 + matrix2.M21,
+ M22 = matrix1.M22 + matrix2.M22,
+ M31 = matrix1.M31 + matrix2.M31,
+ M32 = matrix1.M32 + matrix2.M32
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the summation of two <see cref="Matrix2" />
+ /// s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 operator +(Matrix2 matrix1, Matrix2 matrix2)
+ {
+ matrix1.M11 = matrix1.M11 + matrix2.M11;
+ matrix1.M12 = matrix1.M12 + matrix2.M12;
+
+ matrix1.M21 = matrix1.M21 + matrix2.M21;
+ matrix1.M22 = matrix1.M22 + matrix2.M22;
+
+ matrix1.M31 = matrix1.M31 + matrix2.M31;
+ matrix1.M32 = matrix1.M32 + matrix2.M32;
+
+ return matrix1;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the substraction of two
+ /// <see cref="Matrix2" />
+ /// s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 Subtract(Matrix2 matrix1, Matrix2 matrix2)
+ {
+ matrix1.M11 = matrix1.M11 - matrix2.M11;
+ matrix1.M12 = matrix1.M12 - matrix2.M12;
+
+ matrix1.M21 = matrix1.M21 - matrix2.M21;
+ matrix1.M22 = matrix1.M22 - matrix2.M22;
+
+ matrix1.M31 = matrix1.M31 - matrix2.M31;
+ matrix1.M32 = matrix1.M32 - matrix2.M32;
+ return matrix1;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Matrix2" /> substraction of two <see cref="Matrix2" />s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void Subtract(ref Matrix2 matrix1, ref Matrix2 matrix2, out Matrix2 result)
+ {
+ result = new Matrix2
+ {
+ M11 = matrix1.M11 - matrix2.M11,
+ M12 = matrix1.M12 - matrix2.M12,
+ M21 = matrix1.M21 - matrix2.M21,
+ M22 = matrix1.M22 - matrix2.M22,
+ M31 = matrix1.M31 - matrix2.M31,
+ M32 = matrix1.M32 - matrix2.M32
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the substraction of two
+ /// <see cref="Matrix2" />
+ /// s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 operator -(Matrix2 matrix1, Matrix2 matrix2)
+ {
+ matrix1.M11 = matrix1.M11 - matrix2.M11;
+ matrix1.M12 = matrix1.M12 - matrix2.M12;
+
+ matrix1.M21 = matrix1.M21 - matrix2.M21;
+ matrix1.M22 = matrix1.M22 - matrix2.M22;
+
+ matrix1.M31 = matrix1.M31 - matrix2.M31;
+ matrix1.M32 = matrix1.M32 - matrix2.M32;
+ return matrix1;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the multiplication of two
+ /// <see cref="Matrix2" />
+ /// s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 Multiply(Matrix2 matrix1, Matrix2 matrix2)
+ {
+ var m11 = matrix1.M11 * matrix2.M11 + matrix1.M12 * matrix2.M21;
+ var m12 = matrix1.M11 * matrix2.M12 + matrix1.M12 * matrix2.M22;
+
+ var m21 = matrix1.M21 * matrix2.M11 + matrix1.M22 * matrix2.M21;
+ var m22 = matrix1.M21 * matrix2.M12 + matrix1.M22 * matrix2.M22;
+
+ var m31 = matrix1.M31 * matrix2.M11 + matrix1.M32 * matrix2.M21 + matrix2.M31;
+ var m32 = matrix1.M31 * matrix2.M12 + matrix1.M32 * matrix2.M22 + matrix2.M32;
+
+ matrix1.M11 = m11;
+ matrix1.M12 = m12;
+
+ matrix1.M21 = m21;
+ matrix1.M22 = m22;
+
+ matrix1.M31 = m31;
+ matrix1.M32 = m32;
+
+ return matrix1;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Matrix2" /> multiplication of two <see cref="Matrix2" />s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void Multiply(ref Matrix2 matrix1, ref Matrix2 matrix2, out Matrix2 result)
+ {
+ var m11 = matrix1.M11 * matrix2.M11 + matrix1.M12 * matrix2.M21;
+ var m12 = matrix1.M11 * matrix2.M12 + matrix1.M12 * matrix2.M22;
+
+ var m21 = matrix1.M21 * matrix2.M11 + matrix1.M22 * matrix2.M21;
+ var m22 = matrix1.M21 * matrix2.M12 + matrix1.M22 * matrix2.M22;
+
+ var m31 = matrix1.M31 * matrix2.M11 + matrix1.M32 * matrix2.M21 + matrix2.M31;
+ var m32 = matrix1.M31 * matrix2.M12 + matrix1.M32 * matrix2.M22 + matrix2.M32;
+
+ result = new Matrix2
+ {
+ M11 = m11,
+ M12 = m12,
+ M21 = m21,
+ M22 = m22,
+ M31 = m31,
+ M32 = m32
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the division of a <see cref="Matrix2" /> by
+ /// a scalar.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <param name="scalar">The amount to divide the <see cref="Matrix2" /> by.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 Multiply(Matrix2 matrix, float scalar)
+ {
+ matrix.M11 *= scalar;
+ matrix.M12 *= scalar;
+
+ matrix.M21 *= scalar;
+ matrix.M22 *= scalar;
+
+ matrix.M31 *= scalar;
+ matrix.M32 *= scalar;
+ return matrix;
+ }
+
+ /// <summary>
+ /// Calculates the multiplication of a <see cref="Matrix2" /> by a scalar.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <param name="scalar">The amount to multiple the <see cref="Matrix2" /> by.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void Multiply(ref Matrix2 matrix, float scalar, out Matrix2 result)
+ {
+ result = new Matrix2
+ {
+ M11 = matrix.M11 * scalar,
+ M12 = matrix.M12 * scalar,
+ M21 = matrix.M21 * scalar,
+ M22 = matrix.M22 * scalar,
+ M31 = matrix.M31 * scalar,
+ M32 = matrix.M32 * scalar
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the multiplication of two
+ /// <see cref="Matrix2" />
+ /// s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 operator *(Matrix2 matrix1, Matrix2 matrix2)
+ {
+ var m11 = matrix1.M11 * matrix2.M11 + matrix1.M12 * matrix2.M21;
+ var m12 = matrix1.M11 * matrix2.M12 + matrix1.M12 * matrix2.M22;
+
+ var m21 = matrix1.M21 * matrix2.M11 + matrix1.M22 * matrix2.M21;
+ var m22 = matrix1.M21 * matrix2.M12 + matrix1.M22 * matrix2.M22;
+
+ var m31 = matrix1.M31 * matrix2.M11 + matrix1.M32 * matrix2.M21 + matrix2.M31;
+ var m32 = matrix1.M31 * matrix2.M12 + matrix1.M32 * matrix2.M22 + matrix2.M32;
+
+ matrix1.M11 = m11;
+ matrix1.M12 = m12;
+
+ matrix1.M21 = m21;
+ matrix1.M22 = m22;
+
+ matrix1.M31 = m31;
+ matrix1.M32 = m32;
+
+ return matrix1;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the division of a <see cref="Matrix2" /> by
+ /// a scalar.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <param name="scalar">The amount to divide the <see cref="Matrix2" /> by.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 operator *(Matrix2 matrix, float scalar)
+ {
+ matrix.M11 = matrix.M11 * scalar;
+ matrix.M12 = matrix.M12 * scalar;
+
+ matrix.M21 = matrix.M21 * scalar;
+ matrix.M22 = matrix.M22 * scalar;
+
+ matrix.M31 = matrix.M31 * scalar;
+ matrix.M32 = matrix.M32 * scalar;
+
+ return matrix;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the division of two <see cref="Matrix2" />
+ /// s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 Divide(Matrix2 matrix1, Matrix2 matrix2)
+ {
+ matrix1.M11 = matrix1.M11 / matrix2.M11;
+ matrix1.M12 = matrix1.M12 / matrix2.M12;
+
+ matrix1.M21 = matrix1.M21 / matrix2.M21;
+ matrix1.M22 = matrix1.M22 / matrix2.M22;
+
+ matrix1.M31 = matrix1.M31 / matrix2.M31;
+ matrix1.M32 = matrix1.M32 / matrix2.M32;
+ return matrix1;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Matrix2" /> division of two <see cref="Matrix2" />s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void Divide(ref Matrix2 matrix1, ref Matrix2 matrix2, out Matrix2 result)
+ {
+ result = new Matrix2
+ {
+ M11 = matrix1.M11 / matrix2.M11,
+ M12 = matrix1.M12 / matrix2.M12,
+ M21 = matrix1.M21 / matrix2.M21,
+ M22 = matrix1.M22 / matrix2.M22,
+ M31 = matrix1.M31 / matrix2.M31,
+ M32 = matrix1.M32 / matrix2.M32
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the division of a <see cref="Matrix2" /> by
+ /// a scalar.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <param name="scalar">The amount to divide the <see cref="Matrix2" /> by.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 Divide(Matrix2 matrix, float scalar)
+ {
+ var num = 1f / scalar;
+ matrix.M11 = matrix.M11 * num;
+ matrix.M12 = matrix.M12 * num;
+
+ matrix.M21 = matrix.M21 * num;
+ matrix.M22 = matrix.M22 * num;
+
+ matrix.M31 = matrix.M31 * num;
+ matrix.M32 = matrix.M32 * num;
+
+ return matrix;
+ }
+
+ /// <summary>
+ /// Calculates the division of a <see cref="Matrix2" /> by a scalar.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <param name="scalar">The amount to divide the <see cref="Matrix2" /> by.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void Divide(ref Matrix2 matrix, float scalar, out Matrix2 result)
+ {
+ var num = 1f / scalar;
+ result = new Matrix2
+ {
+ M11 = matrix.M11 * num,
+ M12 = matrix.M12 * num,
+ M21 = matrix.M21 * num,
+ M22 = matrix.M22 * num,
+ M31 = matrix.M31 * num,
+ M32 = matrix.M32 * num
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the division of two <see cref="Matrix2" />
+ /// s.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 operator /(Matrix2 matrix1, Matrix2 matrix2)
+ {
+ matrix1.M11 = matrix1.M11 / matrix2.M11;
+ matrix1.M12 = matrix1.M12 / matrix2.M12;
+
+ matrix1.M21 = matrix1.M21 / matrix2.M21;
+ matrix1.M22 = matrix1.M22 / matrix2.M22;
+
+ matrix1.M31 = matrix1.M31 / matrix2.M31;
+ matrix1.M32 = matrix1.M32 / matrix2.M32;
+ return matrix1;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the division of a <see cref="Matrix2" /> by
+ /// a scalar.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <param name="scalar">The amount to divide the <see cref="Matrix2" /> by.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 operator /(Matrix2 matrix, float scalar)
+ {
+ var num = 1f / scalar;
+ matrix.M11 = matrix.M11 * num;
+ matrix.M12 = matrix.M12 * num;
+
+ matrix.M21 = matrix.M21 * num;
+ matrix.M22 = matrix.M22 * num;
+
+ matrix.M31 = matrix.M31 * num;
+ matrix.M32 = matrix.M32 * num;
+ return matrix;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the inversion of a <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 Invert(Matrix2 matrix)
+ {
+ var det = 1 / matrix.Determinant();
+
+ var m11 = matrix.M22 * det;
+ var m12 = -matrix.M12 * det;
+
+ var m21 = -matrix.M21 * det;
+ var m22 = matrix.M11 * det;
+
+ var m31 = (matrix.M32 * matrix.M21 - matrix.M31 * matrix.M22) * det;
+ var m32 = -(matrix.M32 * matrix.M11 - matrix.M31 * matrix.M12) * det;
+
+ return new Matrix2(m11, m12, m21, m22, m31, m32);
+ }
+
+ /// <summary>
+ /// Calculates the inversion of a <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <param name="result">The resulting <see cref="Matrix2" />.</param>
+ public static void Invert(ref Matrix2 matrix, out Matrix2 result)
+ {
+ var det = 1 / matrix.Determinant();
+
+ result = new Matrix2
+ {
+ M11 = matrix.M22 * det,
+ M12 = -matrix.M12 * det,
+ M21 = -matrix.M21 * det,
+ M22 = matrix.M11 * det,
+ M31 = (matrix.M32 * matrix.M21 - matrix.M31 * matrix.M22) * det,
+ M32 = -(matrix.M32 * matrix.M11 - matrix.M31 * matrix.M12) * det
+ };
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Matrix2" /> struct with the inversion of a <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <returns>The resulting <see cref="Matrix2" />.</returns>
+ public static Matrix2 operator -(Matrix2 matrix)
+ {
+ matrix.M11 = -matrix.M11;
+ matrix.M12 = -matrix.M12;
+
+ matrix.M21 = -matrix.M21;
+ matrix.M22 = -matrix.M22;
+
+ matrix.M31 = -matrix.M31;
+ matrix.M32 = -matrix.M32;
+ return matrix;
+ }
+
+ /// <summary>
+ /// Compares a <see cref="Matrix2" /> for equality with another <see cref="Matrix2" /> without any tolerance.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <returns><c>true</c> if the <see cref="Matrix2" />s are equal; <c>false</c> otherwise.</returns>
+ public static bool operator ==(Matrix2 matrix1, Matrix2 matrix2)
+ {
+ return (matrix1.M11 == matrix2.M11) && (matrix1.M12 == matrix2.M12) && (matrix1.M21 == matrix2.M21) &&
+ (matrix1.M22 == matrix2.M22) && (matrix1.M31 == matrix2.M31) && (matrix1.M32 == matrix2.M32);
+ }
+
+ /// <summary>
+ /// Compares a <see cref="Matrix2" /> for inequality with another <see cref="Matrix2" /> without any tolerance.
+ /// </summary>
+ /// <param name="matrix1">The first <see cref="Matrix2" />.</param>
+ /// <param name="matrix2">The second <see cref="Matrix2" />.</param>
+ /// <returns><c>true</c> if the <see cref="Matrix2" />s are not equal; <c>false</c> otherwise.</returns>
+ public static bool operator !=(Matrix2 matrix1, Matrix2 matrix2)
+ {
+ return (matrix1.M11 != matrix2.M11) || (matrix1.M12 != matrix2.M12) || (matrix1.M21 != matrix2.M21) ||
+ (matrix1.M22 != matrix2.M22) || (matrix1.M31 != matrix2.M31) || (matrix1.M32 != matrix2.M32);
+ }
+
+ /// <summary>
+ /// Returns a value that indicates whether the current <see cref="Matrix2" /> is equal to a specified
+ /// <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" /> with which to make the comparison.</param>
+ /// <returns>
+ /// <c>true</c> if the current <see cref="Matrix2" /> is equal to the specified <see cref="Matrix2" />;
+ /// <c>false</c> otherwise.
+ /// </returns>
+ public bool Equals(ref Matrix2 matrix)
+ {
+ return (M11 == matrix.M11) && (M12 == matrix.M12) && (M21 == matrix.M21) && (M22 == matrix.M22) &&
+ (M31 == matrix.M31) && (M32 == matrix.M32);
+ }
+
+ /// <summary>
+ /// Returns a value that indicates whether the current <see cref="Matrix2" /> is equal to a specified
+ /// <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" /> with which to make the comparison.</param>
+ /// <returns>
+ /// <c>true</c> if the current <see cref="Matrix2" /> is equal to the specified <see cref="Matrix2" />;
+ /// <c>false</c> otherwise.
+ /// </returns>
+ public bool Equals(Matrix2 matrix)
+ {
+ return Equals(ref matrix);
+ }
+
+ /// <summary>
+ /// Returns a value that indicates whether the current <see cref="Matrix2" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object with which to make the comparison.</param>
+ /// <returns>
+ /// <c>true</c> if the current <see cref="Matrix2" /> is equal to the specified object;
+ /// <c>false</c> otherwise.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ return obj is Matrix2 && Equals((Matrix2)obj);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this <see cref="Matrix2" />.
+ /// </summary>
+ /// <returns>
+ /// A hash code for this <see cref="Matrix2" />, suitable for use in hashing algorithms and data structures like a
+ /// hash table.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ // ReSharper disable NonReadonlyMemberInGetHashCode
+ return M11.GetHashCode() + M12.GetHashCode() + M21.GetHashCode() + M22.GetHashCode() + M31.GetHashCode() +
+ M32.GetHashCode();
+ // ReSharper restore NonReadonlyMemberInGetHashCode
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from <see cref="Matrix2" /> to <see cref="Matrix" />.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <returns>
+ /// The resulting <see cref="Matrix" />.
+ /// </returns>
+ public static implicit operator Matrix(Matrix2 matrix)
+ {
+ return new Matrix(matrix.M11, matrix.M12, 0, 0, matrix.M21, matrix.M22, 0, 0, 0, 0, 1, 0, matrix.M31,
+ matrix.M32, 0, 1);
+ }
+
+ /// <summary>
+ /// Performs an explicit conversion from a specified <see cref="Matrix2" /> to a <see cref="Matrix" />.
+ /// </summary>
+ /// <param name="matrix">The <see cref="Matrix2" />.</param>
+ /// <param name="depth">The depth value.</param>
+ /// <param name="result">The resulting <see cref="Matrix" />.</param>
+ public static void ToMatrix(ref Matrix2 matrix, float depth, out Matrix result)
+ {
+ result.M11 = matrix.M11;
+ result.M12 = matrix.M12;
+ result.M13 = 0;
+ result.M14 = 0;
+
+ result.M21 = matrix.M21;
+ result.M22 = matrix.M22;
+ result.M23 = 0;
+ result.M24 = 0;
+
+ result.M31 = 0;
+ result.M32 = 0;
+ result.M33 = 1;
+ result.M34 = 0;
+
+ result.M41 = matrix.M31;
+ result.M42 = matrix.M32;
+ result.M43 = depth;
+ result.M44 = 1;
+ }
+
+ /// <summary>
+ /// Performs an explicit conversion from a specified <see cref="Matrix2" /> to a <see cref="Matrix" />.
+ /// </summary>
+ /// <param name="depth">The depth value.</param>
+ /// <returns>The resulting <see cref="Matrix" />.</returns>
+ public Matrix ToMatrix(float depth = 0)
+ {
+ Matrix result;
+ ToMatrix(ref this, depth, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Gets the debug display string.
+ /// </summary>
+ /// <value>
+ /// The debug display string.
+ /// </value>
+ internal string DebugDisplayString => this == Identity
+ ? "Identity"
+ : $"T:({Translation.X:0.##},{Translation.Y:0.##}), R:{MathHelper.ToDegrees(Rotation):0.##}°, S:({Scale.X:0.##},{Scale.Y:0.##})"
+ ;
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="Matrix2" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="Matrix2" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"{{M11:{M11} M12:{M12}}} {{M21:{M21} M22:{M22}}} {{M31:{M31} M32:{M32}}}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/MatrixExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/MatrixExtensions.cs
new file mode 100644
index 0000000..f69da97
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/MatrixExtensions.cs
@@ -0,0 +1,28 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public static class MatrixExtensions
+ {
+ public static bool Decompose(this Matrix matrix, out Vector2 position, out float rotation, out Vector2 scale)
+ {
+ Vector3 position3, scale3;
+ Quaternion rotationQuaternion;
+
+ if (matrix.Decompose(out scale3, out rotationQuaternion, out position3))
+ {
+ var direction = Vector2.Transform(Vector2.UnitX, rotationQuaternion);
+ rotation = (float) Math.Atan2(direction.Y, direction.X);
+ position = new Vector2(position3.X, position3.Y);
+ scale = new Vector2(scale3.X, scale3.Y);
+ return true;
+ }
+
+ position = Vector2.Zero;
+ rotation = 0;
+ scale = Vector2.One;
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/OrientedRectangle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/OrientedRectangle.cs
new file mode 100644
index 0000000..1439e9f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/OrientedRectangle.cs
@@ -0,0 +1,302 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using Microsoft.Xna.Framework;
+using SharpDX.Direct3D;
+
+namespace MonoGame.Extended
+{
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.4; Bounding Volumes - Oriented Bounding Boxes (OBBs), pg 101.
+
+ /// <summary>
+ /// An oriented bounding rectangle is a rectangular block, much like a bounding rectangle
+ /// <see cref="BoundingRectangle" /> but with an arbitrary orientation <see cref="Vector2" />.
+ /// </summary>
+ /// <seealso cref="IEquatable{T}" />
+ [DebuggerDisplay($"{{{nameof(DebugDisplayString)},nq}}")]
+ public struct OrientedRectangle : IEquatable<OrientedRectangle>, IShapeF
+ {
+ /// <summary>
+ /// The centre position of this <see cref="OrientedRectangle" />.
+ /// </summary>
+ public Point2 Center;
+
+ /// <summary>
+ /// The distance from the <see cref="Center" /> point along both axes to any point on the boundary of this
+ /// <see cref="OrientedRectangle" />.
+ /// </summary>
+ ///
+ public Vector2 Radii;
+
+ /// <summary>
+ /// The rotation matrix <see cref="Matrix2" /> of the bounding rectangle <see cref="OrientedRectangle" />.
+ /// </summary>
+ public Matrix2 Orientation;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BoundingRectangle" /> structure from the specified centre
+ /// <see cref="Point2" /> and the radii <see cref="Size2" />.
+ /// </summary>
+ /// <param name="center">The centre <see cref="Point2" />.</param>
+ /// <param name="radii">The radii <see cref="Vector2" />.</param>
+ /// <param name="orientation">The orientation <see cref="Matrix2" />.</param>
+ public OrientedRectangle(Point2 center, Size2 radii, Matrix2 orientation)
+ {
+ Center = center;
+ Radii = radii;
+ Orientation = orientation;
+ }
+
+ /// <summary>
+ /// Gets a list of points defining the corner points of the oriented rectangle.
+ /// </summary>
+ public IReadOnlyList<Vector2> Points
+ {
+ get
+ {
+ var topLeft = -Radii;
+ var bottomLeft = -new Vector2(Radii.X, -Radii.Y);
+ var topRight = (Vector2)new Point2(Radii.X, -Radii.Y);
+ var bottomRight = Radii;
+
+ return new List<Vector2>
+ {
+ Vector2.Transform(topRight, Orientation) + Center,
+ Vector2.Transform(topLeft, Orientation) + Center,
+ Vector2.Transform(bottomLeft, Orientation) + Center,
+ Vector2.Transform(bottomRight, Orientation) + Center
+ };
+ }
+ }
+
+ public Point2 Position
+ {
+ get => Vector2.Transform(-Radii, Orientation) + Center;
+ set => throw new NotImplementedException();
+ }
+
+ public RectangleF BoundingRectangle => (RectangleF)this;
+
+ /// <summary>
+ /// Computes the <see cref="OrientedRectangle"/> from the specified <paramref name="rectangle"/>
+ /// transformed by <paramref name="transformMatrix"/>.
+ /// </summary>
+ /// <param name="rectangle">The <see cref="OrientedRectangle"/> to transform.</param>
+ /// <param name="transformMatrix">The <see cref="Matrix2"/> transformation.</param>
+ /// <returns>A new <see cref="OrientedRectangle"/>.</returns>
+ public static OrientedRectangle Transform(OrientedRectangle rectangle, ref Matrix2 transformMatrix)
+ {
+ Transform(ref rectangle, ref transformMatrix, out var result);
+ return result;
+ }
+
+ private static void Transform(ref OrientedRectangle rectangle, ref Matrix2 transformMatrix, out OrientedRectangle result)
+ {
+ PrimitivesHelper.TransformOrientedRectangle(
+ ref rectangle.Center,
+ ref rectangle.Orientation,
+ ref transformMatrix);
+ result = new OrientedRectangle();
+ result.Center = rectangle.Center;
+ result.Radii = rectangle.Radii;
+ result.Orientation = rectangle.Orientation;
+ }
+
+ /// <summary>
+ /// Compares to two <see cref="OrientedRectangle"/> structures. The result specifies whether the
+ /// the values of the <see cref="Center"/>, <see cref="Radii"/> and <see cref="Orientation"/> are
+ /// equal.
+ /// </summary>
+ /// <param name="left">The left <see cref="OrientedRectangle" />.</param>
+ /// <param name="right">The right <see cref="OrientedRectangle" />.</param>
+ /// <returns><c>true</c> if left and right argument are equal; otherwise, <c>false</c>.</returns>
+ public static bool operator ==(OrientedRectangle left, OrientedRectangle right)
+ {
+ return left.Equals(right);
+ }
+
+ /// <summary>
+ /// Compares to two <see cref="OrientedRectangle"/> structures. The result specifies whether the
+ /// the values of the <see cref="Center"/>, <see cref="Radii"/> or <see cref="Orientation"/> are
+ /// unequal.
+ /// </summary>
+ /// <param name="left">The left <see cref="OrientedRectangle" />.</param>
+ /// <param name="right">The right <see cref="OrientedRectangle" />.</param>
+ /// <returns><c>true</c> if left and right argument are unequal; otherwise, <c>false</c>.</returns>
+ public static bool operator !=(OrientedRectangle left, OrientedRectangle right)
+ {
+ return !left.Equals(right);
+ }
+
+ /// <summary>
+ /// Determines whether two instances of <see cref="OrientedRectangle"/> are equal.
+ /// </summary>
+ /// <param name="other">The other <see cref="OrientedRectangle"/>.</param>
+ /// <returns><c>true</c> if the specified <see cref="OrientedRectangle"/> is equal
+ /// to the current <see cref="OrientedRectangle"/>; otherwise, <c>false</c>.</returns>
+ public bool Equals(OrientedRectangle other)
+ {
+ return Center.Equals(other.Center) && Radii.Equals(other.Radii) && Orientation.Equals(other.Orientation);
+ }
+
+ /// <summary>
+ /// Determines whether two instances of <see cref="OrientedRectangle"/> are equal.
+ /// </summary>
+ /// <param name="obj">The <see cref="OrientedRectangle"/> to compare to.</param>
+ /// <returns><c>true</c> if the specified <see cref="OrientedRectangle"/> is equal
+ /// to the current <see cref="OrientedRectangle"/>; otherwise, <c>false</c>.</returns>
+ public override bool Equals(object obj)
+ {
+ return obj is OrientedRectangle other && Equals(other);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this object instance.
+ /// </summary>
+ /// <returns></returns>
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(Center, Radii, Orientation);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="RectangleF" /> to <see cref="OrientedRectangle" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle to convert.</param>
+ /// <returns>The resulting <see cref="OrientedRectangle" />.</returns>
+ public static explicit operator OrientedRectangle(RectangleF rectangle)
+ {
+ var radii = new Size2(rectangle.Width * 0.5f, rectangle.Height * 0.5f);
+ var centre = new Point2(rectangle.X + radii.Width, rectangle.Y + radii.Height);
+
+ return new OrientedRectangle(centre, radii, Matrix2.Identity);
+ }
+
+ public static explicit operator RectangleF(OrientedRectangle orientedRectangle)
+ {
+ var topLeft = -orientedRectangle.Radii;
+ var rectangle = new RectangleF(topLeft, orientedRectangle.Radii * 2);
+ var orientation = orientedRectangle.Orientation * Matrix2.CreateTranslation(orientedRectangle.Center);
+ return RectangleF.Transform(rectangle, ref orientation);
+ }
+
+ /// <summary>
+ /// See:
+ /// https://www.flipcode.com/archives/2D_OBB_Intersection.shtml
+ /// https://dyn4j.org/2010/01/sat
+ /// </summary>
+ /// <param name="rectangle"></param>
+ /// <param name="other"></param>
+ /// <returns></returns>
+ /// <exception cref="NotImplementedException"></exception>
+ public static (bool Intersects, Vector2 MinimumTranslationVector) Intersects(
+ OrientedRectangle rectangle, OrientedRectangle other)
+ {
+ var corners = rectangle.Points;
+ var otherCorners = other.Points;
+
+ var allAxis = new[]
+ {
+ corners[1] - corners[0],
+ corners[3] - corners[0],
+ otherCorners[1] - otherCorners[0],
+ otherCorners[3] - otherCorners[0],
+ };
+ var normalizedAxis = new[]
+ {
+ allAxis[0],
+ allAxis[1],
+ allAxis[2],
+ allAxis[3]
+ };
+ var overlap = 0f;
+ var minimumTranslationVector = Vector2.Zero;
+
+ // Make the length of each axis 1/edge length, so we know any
+ // dot product must be less than 1 to fall within the edge.
+ for (var a = 0; a < normalizedAxis.Length; a++)
+ {
+ normalizedAxis[a] /= normalizedAxis[a].LengthSquared();
+ }
+
+ for (var a = 0; a < normalizedAxis.Length; a++)
+ {
+ var axisProjectedOnto = normalizedAxis[a];
+ var originalAxis = allAxis[a];
+
+ var p1 = Project(corners, axisProjectedOnto);
+ var p2 = Project(otherCorners, axisProjectedOnto);
+
+ if (!IsOverlapping(p1, p2))
+ {
+ // There was no intersection along this dimension;
+ // the boxes cannot possibly overlap.
+ return (false, Vector2.Zero);
+ }
+
+ var o = GetOverlap(p1, p2);
+ if (o < overlap || overlap == 0f)
+ {
+ overlap = o;
+ minimumTranslationVector = originalAxis * overlap;
+ if (p1.Min > p2.Min)
+ {
+ minimumTranslationVector = -minimumTranslationVector;
+ }
+ }
+ }
+
+ // There was no dimension along which there is no intersection.
+ // Therefore, the boxes overlap.
+ return (true, minimumTranslationVector);
+
+ (float Min, float Max) Project(IReadOnlyList<Vector2> vertices, Vector2 axis)
+ {
+ var t = vertices[0].Dot(axis);
+
+ // Find the extent of box 2 on axis a
+ var min = t;
+ var max = t;
+
+ for (var c = 1; c < 4; c++)
+ {
+ t = vertices[c].Dot(axis);
+
+ if (t < min)
+ {
+ min = t;
+ }
+ else if (t > max)
+ {
+ max = t;
+ }
+ }
+
+ return (min, max);
+ }
+
+ bool IsOverlapping((float Min, float Max) p1, (float Min, float Max) p2)
+ {
+ return p1.Min <= p2.Max && p1.Max >= p2.Min;
+ }
+
+ float GetOverlap((float Min, float Max) p1, (float Min, float Max) p2)
+ {
+ return Math.Min(p1.Max, p2.Max) - Math.Max(p1.Min, p2.Min);
+ }
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="OrientedRectangle" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="OrientedRectangle" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"Centre: {Center}, Radii: {Radii}, Orientation: {Orientation}";
+ }
+
+ internal string DebugDisplayString => ToString();
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point2.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point2.cs
new file mode 100644
index 0000000..5f8de92
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point2.cs
@@ -0,0 +1,380 @@
+using System;
+using System.Diagnostics;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 3.2; A Math and Geometry Primer - Coordinate Systems and Points. pg 35
+
+ /// <summary>
+ /// A two-dimensional point defined by a 2-tuple of real numbers, (x, y).
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// A point is a position in two-dimensional space, the location of which is described in terms of a
+ /// two-dimensional coordinate system, given by a reference point, called the origin, and two coordinate axes.
+ /// </para>
+ /// <para>
+ /// A common two-dimensional coordinate system is the Cartesian (or rectangular) coordinate system where the
+ /// coordinate axes, conventionally denoted the X axis and Y axis, are perpindicular to each other. For the
+ /// three-dimensional rectangular coordinate system, the third axis is called the Z axis.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="IEquatable{T}" />
+ /// <seealso cref="IEquatableByRef{Point2}" />
+ [DebuggerDisplay("{DebugDisplayString,nq}")]
+ public struct Point2 : IEquatable<Point2>, IEquatableByRef<Point2>
+ {
+ /// <summary>
+ /// Returns a <see cref="Point2" /> with <see cref="X" /> and <see cref="Y" /> equal to <c>0.0f</c>.
+ /// </summary>
+ public static readonly Point2 Zero = new Point2();
+
+ /// <summary>
+ /// Returns a <see cref="Point2" /> with <see cref="X" /> and <see cref="Y" /> set to not a number.
+ /// </summary>
+ public static readonly Point2 NaN = new Point2(float.NaN, float.NaN);
+
+ /// <summary>
+ /// The x-coordinate of this <see cref="Point2" />.
+ /// </summary>
+ public float X;
+
+ /// <summary>
+ /// The y-coordinate of this <see cref="Point2" />.
+ /// </summary>
+ public float Y;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Point2" /> structure from the specified coordinates.
+ /// </summary>
+ /// <param name="x">The x-coordinate.</param>
+ /// <param name="y">The y-coordinate.</param>
+ public Point2(float x, float y)
+ {
+ X = x;
+ Y = y;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Point2" /> structures. The result specifies
+ /// whether the values of the <see cref="X" /> and <see cref="Y" />
+ /// fields of the two <see cref="Point2" />
+ /// structures are equal.
+ /// </summary>
+ /// <param name="first">The first point.</param>
+ /// <param name="second">The second point.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="X" /> and <see cref="Y" />
+ /// fields of the two <see cref="Point2" />
+ /// structures are equal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(Point2 first, Point2 second)
+ {
+ return first.Equals(ref second);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Point2" /> is equal to another <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point2" /> is equal to the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(Point2 point)
+ {
+ return Equals(ref point);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Point2" /> is equal to another <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point2" /> is equal to the <paramref name="point" /> parameter; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(ref Point2 point)
+ {
+ // ReSharper disable CompareOfFloatsByEqualityOperator
+ return (point.X == X) && (point.Y == Y);
+ // ReSharper restore CompareOfFloatsByEqualityOperator
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="Point2" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object to make the comparison with.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point2" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ if (obj is Point2)
+ return Equals((Point2) obj);
+ return false;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Point2" /> structures. The result specifies
+ /// whether the values of the <see cref="X" /> or <see cref="Y" />
+ /// fields of the two <see cref="Point2" />
+ /// structures are unequal.
+ /// </summary>
+ /// <param name="first">The first point.</param>
+ /// <param name="second">The second point.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="X" /> or <see cref="Y" />
+ /// fields of the two <see cref="Point2" />
+ /// structures are unequal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(Point2 first, Point2 second)
+ {
+ return !(first == second);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point2" /> representing the addition of a <see cref="Point2" /> and a
+ /// <see cref="Vector2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The <see cref="Point2" /> representing the addition of a <see cref="Point2" /> and a <see cref="Vector2" />.
+ /// </returns>
+ public static Point2 operator +(Point2 point, Vector2 vector)
+ {
+ return Add(point, vector);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point2" /> representing the addition of a <see cref="Point2" /> and a
+ /// <see cref="Vector2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The <see cref="Point2" /> representing the addition of a <see cref="Point2" /> and a <see cref="Vector2" />.
+ /// </returns>
+ public static Point2 Add(Point2 point, Vector2 vector)
+ {
+ Point2 p;
+ p.X = point.X + vector.X;
+ p.Y = point.Y + vector.Y;
+ return p;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point2" /> representing the subtraction of a <see cref="Point2" /> and a
+ /// <see cref="Vector2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The <see cref="Point2" /> representing the substraction of a <see cref="Point2" /> and a <see cref="Vector2" />.
+ /// </returns>
+ public static Point2 operator -(Point2 point, Vector2 vector)
+ {
+ return Subtract(point, vector);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point2" /> representing the addition of a <see cref="Point2" /> and a
+ /// <see cref="Vector2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The <see cref="Point2" /> representing the substraction of a <see cref="Point2" /> and a <see cref="Vector2" />.
+ /// </returns>
+ public static Point2 Subtract(Point2 point, Vector2 vector)
+ {
+ Point2 p;
+ p.X = point.X - vector.X;
+ p.Y = point.Y - vector.Y;
+ return p;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Vector2" /> representing the displacement of two <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="point2">The second point.</param>
+ /// <param name="point1">The first point.</param>
+ /// <returns>
+ /// The <see cref="Vector2" /> representing the displacement of two <see cref="Point2" /> structures.
+ /// </returns>
+ public static Vector2 operator -(Point2 point1, Point2 point2)
+ {
+ return Displacement(point1, point2);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Vector2" /> representing the displacement of two <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="point2">The second point.</param>
+ /// <param name="point1">The first point.</param>
+ /// <returns>
+ /// The <see cref="Vector2" /> representing the displacement of two <see cref="Point2" /> structures.
+ /// </returns>
+ public static Vector2 Displacement(Point2 point2, Point2 point1)
+ {
+ Vector2 vector;
+ vector.X = point2.X - point1.X;
+ vector.Y = point2.Y - point1.Y;
+ return vector;
+ }
+
+ /// <summary>
+ /// Translates a <see cref='Point2' /> by a given <see cref='Size2' />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The result of the operator.
+ /// </returns>
+ public static Point2 operator +(Point2 point, Size2 size)
+ {
+ return Add(point, size);
+ }
+
+ /// <summary>
+ /// Translates a <see cref='Point2' /> by a given <see cref='Size2' />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The result of the operator.
+ /// </returns>
+ public static Point2 Add(Point2 point, Size2 size)
+ {
+ return new Point2(point.X + size.Width, point.Y + size.Height);
+ }
+
+ /// <summary>
+ /// Translates a <see cref='Point2' /> by the negative of a given <see cref='Size2' />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The result of the operator.
+ /// </returns>
+ public static Point2 operator -(Point2 point, Size2 size)
+ {
+ return Subtract(point, size);
+ }
+
+ /// <summary>
+ /// Translates a <see cref='Point2' /> by the negative of a given <see cref='Size2' /> .
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The result of the operator.
+ /// </returns>
+ public static Point2 Subtract(Point2 point, Size2 size)
+ {
+ return new Point2(point.X - size.Width, point.Y - size.Height);
+ }
+
+ /// <summary>
+ /// Returns a hash code of this <see cref="Point2" /> suitable for use in hashing algorithms and data
+ /// structures like a hash table.
+ /// </summary>
+ /// <returns>
+ /// A hash code of this <see cref="Point2" />.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (X.GetHashCode()*397) ^ Y.GetHashCode();
+ }
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point2" /> that contains the minimal coordinate values from two <see cref="Point2" />
+ /// structures.
+ /// structures.
+ /// </summary>
+ /// <param name="first">The first point.</param>
+ /// <param name="second">The second point.</param>
+ /// <returns>
+ /// The the <see cref="Point2" /> that contains the minimal coordinate values from two <see cref="Point2" />
+ /// structures.
+ /// </returns>
+ public static Point2 Minimum(Point2 first, Point2 second)
+ {
+ return new Point2(first.X < second.X ? first.X : second.X,
+ first.Y < second.Y ? first.Y : second.Y);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point2" /> that contains the maximal coordinate values from two <see cref="Point2" />
+ /// structures.
+ /// structures.
+ /// </summary>
+ /// <param name="first">The first point.</param>
+ /// <param name="second">The second point.</param>
+ /// <returns>
+ /// The the <see cref="Point2" /> that contains the maximal coordinate values from two <see cref="Point2" />
+ /// structures.
+ /// </returns>
+ public static Point2 Maximum(Point2 first, Point2 second)
+ {
+ return new Point2(first.X > second.X ? first.X : second.X,
+ first.Y > second.Y ? first.Y : second.Y);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Point2" /> to a position <see cref="Vector2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// The resulting <see cref="Vector2" />.
+ /// </returns>
+ public static implicit operator Vector2(Point2 point)
+ {
+ return new Vector2(point.X, point.Y);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Vector2" /> to a position <see cref="Point2" />.
+ /// </summary>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The resulting <see cref="Point2" />.
+ /// </returns>
+ public static implicit operator Point2(Vector2 vector)
+ {
+ return new Point2(vector.X, vector.Y);
+ }
+
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Point" /> to a <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// The resulting <see cref="Point2" />.
+ /// </returns>
+ public static implicit operator Point2(Point point)
+ {
+ return new Point2(point.X, point.Y);
+ }
+
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="Point2" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="Point2" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"({X}, {Y})";
+ }
+
+ internal string DebugDisplayString => ToString();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point3.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point3.cs
new file mode 100644
index 0000000..cb7b249
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point3.cs
@@ -0,0 +1,379 @@
+using System;
+using System.Diagnostics;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// A three-dimensional point defined by a 3-tuple of real numbers, (x, y, z).
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// A point is a position in three-dimensional space, the location of which is described in terms of a
+ /// three-dimensional coordinate system, given by a reference point, called the origin, and three coordinate axes.
+ /// </para>
+ /// <para>
+ /// A common three-dimensional coordinate system is the Cartesian (or rectangular) coordinate system where the
+ /// coordinate axes, conventionally denoted the X axis, Y axis and Z axis, are perpindicular to each other.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="IEquatable{T}" />
+ /// <seealso cref="IEquatableByRef{Point3}" />
+ [DebuggerDisplay("{DebugDisplayString,nq}")]
+ public struct Point3 : IEquatable<Point3>, IEquatableByRef<Point3>
+ {
+ /// <summary>
+ /// Returns a <see cref="Point3" /> with <see cref="X" /> <see cref="Y" /> and <see cref="Z" /> equal to <c>0.0f</c>.
+ /// </summary>
+ public static readonly Point3 Zero = new Point3();
+
+ /// <summary>
+ /// Returns a <see cref="Point3" /> with <see cref="X" /> <see cref="Y" /> and <see cref="Z" /> set to not a number.
+ /// </summary>
+ public static readonly Point3 NaN = new Point3(float.NaN, float.NaN, float.NaN);
+
+ /// <summary>
+ /// The x-coordinate of this <see cref="Point3" />.
+ /// </summary>
+ public float X;
+
+ /// <summary>
+ /// The y-coordinate of this <see cref="Point3" />.
+ /// </summary>
+ public float Y;
+
+ /// <summary>
+ /// The z-coordinate of this <see cref="Point3" />.
+ /// </summary>
+ public float Z;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Point3" /> structure from the specified coordinates.
+ /// </summary>
+ /// <param name="x">The x-coordinate.</param>
+ /// <param name="y">The y-coordinate.</param>
+ /// <param name="z">The z-coordinate.</param>
+ public Point3(float x, float y, float z)
+ {
+ X = x;
+ Y = y;
+ Z = z;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Point3" /> structures. The result specifies
+ /// whether the values of the <see cref="X" /> <see cref="Y" /> and <see cref="Z" />
+ /// fields of the two <see cref="Point3" />
+ /// structures are equal.
+ /// </summary>
+ /// <param name="first">The first point.</param>
+ /// <param name="second">The second point.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="X" /> <see cref="Y" /> and <see cref="Z" />
+ /// fields of the two <see cref="Point3" />
+ /// structures are equal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(Point3 first, Point3 second)
+ {
+ return first.Equals(ref second);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Point3" /> is equal to another <see cref="Point3" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point3" /> is equal to the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(Point3 point)
+ {
+ return Equals(ref point);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Point3" /> is equal to another <see cref="Point3" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point3" /> is equal to the <paramref name="point" /> parameter; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(ref Point3 point)
+ {
+ // ReSharper disable CompareOfFloatsByEqualityOperator
+ return (point.X == X) && (point.Y == Y) && (point.Z == Z);
+ // ReSharper restore CompareOfFloatsByEqualityOperator
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="Point3" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object to make the comparison with.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point3" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ if (obj is Point3)
+ return Equals((Point3) obj);
+ return false;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Point3" /> structures. The result specifies
+ /// whether the values of the <see cref="X" /> <see cref="Y" /> or <see cref="Z" />
+ /// fields of the two <see cref="Point3" />
+ /// structures are unequal.
+ /// </summary>
+ /// <param name="first">The first point.</param>
+ /// <param name="second">The second point.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="X" /> <see cref="Y" /> or <see cref="Z" />
+ /// fields of the two <see cref="Point3" />
+ /// structures are unequal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(Point3 first, Point3 second)
+ {
+ return !(first == second);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point3" /> representing the addition of a <see cref="Point3" /> and a
+ /// <see cref="Vector2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The <see cref="Point3" /> representing the addition of a <see cref="Point3" /> and a <see cref="Vector3" />.
+ /// </returns>
+ public static Point3 operator +(Point3 point, Vector3 vector)
+ {
+ return Add(point, vector);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point3" /> representing the addition of a <see cref="Point3" /> and a
+ /// <see cref="Vector3" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The <see cref="Point3" /> representing the addition of a <see cref="Point3" /> and a <see cref="Vector3" />.
+ /// </returns>
+ public static Point3 Add(Point3 point, Vector3 vector)
+ {
+ Point3 p;
+ p.X = point.X + vector.X;
+ p.Y = point.Y + vector.Y;
+ p.Z = point.Z + vector.Z;
+ return p;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point3" /> representing the subtraction of a <see cref="Point3" /> and a
+ /// <see cref="Vector3" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The <see cref="Point3" /> representing the substraction of a <see cref="Point3" /> and a <see cref="Vector3" />.
+ /// </returns>
+ public static Point3 operator -(Point3 point, Vector3 vector)
+ {
+ return Subtract(point, vector);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point3" /> representing the addition of a <see cref="Point3" /> and a
+ /// <see cref="Vector3" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The <see cref="Point3" /> representing the substraction of a <see cref="Point3" /> and a <see cref="Vector3" />.
+ /// </returns>
+ public static Point3 Subtract(Point3 point, Vector3 vector)
+ {
+ Point3 p;
+ p.X = point.X - vector.X;
+ p.Y = point.Y - vector.Y;
+ p.Z = point.Z - vector.Z;
+ return p;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Vector3" /> representing the displacement of two <see cref="Point3" /> structures.
+ /// </summary>
+ /// <param name="point2">The second point.</param>
+ /// <param name="point1">The first point.</param>
+ /// <returns>
+ /// The <see cref="Vector3" /> representing the displacement of two <see cref="Point3" /> structures.
+ /// </returns>
+ public static Vector3 operator -(Point3 point1, Point3 point2)
+ {
+ return Displacement(point1, point2);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Vector3" /> representing the displacement of two <see cref="Point3" /> structures.
+ /// </summary>
+ /// <param name="point2">The second point.</param>
+ /// <param name="point1">The first point.</param>
+ /// <returns>
+ /// The <see cref="Vector3" /> representing the displacement of two <see cref="Point3" /> structures.
+ /// </returns>
+ public static Vector3 Displacement(Point3 point2, Point3 point1)
+ {
+ Vector3 vector;
+ vector.X = point2.X - point1.X;
+ vector.Y = point2.Y - point1.Y;
+ vector.Z = point2.Z - point1.Z;
+ return vector;
+ }
+
+ /// <summary>
+ /// Translates a <see cref='Point3' /> by a given <see cref='Size3' />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The result of the operator.
+ /// </returns>
+ public static Point3 operator +(Point3 point, Size3 size)
+ {
+ return Add(point, size);
+ }
+
+ /// <summary>
+ /// Translates a <see cref='Point3' /> by a given <see cref='Size3' />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The result of the operator.
+ /// </returns>
+ public static Point3 Add(Point3 point, Size3 size)
+ {
+ return new Point3(point.X + size.Width, point.Y + size.Height, point.Z + size.Depth);
+ }
+
+ /// <summary>
+ /// Translates a <see cref='Point3' /> by the negative of a given <see cref='Size3' />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The result of the operator.
+ /// </returns>
+ public static Point3 operator -(Point3 point, Size3 size)
+ {
+ return Subtract(point, size);
+ }
+
+ /// <summary>
+ /// Translates a <see cref='Point3' /> by the negative of a given <see cref='Size3' /> .
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The result of the operator.
+ /// </returns>
+ public static Point3 Subtract(Point3 point, Size3 size)
+ {
+ return new Point3(point.X - size.Width, point.Y - size.Height, point.Z - size.Depth);
+ }
+
+ /// <summary>
+ /// Returns a hash code of this <see cref="Point3" /> suitable for use in hashing algorithms and data
+ /// structures like a hash table.
+ /// </summary>
+ /// <returns>
+ /// A hash code of this <see cref="Point3" />.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int hash = 17;
+ hash = hash * 23 + X.GetHashCode();
+ hash = hash * 23 + Y.GetHashCode();
+ hash = hash * 23 + Z.GetHashCode();
+ return hash;
+ }
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point3" /> that contains the minimal coordinate values from two <see cref="Point3" />
+ /// structures.
+ /// structures.
+ /// </summary>
+ /// <param name="first">The first point.</param>
+ /// <param name="second">The second point.</param>
+ /// <returns>
+ /// The the <see cref="Point3" /> that contains the minimal coordinate values from two <see cref="Point3" />
+ /// structures.
+ /// </returns>
+ public static Point3 Minimum(Point3 first, Point3 second)
+ {
+ return new Point3(first.X < second.X ? first.X : second.X,
+ first.Y < second.Y ? first.Y : second.Y,
+ first.Z < second.Z ? first.Z : second.Z);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Point3" /> that contains the maximal coordinate values from two <see cref="Point3" />
+ /// structures.
+ /// structures.
+ /// </summary>
+ /// <param name="first">The first point.</param>
+ /// <param name="second">The second point.</param>
+ /// <returns>
+ /// The the <see cref="Point3" /> that contains the maximal coordinate values from two <see cref="Point3" />
+ /// structures.
+ /// </returns>
+ public static Point3 Maximum(Point3 first, Point3 second)
+ {
+ return new Point3(first.X > second.X ? first.X : second.X,
+ first.Y > second.Y ? first.Y : second.Y,
+ first.Z > second.Z ? first.Z : second.Z);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Point3" /> to a position <see cref="Vector3" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// The resulting <see cref="Vector3" />.
+ /// </returns>
+ public static implicit operator Vector3(Point3 point)
+ {
+ return new Vector3(point.X, point.Y, point.Z);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Vector3" /> to a position <see cref="Point3" />.
+ /// </summary>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The resulting <see cref="Point3" />.
+ /// </returns>
+ public static implicit operator Point3(Vector3 vector)
+ {
+ return new Point3(vector.X, vector.Y, vector.Z);
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="Point3" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="Point3" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"({X}, {Y}, {Z})";
+ }
+
+ internal string DebugDisplayString => ToString();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/PrimitivesHelper.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/PrimitivesHelper.cs
new file mode 100644
index 0000000..ed08270
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/PrimitivesHelper.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ internal class PrimitivesHelper
+ {
+ // Used by Ray2 and Segment2
+ internal static bool IntersectsSlab(float positionCoordinate, float directionCoordinate, float slabMinimum,
+ float slabMaximum, ref float rayMinimumDistance, ref float rayMaximumDistance)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.3; Basic Primitive Tests - Intersecting Lines, Rays, and (Directed Segments). pg 179-181
+
+ if (Math.Abs(directionCoordinate) < float.Epsilon)
+ return (positionCoordinate >= slabMinimum) && (positionCoordinate <= slabMaximum);
+
+ // Compute intersection values of ray with near and far plane of slab
+ var rayNearDistance = (slabMinimum - positionCoordinate)/directionCoordinate;
+ var rayFarDistance = (slabMaximum - positionCoordinate)/directionCoordinate;
+
+ if (rayNearDistance > rayFarDistance)
+ {
+ // Swap near and far distance
+ var temp = rayFarDistance;
+ rayNearDistance = rayFarDistance;
+ rayFarDistance = temp;
+ }
+
+ // Compute the intersection of slab intersection intervals
+ rayMinimumDistance = rayNearDistance > rayMinimumDistance ? rayNearDistance : rayMinimumDistance;
+ rayMaximumDistance = rayFarDistance < rayMaximumDistance ? rayFarDistance : rayMaximumDistance;
+
+ // Exit with no collision as soon as slab intersection becomes empty
+ return rayMinimumDistance <= rayMaximumDistance;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static void CreateRectangleFromPoints(IReadOnlyList<Point2> points, out Point2 minimum, out Point2 maximum)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.2; Bounding Volumes - Axis-aligned Bounding Boxes (AABBs). pg 82-84
+
+ if (points == null || points.Count == 0)
+ {
+ minimum = Point2.Zero;
+ maximum = Point2.Zero;
+ return;
+ }
+
+ minimum = maximum = points[0];
+
+ // ReSharper disable once ForCanBeConvertedToForeach
+ for (var index = points.Count - 1; index > 0; --index)
+ {
+ var point = points[index];
+ minimum = Point2.Minimum(minimum, point);
+ maximum = Point2.Maximum(maximum, point);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static void TransformRectangle(ref Point2 center, ref Vector2 halfExtents, ref Matrix2 transformMatrix)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.2; Bounding Volumes - Axis-aligned Bounding Boxes (AABBs). pg 86-87
+
+ center = transformMatrix.Transform(center);
+ var xRadius = halfExtents.X;
+ var yRadius = halfExtents.Y;
+ halfExtents.X = xRadius * Math.Abs(transformMatrix.M11) + yRadius * Math.Abs(transformMatrix.M12);
+ halfExtents.Y = xRadius * Math.Abs(transformMatrix.M21) + yRadius * Math.Abs(transformMatrix.M22);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static void TransformOrientedRectangle(
+ ref Point2 center,
+ ref Matrix2 orientation,
+ ref Matrix2 transformMatrix)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.4; Oriented Bounding Boxes (OBBs), pg 101-105.
+
+ center = transformMatrix.Transform(center);
+ orientation *= transformMatrix;
+ // Reset the translation since orientation is only about rotation
+ orientation.M31 = 0;
+ orientation.M32 = 0;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static float SquaredDistanceToPointFromRectangle(Point2 minimum, Point2 maximum, Point2 point)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.1.3.1; Basic Primitive Tests - Closest-point Computations - Distance of Point to AABB. pg 130-131
+ var squaredDistance = 0.0f;
+
+ // for each axis add up the excess distance outside the box
+
+ // x-axis
+ if (point.X < minimum.X)
+ {
+ var distance = minimum.X - point.X;
+ squaredDistance += distance * distance;
+ }
+ else if (point.X > maximum.X)
+ {
+ var distance = maximum.X - point.X;
+ squaredDistance += distance * distance;
+ }
+
+ // y-axis
+ if (point.Y < minimum.Y)
+ {
+ var distance = minimum.Y - point.Y;
+ squaredDistance += distance * distance;
+ }
+ else if (point.Y > maximum.Y)
+ {
+ var distance = maximum.Y - point.Y;
+ squaredDistance += distance * distance;
+ }
+ return squaredDistance;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static void ClosestPointToPointFromRectangle(Point2 minimum, Point2 maximum, Point2 point, out Point2 result)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.1.2; Basic Primitive Tests - Closest-point Computations. pg 130-131
+
+ result = point;
+
+ // For each coordinate axis, if the point coordinate value is outside box, clamp it to the box, else keep it as is
+ if (result.X < minimum.X)
+ result.X = minimum.X;
+ else if (result.X > maximum.X)
+ result.X = maximum.X;
+
+ if (result.Y < minimum.Y)
+ result.Y = minimum.Y;
+ else if (result.Y > maximum.Y)
+ result.Y = maximum.Y;
+ }
+
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RandomExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RandomExtensions.cs
new file mode 100644
index 0000000..14abb5d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RandomExtensions.cs
@@ -0,0 +1,44 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public static class RandomExtensions
+ {
+ public static int Next(this Random random, Range<int> range)
+ {
+ return random.Next(range.Min, range.Max);
+ }
+
+ public static float NextSingle(this Random random, float min, float max)
+ {
+ return (max - min)*NextSingle(random) + min;
+ }
+
+ public static float NextSingle(this Random random, float max)
+ {
+ return max*NextSingle(random);
+ }
+
+ public static float NextSingle(this Random random)
+ {
+ return (float) random.NextDouble();
+ }
+
+ public static float NextSingle(this Random random, Range<float> range)
+ {
+ return NextSingle(random, range.Min, range.Max);
+ }
+
+ public static float NextAngle(this Random random)
+ {
+ return NextSingle(random, -MathHelper.Pi, MathHelper.Pi);
+ }
+
+ public static void NextUnitVector(this Random random, out Vector2 vector)
+ {
+ var angle = NextAngle(random);
+ vector = new Vector2((float) Math.Cos(angle), (float) Math.Sin(angle));
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Range.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Range.cs
new file mode 100644
index 0000000..5402233
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Range.cs
@@ -0,0 +1,84 @@
+using System;
+
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// Represents a closed interval defined by a minimum and a maximum value of a give type.
+ /// </summary>
+ public struct Range<T> : IEquatable<Range<T>> where T : IComparable<T>
+ {
+ public Range(T min, T max)
+ {
+ if (min.CompareTo(max) > 0 || max.CompareTo(min) < 0)
+ throw new ArgumentException("Min has to be smaller than or equal to max.");
+
+ Min = min;
+ Max = max;
+ }
+
+ public Range(T value)
+ : this(value, value)
+ {
+ }
+
+ /// <summary>
+ /// Gets the minium value of the <see cref="Range{T}" />.
+ /// </summary>
+ public T Min { get; }
+
+ /// <summary>
+ /// Gets the maximum value of the <see cref="Range{T}" />.
+ /// </summary>
+ public T Max { get; }
+
+
+ /// <summary>
+ /// Returns wheter or not this <see cref="Range{T}" /> is degenerate.
+ /// (Min and Max are the same)
+ /// </summary>
+ public bool IsDegenerate => Min.Equals(Max);
+
+ /// <summary>
+ /// Returns wheter or not this <see cref="Range{T}" /> is proper.
+ /// (Min and Max are not the same)
+ /// </summary>
+ public bool IsProper => !Min.Equals(Max);
+
+ public bool Equals(Range<T> value) => Min.Equals(value.Min) && Max.Equals(value.Max);
+
+ public override bool Equals(object obj) => obj is Range<T> && Equals((Range<T>) obj);
+
+ public override int GetHashCode() => Min.GetHashCode() ^ Max.GetHashCode();
+
+ public static bool operator ==(Range<T> value1, Range<T> value2) => value1.Equals(value2);
+
+ public static bool operator !=(Range<T> value1, Range<T> value2) => !value1.Equals(value2);
+
+ public static implicit operator Range<T>(T value) => new Range<T>(value, value);
+
+ public override string ToString() => $"Range<{typeof(T).Name}> [{Min} {Max}]";
+
+ /// <summary>
+ /// Returns wheter or not the value falls in this <see cref="Range{T}" />.
+ /// </summary>
+ public bool IsInBetween(T value, bool minValueExclusive = false, bool maxValueExclusive = false)
+ {
+ if (minValueExclusive)
+ {
+ if (value.CompareTo(Min) <= 0)
+ return false;
+ }
+
+ if (value.CompareTo(Min) < 0)
+ return false;
+
+ if (maxValueExclusive)
+ {
+ if (value.CompareTo(Max) >= 0)
+ return false;
+ }
+
+ return value.CompareTo(Max) <= 0;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Ray2.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Ray2.cs
new file mode 100644
index 0000000..c8ed7e5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Ray2.cs
@@ -0,0 +1,199 @@
+using System;
+using System.Diagnostics;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 3.5; A Math and Geometry Primer - Lines, Rays, and Segments. pg 53-54
+ /// <summary>
+ /// A two dimensional ray defined by a starting <see cref="Point2" /> and a direction <see cref="Vector2" />.
+ /// </summary>
+ /// <seealso cref="IEquatable{T}" />
+ /// <seealso cref="IEquatableByRef{Ray2}" />
+ [DebuggerDisplay("{DebugDisplayString,nq}")]
+ public struct Ray2 : IEquatable<Ray2>, IEquatableByRef<Ray2>
+ {
+ /// <summary>
+ /// The starting <see cref="Point2" /> of this <see cref="Ray2" />.
+ /// </summary>
+ public Point2 Position;
+
+ /// <summary>
+ /// The direction <see cref="Vector2" /> of this <see cref="Ray2" />.
+ /// </summary>
+ public Vector2 Direction;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Ray2" /> structure from the specified position and direction.
+ /// </summary>
+ /// <param name="position">The starting point.</param>
+ /// <param name="direction">The direction vector.</param>
+ public Ray2(Point2 position, Vector2 direction)
+ {
+ Position = position;
+ Direction = direction;
+ }
+
+ /// <summary>
+ /// Determines whether this <see cref="Ray2" /> intersects with a specified <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding rectangle.</param>
+ /// <param name="rayNearDistance">
+ /// When this method returns, contains the distance along the ray to the first intersection
+ /// point with the <paramref name="boundingRectangle" />, if an intersection was found; otherwise,
+ /// <see cref="float.NaN" />.
+ /// This parameter is passed uninitialized.
+ /// </param>
+ /// <param name="rayFarDistance">
+ /// When this method returns, contains the distance along the ray to the second intersection
+ /// point with the <paramref name="boundingRectangle" />, if an intersection was found; otherwise,
+ /// <see cref="float.NaN" />.
+ /// This parameter is passed uninitialized.
+ /// </param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Ray2" /> intersects with <paramref name="boundingRectangle" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Intersects(BoundingRectangle boundingRectangle, out float rayNearDistance, out float rayFarDistance)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.3; Basic Primitive Tests - Intersecting Lines, Rays, and (Directed Segments). pg 179-181
+
+ var minimum = boundingRectangle.Center - boundingRectangle.HalfExtents;
+ var maximum = boundingRectangle.Center + boundingRectangle.HalfExtents;
+
+ // Set to the smallest possible value so the algorithm can find the first hit along the ray
+ var minimumDistanceAlongRay = float.MinValue;
+ // Set to the maximum possible value so the algorithm can find the last hit along the ray
+ var maximumDistanceAlongRay = float.MaxValue;
+
+ // For all relevant slabs which in this case is two.
+
+ // The first, horizontal, slab.
+ if (!PrimitivesHelper.IntersectsSlab(Position.X, Direction.X, minimum.X, maximum.X,
+ ref minimumDistanceAlongRay,
+ ref maximumDistanceAlongRay))
+ {
+ rayNearDistance = rayFarDistance = float.NaN;
+ return false;
+ }
+
+ // The second, vertical, slab.
+ if (!PrimitivesHelper.IntersectsSlab(Position.Y, Direction.Y, minimum.Y, maximum.Y,
+ ref minimumDistanceAlongRay,
+ ref maximumDistanceAlongRay))
+ {
+ rayNearDistance = rayFarDistance = float.NaN;
+ return false;
+ }
+
+ // Ray intersects the 2 slabs.
+ rayNearDistance = minimumDistanceAlongRay < 0 ? 0 : minimumDistanceAlongRay;
+ rayFarDistance = maximumDistanceAlongRay;
+ return true;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Ray2" /> structures. The result specifies whether the values of the
+ /// <see cref="Position" />
+ /// and <see cref="Direction" /> fields of the two <see cref="Ray2" /> structures are equal.
+ /// </summary>
+ /// <param name="first">The first ray.</param>
+ /// <param name="second">The second ray.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Position" /> and <see cref="Direction" />
+ /// fields of the two <see cref="Ray2" />
+ /// structures are equal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(Ray2 first, Ray2 second)
+ {
+ return first.Equals(ref second);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Ray2" /> is equal to another <see cref="Ray2" />.
+ /// </summary>
+ /// <param name="ray">The ray.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Ray2" /> is equal to the <paramref name="ray" /> parameter; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(Ray2 ray)
+ {
+ return Equals(ref ray);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Ray2" /> is equal to another <see cref="Ray2" />.
+ /// </summary>
+ /// <param name="ray">The ray.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Ray2" /> is equal to the <paramref name="ray" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(ref Ray2 ray)
+ {
+ // ReSharper disable CompareOfFloatsByEqualityOperator
+ return (ray.Position == Position) && (ray.Direction == Direction);
+ // ReSharper restore CompareOfFloatsByEqualityOperator
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="Ray2" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object to make the comparison with.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Ray2" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ if (obj is Ray2)
+ return Equals((Ray2) obj);
+ return false;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Ray2" /> structures. The result specifies whether the values of the
+ /// <see cref='Position' />
+ /// and <see cref="Direction" /> fields of the two <see cref="Ray2" /> structures are unequal.
+ /// </summary>
+ /// <param name="first">The first ray.</param>
+ /// <param name="second">The second ray.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Position" /> and <see cref="Direction" />
+ /// fields of the two <see cref="Ray2" />
+ /// structures are unequal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(Ray2 first, Ray2 second)
+ {
+ return !(first == second);
+ }
+
+ /// <summary>
+ /// Returns a hash code of this <see cref="Ray2" /> suitable for use in hashing algorithms and data
+ /// structures like a hash table.
+ /// </summary>
+ /// <returns>
+ /// A hash code of this <see cref="Ray2" />.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (Position.GetHashCode()*397) ^ Direction.GetHashCode();
+ }
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="Ray2" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="Ray2" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"Position: {Position}, Direction: {Direction}";
+ }
+
+ internal string DebugDisplayString => ToString();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleExtensions.cs
new file mode 100644
index 0000000..a6b917a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleExtensions.cs
@@ -0,0 +1,71 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public static class RectangleExtensions
+ {
+ /// <summary>
+ /// Gets the corners of the rectangle in a clockwise direction starting at the top left.
+ /// </summary>
+ public static Point[] GetCorners(this Rectangle rectangle)
+ {
+ var corners = new Point[4];
+ corners[0] = new Point(rectangle.Left, rectangle.Top);
+ corners[1] = new Point(rectangle.Right, rectangle.Top);
+ corners[2] = new Point(rectangle.Right, rectangle.Bottom);
+ corners[3] = new Point(rectangle.Left, rectangle.Bottom);
+ return corners;
+ }
+
+ /// <summary>
+ /// Gets the corners of the rectangle in a clockwise direction starting at the top left.
+ /// </summary>
+ public static Vector2[] GetCorners(this RectangleF rectangle)
+ {
+ var corners = new Vector2[4];
+ corners[0] = new Vector2(rectangle.Left, rectangle.Top);
+ corners[1] = new Vector2(rectangle.Right, rectangle.Top);
+ corners[2] = new Vector2(rectangle.Right, rectangle.Bottom);
+ corners[3] = new Vector2(rectangle.Left, rectangle.Bottom);
+ return corners;
+ }
+
+ public static Rectangle ToRectangle(this RectangleF rectangle)
+ {
+ return new Rectangle((int) rectangle.X, (int) rectangle.Y, (int) rectangle.Width, (int) rectangle.Height);
+ }
+
+ public static RectangleF ToRectangleF(this Rectangle rectangle)
+ {
+ return new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
+ }
+
+ public static Rectangle Clip(this Rectangle rectangle, Rectangle clippingRectangle)
+ {
+ var clip = clippingRectangle;
+ rectangle.X = clip.X > rectangle.X ? clip.X : rectangle.X;
+ rectangle.Y = clip.Y > rectangle.Y ? clip.Y : rectangle.Y;
+ rectangle.Width = rectangle.Right > clip.Right ? clip.Right - rectangle.X : rectangle.Width;
+ rectangle.Height = rectangle.Bottom > clip.Bottom ? clip.Bottom - rectangle.Y : rectangle.Height;
+
+ if (rectangle.Width <= 0 || rectangle.Height <= 0)
+ return Rectangle.Empty;
+
+ return rectangle;
+ }
+
+ public static RectangleF Clip(this RectangleF rectangle, RectangleF clippingRectangle)
+ {
+ var clip = clippingRectangle;
+ rectangle.X = clip.X > rectangle.X ? clip.X : rectangle.X;
+ rectangle.Y = clip.Y > rectangle.Y ? clip.Y : rectangle.Y;
+ rectangle.Width = rectangle.Right > clip.Right ? clip.Right - rectangle.X : rectangle.Width;
+ rectangle.Height = rectangle.Bottom > clip.Bottom ? clip.Bottom - rectangle.Y : rectangle.Height;
+
+ if(rectangle.Width <= 0 || rectangle.Height <= 0)
+ return RectangleF.Empty;
+
+ return rectangle;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleF.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleF.cs
new file mode 100644
index 0000000..1a1d685
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleF.cs
@@ -0,0 +1,694 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.Serialization;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.2; Bounding Volumes - Axis-aligned Bounding Boxes (AABBs). pg 77
+
+ /// <summary>
+ /// An axis-aligned, four sided, two dimensional box defined by a top-left position (<see cref="X" /> and
+ /// <see cref="Y" />) and a size (<see cref="Width" /> and <see cref="Height" />).
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// An <see cref="RectangleF" /> is categorized by having its faces oriented in such a way that its
+ /// face normals are at all times parallel with the axes of the given coordinate system.
+ /// </para>
+ /// <para>
+ /// The bounding <see cref="RectangleF" /> of a rotated <see cref="RectangleF" /> will be equivalent or larger
+ /// in size than the original depending on the angle of rotation.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="IEquatable{T}" />
+ /// <seealso cref="IEquatableByRef{T}" />
+ [DataContract]
+ [DebuggerDisplay("{DebugDisplayString,nq}")]
+ public struct RectangleF : IEquatable<RectangleF>, IEquatableByRef<RectangleF>, IShapeF
+ {
+ /// <summary>
+ /// The <see cref="RectangleF" /> with <see cref="X" />, <see cref="Y" />, <see cref="Width" /> and
+ /// <see cref="Height" /> all set to <code>0.0f</code>.
+ /// </summary>
+ public static readonly RectangleF Empty = new RectangleF();
+
+ /// <summary>
+ /// The x-coordinate of the top-left corner position of this <see cref="RectangleF" />.
+ /// </summary>
+ [DataMember] public float X;
+
+ /// <summary>
+ /// The y-coordinate of the top-left corner position of this <see cref="RectangleF" />.
+ /// </summary>
+ [DataMember] public float Y;
+
+ /// <summary>
+ /// The width of this <see cref="RectangleF" />.
+ /// </summary>
+ [DataMember] public float Width;
+
+ /// <summary>
+ /// The height of this <see cref="RectangleF" />.
+ /// </summary>
+ [DataMember] public float Height;
+
+ /// <summary>
+ /// Gets the x-coordinate of the left edge of this <see cref="RectangleF" />.
+ /// </summary>
+ public float Left => X;
+
+ /// <summary>
+ /// Gets the x-coordinate of the right edge of this <see cref="RectangleF" />.
+ /// </summary>
+ public float Right => X + Width;
+
+ /// <summary>
+ /// Gets the y-coordinate of the top edge of this <see cref="RectangleF" />.
+ /// </summary>
+ public float Top => Y;
+
+ /// <summary>
+ /// Gets the y-coordinate of the bottom edge of this <see cref="RectangleF" />.
+ /// </summary>
+ public float Bottom => Y + Height;
+
+ /// <summary>
+ /// Gets a value indicating whether this <see cref="RectangleF" /> has a <see cref="X" />, <see cref="Y" />,
+ /// <see cref="Width" />,
+ /// <see cref="Height" /> all equal to <code>0.0f</code>.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this instance is empty; otherwise, <c>false</c>.
+ /// </value>
+ public bool IsEmpty => Width.Equals(0) && Height.Equals(0) && X.Equals(0) && Y.Equals(0);
+
+ /// <summary>
+ /// Gets the <see cref="Point2" /> representing the the top-left of this <see cref="RectangleF" />.
+ /// </summary>
+ public Point2 Position
+ {
+ get { return new Point2(X, Y); }
+ set
+ {
+ X = value.X;
+ Y = value.Y;
+ }
+ }
+
+ public RectangleF BoundingRectangle => this;
+
+ /// <summary>
+ /// Gets the <see cref="Size2" /> representing the extents of this <see cref="RectangleF" />.
+ /// </summary>
+ public Size2 Size
+ {
+ get { return new Size2(Width, Height); }
+ set
+ {
+ Width = value.Width;
+ Height = value.Height;
+ }
+ }
+
+ /// <summary>
+ /// Gets the <see cref="Point2" /> representing the center of this <see cref="RectangleF" />.
+ /// </summary>
+ public Point2 Center => new Point2(X + Width * 0.5f, Y + Height * 0.5f);
+
+ /// <summary>
+ /// Gets the <see cref="Point2" /> representing the top-left of this <see cref="RectangleF" />.
+ /// </summary>
+ public Point2 TopLeft => new Point2(X, Y);
+
+ /// <summary>
+ /// Gets the <see cref="Point2" /> representing the top-right of this <see cref="RectangleF" />.
+ /// </summary>
+ public Point2 TopRight => new Point2(X + Width, Y);
+
+ /// <summary>
+ /// Gets the <see cref="Point2" /> representing the bottom-left of this <see cref="RectangleF" />.
+ /// </summary>
+ public Point2 BottomLeft => new Point2(X, Y + Height);
+
+ /// <summary>
+ /// Gets the <see cref="Point2" /> representing the bottom-right of this <see cref="RectangleF" />.
+ /// </summary>
+ public Point2 BottomRight => new Point2(X + Width, Y + Height);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RectangleF" /> structure from the specified top-left xy-coordinate
+ /// <see cref="float" />s, width <see cref="float" /> and height <see cref="float" />.
+ /// </summary>
+ /// <param name="x">The x-coordinate.</param>
+ /// <param name="y">The y-coordinate.</param>
+ /// <param name="width">The width.</param>
+ /// <param name="height">The height.</param>
+ public RectangleF(float x, float y, float width, float height)
+ {
+ X = x;
+ Y = y;
+ Width = width;
+ Height = height;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RectangleF" /> structure from the specified top-left
+ /// <see cref="Point2" /> and the extents <see cref="Size2" />.
+ /// </summary>
+ /// <param name="position">The top-left point.</param>
+ /// <param name="size">The extents.</param>
+ public RectangleF(Point2 position, Size2 size)
+ {
+ X = position.X;
+ Y = position.Y;
+ Width = size.Width;
+ Height = size.Height;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> from a minimum <see cref="Point2" /> and maximum
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="minimum">The minimum point.</param>
+ /// <param name="maximum">The maximum point.</param>
+ /// <param name="result">The resulting rectangle.</param>
+ public static void CreateFrom(Point2 minimum, Point2 maximum, out RectangleF result)
+ {
+ result.X = minimum.X;
+ result.Y = minimum.Y;
+ result.Width = maximum.X - minimum.X;
+ result.Height = maximum.Y - minimum.Y;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> from a minimum <see cref="Point2" /> and maximum
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="minimum">The minimum point.</param>
+ /// <param name="maximum">The maximum point.</param>
+ /// <returns>The resulting <see cref="RectangleF" />.</returns>
+ public static RectangleF CreateFrom(Point2 minimum, Point2 maximum)
+ {
+ RectangleF result;
+ CreateFrom(minimum, maximum, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> from a list of <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="points">The points.</param>
+ /// <param name="result">The resulting rectangle.</param>
+ public static void CreateFrom(IReadOnlyList<Point2> points, out RectangleF result)
+ {
+ Point2 minimum;
+ Point2 maximum;
+ PrimitivesHelper.CreateRectangleFromPoints(points, out minimum, out maximum);
+ CreateFrom(minimum, maximum, out result);
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> from a list of <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="points">The points.</param>
+ /// <returns>The resulting <see cref="RectangleF" />.</returns>
+ public static RectangleF CreateFrom(IReadOnlyList<Point2> points)
+ {
+ RectangleF result;
+ CreateFrom(points, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> from the specified <see cref="RectangleF" /> transformed by
+ /// the specified <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle to be transformed.</param>
+ /// <param name="transformMatrix">The transform matrix.</param>
+ /// <param name="result">The resulting transformed rectangle.</param>
+ /// <returns>
+ /// The <see cref="Extended.BoundingRectangle" /> from the <paramref name="rectangle" /> transformed by the
+ /// <paramref name="transformMatrix" />.
+ /// </returns>
+ /// <remarks>
+ /// <para>
+ /// If a transformed <see cref="Extended.BoundingRectangle" /> is used for <paramref name="rectangle" /> then the
+ /// resulting <see cref="Extended.BoundingRectangle" /> will have the compounded transformation, which most likely is
+ /// not desired.
+ /// </para>
+ /// </remarks>
+ public static void Transform(ref RectangleF rectangle,
+ ref Matrix2 transformMatrix, out RectangleF result)
+ {
+ var center = rectangle.Center;
+ var halfExtents = (Vector2)rectangle.Size * 0.5f;
+
+ PrimitivesHelper.TransformRectangle(ref center, ref halfExtents, ref transformMatrix);
+
+ result.X = center.X - halfExtents.X;
+ result.Y = center.Y - halfExtents.Y;
+ result.Width = halfExtents.X * 2;
+ result.Height = halfExtents.Y * 2;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> from the specified <see cref="Extended.BoundingRectangle" /> transformed by
+ /// the
+ /// specified <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="rectangle">The bounding rectangle.</param>
+ /// <param name="transformMatrix">The transform matrix.</param>
+ /// <returns>
+ /// The <see cref="Extended.BoundingRectangle" /> from the <paramref name="rectangle" /> transformed by the
+ /// <paramref name="transformMatrix" />.
+ /// </returns>
+ /// <remarks>
+ /// <para>
+ /// If a transformed <see cref="Extended.BoundingRectangle" /> is used for <paramref name="rectangle" /> then the
+ /// resulting <see cref="Extended.BoundingRectangle" /> will have the compounded transformation, which most likely is
+ /// not desired.
+ /// </para>
+ /// </remarks>
+ public static RectangleF Transform(RectangleF rectangle, ref Matrix2 transformMatrix)
+ {
+ RectangleF result;
+ Transform(ref rectangle, ref transformMatrix, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> that contains the two specified
+ /// <see cref="RectangleF" /> structures.
+ /// </summary>
+ /// <param name="first">The first rectangle.</param>
+ /// <param name="second">The second rectangle.</param>
+ /// <param name="result">The resulting rectangle that contains both the <paramref name="first" /> and the
+ /// <paramref name="second" />.</param>
+ public static void Union(ref RectangleF first, ref RectangleF second, out RectangleF result)
+ {
+ result.X = Math.Min(first.X, second.X);
+ result.Y = Math.Min(first.Y, second.Y);
+ result.Width = Math.Max(first.Right, second.Right) - result.X;
+ result.Height = Math.Max(first.Bottom, second.Bottom) - result.Y;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> that contains the two specified
+ /// <see cref="RectangleF" /> structures.
+ /// </summary>
+ /// <param name="first">The first rectangle.</param>
+ /// <param name="second">The second rectangle.</param>
+ /// <returns>
+ /// An <see cref="RectangleF" /> that contains both the <paramref name="first" /> and the
+ /// <paramref name="second" />.
+ /// </returns>
+ public static RectangleF Union(RectangleF first, RectangleF second)
+ {
+ RectangleF result;
+ Union(ref first, ref second, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> that contains both the specified <see cref="RectangleF" /> and this <see cref="RectangleF" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// An <see cref="RectangleF" /> that contains both the <paramref name="rectangle" /> and
+ /// this <see cref="RectangleF" />.
+ /// </returns>
+ public RectangleF Union(RectangleF rectangle)
+ {
+ RectangleF result;
+ Union(ref this, ref rectangle, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> that is in common between the two specified
+ /// <see cref="RectangleF" /> structures.
+ /// </summary>
+ /// <param name="first">The first rectangle.</param>
+ /// <param name="second">The second rectangle.</param>
+ /// <param name="result">The resulting rectangle that is in common between both the <paramref name="first" /> and
+ /// the <paramref name="second" />, if they intersect; otherwise, <see cref="Empty"/>.</param>
+ public static void Intersection(ref RectangleF first,
+ ref RectangleF second, out RectangleF result)
+ {
+ var firstMinimum = first.TopLeft;
+ var firstMaximum = first.BottomRight;
+ var secondMinimum = second.TopLeft;
+ var secondMaximum = second.BottomRight;
+
+ var minimum = Point2.Maximum(firstMinimum, secondMinimum);
+ var maximum = Point2.Minimum(firstMaximum, secondMaximum);
+
+ if ((maximum.X < minimum.X) || (maximum.Y < minimum.Y))
+ result = new RectangleF();
+ else
+ result = CreateFrom(minimum, maximum);
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> that is in common between the two specified
+ /// <see cref="RectangleF" /> structures.
+ /// </summary>
+ /// <param name="first">The first rectangle.</param>
+ /// <param name="second">The second rectangle.</param>
+ /// <returns>
+ /// A <see cref="RectangleF" /> that is in common between both the <paramref name="first" /> and
+ /// the <paramref name="second" />, if they intersect; otherwise, <see cref="Empty"/>.
+ /// </returns>
+ public static RectangleF Intersection(RectangleF first,
+ RectangleF second)
+ {
+ RectangleF result;
+ Intersection(ref first, ref second, out result);
+ return result;
+ }
+
+ /// <summary>
+ /// Computes the <see cref="RectangleF" /> that is in common between the specified
+ /// <see cref="RectangleF" /> and this <see cref="RectangleF" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// A <see cref="RectangleF" /> that is in common between both the <paramref name="rectangle" /> and
+ /// this <see cref="RectangleF"/>, if they intersect; otherwise, <see cref="Empty"/>.
+ /// </returns>
+ public RectangleF Intersection(RectangleF rectangle)
+ {
+ RectangleF result;
+ Intersection(ref this, ref rectangle, out result);
+ return result;
+ }
+
+ [Obsolete("RectangleF.Intersect() may be removed in the future. Use Intersection() instead.")]
+ public static RectangleF Intersect(RectangleF value1, RectangleF value2)
+ {
+ RectangleF rectangle;
+ Intersection(ref value1, ref value2, out rectangle);
+ return rectangle;
+ }
+
+ [Obsolete("RectangleF.Intersect() may be removed in the future. Use Intersection() instead.")]
+ public static void Intersect(ref RectangleF value1, ref RectangleF value2, out RectangleF result)
+ {
+ Intersection(ref value1, ref value2, out result);
+ }
+
+ /// <summary>
+ /// Determines whether the two specified <see cref="RectangleF" /> structures intersect.
+ /// </summary>
+ /// <param name="first">The first rectangle.</param>
+ /// <param name="second">The second rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="first" /> intersects with the <see cref="second" />; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool Intersects(ref RectangleF first, ref RectangleF second)
+ {
+ return first.X < second.X + second.Width && first.X + first.Width > second.X &&
+ first.Y < second.Y + second.Height && first.Y + first.Height > second.Y;
+ }
+
+ /// <summary>
+ /// Determines whether the two specified <see cref="RectangleF" /> structures intersect.
+ /// </summary>
+ /// <param name="first">The first rectangle.</param>
+ /// <param name="second">The second rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="first" /> intersects with the <see cref="second" />; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool Intersects(RectangleF first, RectangleF second)
+ {
+ return Intersects(ref first, ref second);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="RectangleF" /> intersects with this
+ /// <see cref="RectangleF" />.
+ /// </summary>
+ /// <param name="rectangle">The bounding rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="rectangle" /> intersects with this
+ /// <see cref="RectangleF" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Intersects(RectangleF rectangle)
+ {
+ return Intersects(ref this, ref rectangle);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="RectangleF" /> contains the specified
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="rectangle" /> contains the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public static bool Contains(ref RectangleF rectangle, ref Point2 point)
+ {
+ return rectangle.X <= point.X && point.X < rectangle.X + rectangle.Width && rectangle.Y <= point.Y && point.Y < rectangle.Y + rectangle.Height;
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="RectangleF" /> contains the specified
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if the <paramref name="rectangle" /> contains the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public static bool Contains(RectangleF rectangle, Point2 point)
+ {
+ return Contains(ref rectangle, ref point);
+ }
+
+ /// <summary>
+ /// Determines whether this <see cref="RectangleF" /> contains the specified
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// <c>true</c> if the this <see cref="RectangleF"/> contains the <paramref name="point" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Contains(Point2 point)
+ {
+ return Contains(ref this, ref point);
+ }
+
+ /// <summary>
+ /// Updates this <see cref="RectangleF" /> from a list of <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="points">The points.</param>
+ public void UpdateFromPoints(IReadOnlyList<Point2> points)
+ {
+ var rectangle = CreateFrom(points);
+ X = rectangle.X;
+ Y = rectangle.Y;
+ Width = rectangle.Width;
+ Height = rectangle.Height;
+ }
+
+ /// <summary>
+ /// Computes the squared distance from this <see cref="RectangleF"/> to a <see cref="Point2"/>.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>The squared distance from this <see cref="RectangleF"/> to the <paramref name="point"/>.</returns>
+ public float SquaredDistanceTo(Point2 point)
+ {
+ return PrimitivesHelper.SquaredDistanceToPointFromRectangle(TopLeft, BottomRight, point);
+ }
+
+ /// <summary>
+ /// Computes the distance from this <see cref="RectangleF"/> to a <see cref="Point2"/>.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>The distance from this <see cref="RectangleF"/> to the <paramref name="point"/>.</returns>
+ public float DistanceTo(Point2 point)
+ {
+ return (float)Math.Sqrt(SquaredDistanceTo(point));
+ }
+
+ /// <summary>
+ /// Computes the closest <see cref="Point2" /> on this <see cref="RectangleF" /> to a specified
+ /// <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>The closest <see cref="Point2" /> on this <see cref="RectangleF" /> to the <paramref name="point" />.</returns>
+ public Point2 ClosestPointTo(Point2 point)
+ {
+ Point2 result;
+ PrimitivesHelper.ClosestPointToPointFromRectangle(TopLeft, BottomRight, point, out result);
+ return result;
+ }
+
+ //TODO: Document this.
+ public void Inflate(float horizontalAmount, float verticalAmount)
+ {
+ X -= horizontalAmount;
+ Y -= verticalAmount;
+ Width += horizontalAmount * 2;
+ Height += verticalAmount * 2;
+ }
+
+ //TODO: Document this.
+ public void Offset(float offsetX, float offsetY)
+ {
+ X += offsetX;
+ Y += offsetY;
+ }
+
+ //TODO: Document this.
+ public void Offset(Vector2 amount)
+ {
+ X += amount.X;
+ Y += amount.Y;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="RectangleF" /> structures. The result specifies whether the values of the
+ /// <see cref="X" />, <see cref="Y"/>, <see cref="Width"/> and <see cref="Height" /> fields of the two <see cref="RectangleF" /> structures
+ /// are equal.
+ /// </summary>
+ /// <param name="first">The first rectangle.</param>
+ /// <param name="second">The second rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the values of the
+ /// <see cref="X" />, <see cref="Y"/>, <see cref="Width"/> and <see cref="Height" /> fields of the two <see cref="RectangleF" /> structures
+ /// are equal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(RectangleF first, RectangleF second)
+ {
+ return first.Equals(ref second);
+ }
+
+ /// <summary>
+ /// Compares two <see cref="RectangleF" /> structures. The result specifies whether the values of the
+ /// <see cref="X" />, <see cref="Y"/>, <see cref="Width"/> and <see cref="Height" /> fields of the two <see cref="RectangleF" /> structures
+ /// are unequal.
+ /// </summary>
+ /// <param name="first">The first rectangle.</param>
+ /// <param name="second">The second rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if the values of the
+ /// <see cref="X" />, <see cref="Y"/>, <see cref="Width"/> and <see cref="Height" /> fields of the two <see cref="RectangleF" /> structures
+ /// are unequal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(RectangleF first, RectangleF second)
+ {
+ return !(first == second);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="RectangleF" /> is equal to another <see cref="RectangleF" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="RectangleF" /> is equal to the <paramref name="rectangle" />; otherwise, <c>false</c>.
+ /// </returns>
+ public bool Equals(RectangleF rectangle)
+ {
+ return Equals(ref rectangle);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="RectangleF" /> is equal to another <see cref="RectangleF" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="RectangleF" /> is equal to the <paramref name="rectangle" />; otherwise, <c>false</c>.
+ /// </returns>
+ public bool Equals(ref RectangleF rectangle)
+ {
+ // ReSharper disable CompareOfFloatsByEqualityOperator
+ return X == rectangle.X && Y == rectangle.Y && Width == rectangle.Width && Height == rectangle.Height;
+ // ReSharper restore CompareOfFloatsByEqualityOperator
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="RectangleF" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object to make the comparison with.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="RectangleF" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ return obj is RectangleF && Equals((RectangleF)obj);
+ }
+
+ /// <summary>
+ /// Returns a hash code of this <see cref="RectangleF" /> suitable for use in hashing algorithms and data
+ /// structures like a hash table.
+ /// </summary>
+ /// <returns>
+ /// A hash code of this <see cref="RectangleF" />.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = X.GetHashCode();
+ hashCode = (hashCode * 397) ^ Y.GetHashCode();
+ hashCode = (hashCode * 397) ^ Width.GetHashCode();
+ hashCode = (hashCode * 397) ^ Height.GetHashCode();
+ return hashCode;
+ }
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Rectangle" /> to a <see cref="RectangleF" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// The resulting <see cref="RectangleF" />.
+ /// </returns>
+ public static implicit operator RectangleF(Rectangle rectangle)
+ {
+ return new RectangleF
+ {
+ X = rectangle.X,
+ Y = rectangle.Y,
+ Width = rectangle.Width,
+ Height = rectangle.Height
+ };
+ }
+
+ /// <summary>
+ /// Performs an explicit conversion from a <see cref="Rectangle" /> to a <see cref="RectangleF" />.
+ /// </summary>
+ /// <param name="rectangle">The rectangle.</param>
+ /// <returns>
+ /// The resulting <see cref="RectangleF" />.
+ /// </returns>
+ /// <remarks>
+ /// <para>A loss of precision may occur due to the truncation from <see cref="float" /> to <see cref="int" />.</para>
+ /// </remarks>
+ public static explicit operator Rectangle(RectangleF rectangle)
+ {
+ return new Rectangle((int)rectangle.X, (int)rectangle.Y, (int)rectangle.Width, (int)rectangle.Height);
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="RectangleF" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="RectangleF" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"{{X: {X}, Y: {Y}, Width: {Width}, Height: {Height}";
+ }
+
+ internal string DebugDisplayString => string.Concat(X, " ", Y, " ", Width, " ", Height);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Segment2.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Segment2.cs
new file mode 100644
index 0000000..d8452ae
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Segment2.cs
@@ -0,0 +1,317 @@
+using System;
+
+namespace MonoGame.Extended
+{
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 3.5; A Math and Geometry Primer - Lines, Rays, and Segments. pg 53-54
+ /// <summary>
+ /// A two dimensional line segment defined by two <see cref="Point2" /> structures, a starting point and an ending
+ /// point.
+ /// </summary>
+ /// <seealso cref="IEquatable{T}" />
+ /// <seealso cref="IEquatableByRef{Segment2}" />
+ public struct Segment2 : IEquatable<Segment2>, IEquatableByRef<Segment2>
+ {
+ /// <summary>
+ /// The starting <see cref="Point2" /> of this <see cref="Segment2" />.
+ /// </summary>
+ public Point2 Start;
+
+ /// <summary>
+ /// The ending <see cref="Point2" /> of this <see cref="Segment2" />.
+ /// </summary>
+ public Point2 End;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Segment2" /> structure from the specified starting and ending
+ /// <see cref="Point2" /> structures.
+ /// </summary>
+ /// <param name="start">The starting point.</param>
+ /// <param name="end">The ending point.</param>
+ public Segment2(Point2 start, Point2 end)
+ {
+ Start = start;
+ End = end;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Segment2" /> structure.
+ /// </summary>
+ /// <param name="x1">The starting x-coordinate.</param>
+ /// <param name="y1">The starting y-coordinate.</param>
+ /// <param name="x2">The ending x-coordinate.</param>
+ /// <param name="y2">The ending y-coordinate.</param>
+ public Segment2(float x1, float y1, float x2, float y2)
+ : this(new Point2(x1, y1), new Point2(x2, y2))
+ {
+ }
+
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.1.2; Basic Primitive Tests - Closest Point on Line Segment to Point. pg 127-130
+ /// <summary>
+ /// Computes the closest <see cref="Point2" /> on this <see cref="Segment2" /> to a specified <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>The closest <see cref="Point2" /> on this <see cref="Segment2" /> to the <paramref name="point" />.</returns>
+ public Point2 ClosestPointTo(Point2 point)
+ {
+ // Computes the parameterized position: d(t) = Start + t * (End – Start)
+
+ var startToEnd = End - Start;
+ var startToPoint = point - Start;
+ // Project arbitrary point onto the line segment, deferring the division
+ var t = startToEnd.Dot(startToPoint);
+ // If outside segment, clamp t (and therefore d) to the closest endpoint
+ if (t <= 0)
+ return Start;
+
+ // Always nonnegative since denom = (||vector||)^2
+ var denominator = startToEnd.Dot(startToEnd);
+ if (t >= denominator)
+ return End;
+
+ // The point projects inside the [Start, End] interval, must do deferred division now
+ t /= denominator;
+ startToEnd *= t;
+ return new Point2(Start.X + startToEnd.X, Start.Y + startToEnd.Y);
+ }
+
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.1.2.1; Basic Primitive Tests - Distance of Point to Segment. pg 127-130
+ /// <summary>
+ /// Computes the squared distance from this <see cref="Segment2" /> to a specified <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>The squared distance from this <see cref="Segment2" /> to a specified <see cref="Point2" />.</returns>
+ public float SquaredDistanceTo(Point2 point)
+ {
+ var startToEnd = End - Start;
+ var startToPoint = point - Start;
+ var endToPoint = point - End;
+ // Handle cases where the point projects outside the line segment
+ var dot = startToPoint.Dot(startToEnd);
+ var startToPointDistanceSquared = startToPoint.Dot(startToPoint);
+ if (dot <= 0.0f)
+ return startToPointDistanceSquared;
+ var startToEndDistanceSquared = startToEnd.Dot(startToEnd);
+ if (dot >= startToEndDistanceSquared)
+ endToPoint.Dot(endToPoint);
+ // Handle the case where the point projects onto the line segment
+ return startToPointDistanceSquared - dot*dot/startToEndDistanceSquared;
+ }
+
+ /// <summary>
+ /// Computes the distance from this <see cref="Segment2" /> to a specified <see cref="Point2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>The distance from this <see cref="Segment2" /> to a specified <see cref="Point2" />.</returns>
+ public float DistanceTo(Point2 point)
+ {
+ return (float) Math.Sqrt(SquaredDistanceTo(point));
+ }
+
+ /// <summary>
+ /// Determines whether this <see cref="Segment2" /> intersects with the specified <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="rectangle">The bounding box.</param>
+ /// <param name="intersectionPoint">
+ /// When this method returns, contains the <see cref="Point2" /> of intersection, if an
+ /// intersection was found; otherwise, the <see cref="Point2.NaN" />. This parameter is passed uninitialized.
+ /// </param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Segment2" /> intersects with <paramref name="rectangle" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Intersects(RectangleF rectangle, out Point2 intersectionPoint)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.3; Basic Primitive Tests - Intersecting Lines, Rays, and (Directed Segments). pg 179-181
+
+ var minimumPoint = rectangle.TopLeft;
+ var maximumPoint = rectangle.BottomRight;
+ var minimumDistance = float.MinValue;
+ var maximumDistance = float.MaxValue;
+
+ var direction = End - Start;
+ if (
+ !PrimitivesHelper.IntersectsSlab(Start.X, direction.X, minimumPoint.X, maximumPoint.X, ref minimumDistance,
+ ref maximumDistance))
+ {
+ intersectionPoint = Point2.NaN;
+ return false;
+ }
+
+ if (
+ !PrimitivesHelper.IntersectsSlab(Start.Y, direction.Y, minimumPoint.Y, maximumPoint.Y, ref minimumDistance,
+ ref maximumDistance))
+ {
+ intersectionPoint = Point2.NaN;
+ return false;
+ }
+
+ // Segment intersects the 2 slabs.
+
+ if (minimumDistance <= 0)
+ intersectionPoint = Start;
+ else
+ {
+ intersectionPoint = minimumDistance * direction;
+ intersectionPoint.X += Start.X;
+ intersectionPoint.Y += Start.Y;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Determines whether this <see cref="Segment2" /> intersects with the specified <see cref="BoundingRectangle" />.
+ /// </summary>
+ /// <param name="boundingRectangle">The bounding box.</param>
+ /// <param name="intersectionPoint">
+ /// When this method returns, contains the <see cref="Point2" /> of intersection, if an
+ /// intersection was found; otherwise, the <see cref="Point2.NaN" />. This parameter is passed uninitialized.
+ /// </param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Segment2" /> intersects with <paramref name="boundingRectangle" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Intersects(BoundingRectangle boundingRectangle, out Point2 intersectionPoint)
+ {
+ // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.3; Basic Primitive Tests - Intersecting Lines, Rays, and (Directed Segments). pg 179-181
+
+ var minimumPoint = boundingRectangle.Center - boundingRectangle.HalfExtents;
+ var maximumPoint = boundingRectangle.Center + boundingRectangle.HalfExtents;
+ var minimumDistance = float.MinValue;
+ var maximumDistance = float.MaxValue;
+
+ var direction = End - Start;
+ if (
+ !PrimitivesHelper.IntersectsSlab(Start.X, direction.X, minimumPoint.X, maximumPoint.X, ref minimumDistance,
+ ref maximumDistance))
+ {
+ intersectionPoint = Point2.NaN;
+ return false;
+ }
+
+ if (
+ !PrimitivesHelper.IntersectsSlab(Start.Y, direction.Y, minimumPoint.Y, maximumPoint.Y, ref minimumDistance,
+ ref maximumDistance))
+ {
+ intersectionPoint = Point2.NaN;
+ return false;
+ }
+
+ // Segment intersects the 2 slabs.
+
+ if (minimumDistance <= 0)
+ intersectionPoint = Start;
+ else
+ {
+ intersectionPoint = minimumDistance*direction;
+ intersectionPoint.X += Start.X;
+ intersectionPoint.Y += Start.Y;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Segment2" /> structures. The result specifies
+ /// whether the values of the <see cref="Start" /> and <see cref="End" />
+ /// fields of the two <see cref='Segment2' />
+ /// structures are equal.
+ /// </summary>
+ /// <param name="first">The first segment.</param>
+ /// <param name="second">The second segment.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Start" /> and <see cref="End" />
+ /// fields of the two <see cref="Segment2" />
+ /// structures are equal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(Segment2 first, Segment2 second)
+ {
+ return first.Equals(ref second);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Segment2" /> is equal to another <see cref="Segment2" />.
+ /// </summary>
+ /// <param name="segment">The segment.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Segment2" /> is equal to the <paramref name="segment" />; otherwise, <c>false</c>.
+ /// </returns>
+ public bool Equals(Segment2 segment)
+ {
+ return Equals(ref segment);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Segment2" /> is equal to another <see cref="Segment2" />.
+ /// </summary>
+ /// <param name="segment">The segment.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Segment2" /> is equal to the <paramref name="segment" /> parameter; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(ref Segment2 segment)
+ {
+ return (Start == segment.Start) && (End == segment.End);
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="Segment2" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object to make the comparison with.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Segment2" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ if (obj is Segment2)
+ return Equals((Segment2) obj);
+ return false;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Segment2" /> structures. The result specifies
+ /// whether the values of the <see cref="Start" /> and <see cref="End" />
+ /// fields of the two <see cref="Segment2" />
+ /// structures are unequal.
+ /// </summary>
+ /// <param name="first">The first point.</param>
+ /// <param name="second">The second point.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Start" /> and <see cref="End" />
+ /// fields of the two <see cref="Segment2" />
+ /// structures are unequal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(Segment2 first, Segment2 second)
+ {
+ return !(first == second);
+ }
+
+ /// <summary>
+ /// Returns a hash code of this <see cref="Segment2" /> suitable for use in hashing algorithms and data
+ /// structures like a hash table.
+ /// </summary>
+ /// <returns>
+ /// A hash code of this <see cref="Segment2" />.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (Start.GetHashCode()*397) ^ End.GetHashCode();
+ }
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="Segment2" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="Segment2" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"{Start} -> {End}";
+ }
+
+ internal string DebugDisplayString => ToString();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeExtensions.cs
new file mode 100644
index 0000000..f1f6f10
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeExtensions.cs
@@ -0,0 +1,332 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.Shapes;
+
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// Sprite batch extensions for drawing primitive shapes
+ /// </summary>
+ public static class ShapeExtensions
+ {
+ private static Texture2D _whitePixelTexture;
+
+ private static Texture2D GetTexture(SpriteBatch spriteBatch)
+ {
+ if (_whitePixelTexture == null)
+ {
+ _whitePixelTexture = new Texture2D(spriteBatch.GraphicsDevice, 1, 1, false, SurfaceFormat.Color);
+ _whitePixelTexture.SetData(new[] { Color.White });
+ spriteBatch.Disposing += (sender, args) =>
+ {
+ _whitePixelTexture?.Dispose();
+ _whitePixelTexture = null;
+ };
+ }
+
+ return _whitePixelTexture;
+ }
+
+ /// <summary>
+ /// Draws a closed polygon from a <see cref="Polygon" /> shape
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// ///
+ /// <param name="position">Where to position the polygon</param>
+ /// <param name="polygon">The polygon to draw</param>
+ /// <param name="color">The color to use</param>
+ /// <param name="thickness">The thickness of the lines</param>
+ /// /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawPolygon(this SpriteBatch spriteBatch, Vector2 position, Polygon polygon, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ DrawPolygon(spriteBatch, position, polygon.Vertices, color, thickness, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a closed polygon from an array of points
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// ///
+ /// <param name="offset">Where to offset the points</param>
+ /// <param name="points">The points to connect with lines</param>
+ /// <param name="color">The color to use</param>
+ /// <param name="thickness">The thickness of the lines</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawPolygon(this SpriteBatch spriteBatch, Vector2 offset, IReadOnlyList<Vector2> points, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ if (points.Count == 0)
+ return;
+
+ if (points.Count == 1)
+ {
+ DrawPoint(spriteBatch, points[0], color, (int)thickness);
+ return;
+ }
+
+ var texture = GetTexture(spriteBatch);
+
+ for (var i = 0; i < points.Count - 1; i++)
+ DrawPolygonEdge(spriteBatch, texture, points[i] + offset, points[i + 1] + offset, color, thickness, layerDepth);
+
+ DrawPolygonEdge(spriteBatch, texture, points[points.Count - 1] + offset, points[0] + offset, color, thickness, layerDepth);
+ }
+
+ private static void DrawPolygonEdge(SpriteBatch spriteBatch, Texture2D texture, Vector2 point1, Vector2 point2, Color color, float thickness, float layerDepth)
+ {
+ var length = Vector2.Distance(point1, point2);
+ var angle = (float)Math.Atan2(point2.Y - point1.Y, point2.X - point1.X);
+ var scale = new Vector2(length, thickness);
+ spriteBatch.Draw(texture, point1, null, color, angle, Vector2.Zero, scale, SpriteEffects.None, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a filled rectangle
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="rectangle">The rectangle to draw</param>
+ /// <param name="color">The color to draw the rectangle in</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void FillRectangle(this SpriteBatch spriteBatch, RectangleF rectangle, Color color, float layerDepth = 0)
+ {
+ FillRectangle(spriteBatch, rectangle.Position, rectangle.Size, color, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a filled rectangle
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="location">Where to draw</param>
+ /// <param name="size">The size of the rectangle</param>
+ /// <param name="color">The color to draw the rectangle in</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void FillRectangle(this SpriteBatch spriteBatch, Vector2 location, Size2 size, Color color, float layerDepth = 0)
+ {
+ spriteBatch.Draw(GetTexture(spriteBatch), location, null, color, 0, Vector2.Zero, size, SpriteEffects.None, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a filled rectangle
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="x">The X coord of the left side</param>
+ /// <param name="y">The Y coord of the upper side</param>
+ /// <param name="width">Width</param>
+ /// <param name="height">Height</param>
+ /// <param name="color">The color to draw the rectangle in</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void FillRectangle(this SpriteBatch spriteBatch, float x, float y, float width, float height, Color color, float layerDepth = 0)
+ {
+ FillRectangle(spriteBatch, new Vector2(x, y), new Size2(width, height), color, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a rectangle with the thickness provided
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="rectangle">The rectangle to draw</param>
+ /// <param name="color">The color to draw the rectangle in</param>
+ /// <param name="thickness">The thickness of the lines</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawRectangle(this SpriteBatch spriteBatch, RectangleF rectangle, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ var texture = GetTexture(spriteBatch);
+ var topLeft = new Vector2(rectangle.X, rectangle.Y);
+ var topRight = new Vector2(rectangle.Right - thickness, rectangle.Y);
+ var bottomLeft = new Vector2(rectangle.X, rectangle.Bottom - thickness);
+ var horizontalScale = new Vector2(rectangle.Width, thickness);
+ var verticalScale = new Vector2(thickness, rectangle.Height);
+
+ spriteBatch.Draw(texture, topLeft, null, color, 0f, Vector2.Zero, horizontalScale, SpriteEffects.None, layerDepth);
+ spriteBatch.Draw(texture, topLeft, null, color, 0f, Vector2.Zero, verticalScale, SpriteEffects.None, layerDepth);
+ spriteBatch.Draw(texture, topRight, null, color, 0f, Vector2.Zero, verticalScale, SpriteEffects.None, layerDepth);
+ spriteBatch.Draw(texture, bottomLeft, null, color, 0f, Vector2.Zero, horizontalScale, SpriteEffects.None, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a rectangle with the thickness provided
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="location">Where to draw</param>
+ /// <param name="size">The size of the rectangle</param>
+ /// <param name="color">The color to draw the rectangle in</param>
+ /// <param name="thickness">The thickness of the line</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawRectangle(this SpriteBatch spriteBatch, Vector2 location, Size2 size, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ DrawRectangle(spriteBatch, new RectangleF(location.X, location.Y, size.Width, size.Height), color, thickness, layerDepth);
+ }
+
+
+ /// <summary>
+ /// Draws a rectangle outline.
+ /// </summary>
+ public static void DrawRectangle(this SpriteBatch spriteBatch, float x, float y, float width, float height, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ DrawRectangle(spriteBatch, new RectangleF(x, y, width, height), color, thickness, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a line from point1 to point2 with an offset
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="x1">The X coord of the first point</param>
+ /// <param name="y1">The Y coord of the first point</param>
+ /// <param name="x2">The X coord of the second point</param>
+ /// <param name="y2">The Y coord of the second point</param>
+ /// <param name="color">The color to use</param>
+ /// <param name="thickness">The thickness of the line</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawLine(this SpriteBatch spriteBatch, float x1, float y1, float x2, float y2, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ DrawLine(spriteBatch, new Vector2(x1, y1), new Vector2(x2, y2), color, thickness, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a line from point1 to point2 with an offset
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="point1">The first point</param>
+ /// <param name="point2">The second point</param>
+ /// <param name="color">The color to use</param>
+ /// <param name="thickness">The thickness of the line</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawLine(this SpriteBatch spriteBatch, Vector2 point1, Vector2 point2, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ // calculate the distance between the two vectors
+ var distance = Vector2.Distance(point1, point2);
+
+ // calculate the angle between the two vectors
+ var angle = (float)Math.Atan2(point2.Y - point1.Y, point2.X - point1.X);
+
+ DrawLine(spriteBatch, point1, distance, angle, color, thickness, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a line from point1 to point2 with an offset
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="point">The starting point</param>
+ /// <param name="length">The length of the line</param>
+ /// <param name="angle">The angle of this line from the starting point</param>
+ /// <param name="color">The color to use</param>
+ /// <param name="thickness">The thickness of the line</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawLine(this SpriteBatch spriteBatch, Vector2 point, float length, float angle, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ var origin = new Vector2(0f, 0.5f);
+ var scale = new Vector2(length, thickness);
+ spriteBatch.Draw(GetTexture(spriteBatch), point, null, color, angle, origin, scale, SpriteEffects.None, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a point at the specified x, y position. The center of the point will be at the position.
+ /// </summary>
+ public static void DrawPoint(this SpriteBatch spriteBatch, float x, float y, Color color, float size = 1f, float layerDepth = 0)
+ {
+ DrawPoint(spriteBatch, new Vector2(x, y), color, size, layerDepth);
+ }
+
+ /// <summary>
+ /// Draws a point at the specified position. The center of the point will be at the position.
+ /// </summary>
+ public static void DrawPoint(this SpriteBatch spriteBatch, Vector2 position, Color color, float size = 1f, float layerDepth = 0)
+ {
+ var scale = Vector2.One * size;
+ var offset = new Vector2(0.5f) - new Vector2(size * 0.5f);
+ spriteBatch.Draw(GetTexture(spriteBatch), position + offset, null, color, 0f, Vector2.Zero, scale, SpriteEffects.None, layerDepth);
+ }
+
+ /// <summary>
+ /// Draw a circle from a <see cref="CircleF" /> shape
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="circle">The circle shape to draw</param>
+ /// <param name="sides">The number of sides to generate</param>
+ /// <param name="color">The color of the circle</param>
+ /// <param name="thickness">The thickness of the lines used</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawCircle(this SpriteBatch spriteBatch, CircleF circle, int sides, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ DrawCircle(spriteBatch, circle.Center, circle.Radius, sides, color, thickness, layerDepth);
+ }
+
+ /// <summary>
+ /// Draw a circle
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="center">The center of the circle</param>
+ /// <param name="radius">The radius of the circle</param>
+ /// <param name="sides">The number of sides to generate</param>
+ /// <param name="color">The color of the circle</param>
+ /// <param name="thickness">The thickness of the lines used</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawCircle(this SpriteBatch spriteBatch, Vector2 center, float radius, int sides, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ DrawPolygon(spriteBatch, center, CreateCircle(radius, sides), color, thickness, layerDepth);
+ }
+
+ /// <summary>
+ /// Draw a circle
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="x">The center X of the circle</param>
+ /// <param name="y">The center Y of the circle</param>
+ /// <param name="radius">The radius of the circle</param>
+ /// <param name="sides">The number of sides to generate</param>
+ /// <param name="color">The color of the circle</param>
+ /// <param name="thickness">The thickness of the line</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawCircle(this SpriteBatch spriteBatch, float x, float y, float radius, int sides, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ DrawPolygon(spriteBatch, new Vector2(x, y), CreateCircle(radius, sides), color, thickness, layerDepth);
+ }
+
+ /// <summary>
+ /// Draw an ellipse.
+ /// </summary>
+ /// <param name="spriteBatch">The destination drawing surface</param>
+ /// <param name="center">Center of the ellipse</param>
+ /// <param name="radius">Radius of the ellipse</param>
+ /// <param name="sides">The number of sides to generate.</param>
+ /// <param name="color">The color of the ellipse.</param>
+ /// <param name="thickness">The thickness of the line around the ellipse.</param>
+ /// <param name="layerDepth">The depth of the layer of this shape</param>
+ public static void DrawEllipse(this SpriteBatch spriteBatch, Vector2 center, Vector2 radius, int sides, Color color, float thickness = 1f, float layerDepth = 0)
+ {
+ DrawPolygon(spriteBatch, center, CreateEllipse(radius.X, radius.Y, sides), color, thickness, layerDepth);
+ }
+
+ private static Vector2[] CreateCircle(double radius, int sides)
+ {
+ const double max = 2.0 * Math.PI;
+ var points = new Vector2[sides];
+ var step = max / sides;
+ var theta = 0.0;
+
+ for (var i = 0; i < sides; i++)
+ {
+ points[i] = new Vector2((float)(radius * Math.Cos(theta)), (float)(radius * Math.Sin(theta)));
+ theta += step;
+ }
+
+ return points;
+ }
+
+ private static Vector2[] CreateEllipse(float rx, float ry, int sides)
+ {
+ var vertices = new Vector2[sides];
+
+ var t = 0.0;
+ var dt = 2.0 * Math.PI / sides;
+ for (var i = 0; i < sides; i++, t += dt)
+ {
+ var x = (float)(rx * Math.Cos(t));
+ var y = (float)(ry * Math.Sin(t));
+ vertices[i] = new Vector2(x, y);
+ }
+ return vertices;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeF.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeF.cs
new file mode 100644
index 0000000..7478f75
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeF.cs
@@ -0,0 +1,95 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// Base class for shapes.
+ /// </summary>
+ /// <remakarks>
+ /// Created to allow checking intersection between shapes of different types.
+ /// </remakarks>
+ public interface IShapeF
+ {
+ /// <summary>
+ /// Gets or sets the position of the shape.
+ /// </summary>
+ Point2 Position { get; set; }
+
+ /// <summary>
+ /// Gets escribed rectangle, which lying outside the shape
+ /// </summary>
+ RectangleF BoundingRectangle { get; }
+ }
+
+ /// <summary>
+ /// Class that implements methods for shared <see cref="IShapeF" /> methods.
+ /// </summary>
+ public static class Shape
+ {
+ /// <summary>
+ /// Check if two shapes intersect.
+ /// </summary>
+ /// <param name="a">The first shape.</param>
+ /// <param name="b">The second shape.</param>
+ /// <returns>True if the two shapes intersect.</returns>
+ public static bool Intersects(this IShapeF a, IShapeF b)
+ {
+ return a switch
+ {
+ CircleF circleA when b is CircleF circleB => circleA.Intersects(circleB),
+ CircleF circleA when b is RectangleF rectangleB => circleA.Intersects(rectangleB),
+ CircleF circleA when b is OrientedRectangle orientedRectangleB => Intersects(circleA, orientedRectangleB),
+
+ RectangleF rectangleA when b is CircleF circleB => Intersects(circleB, rectangleA),
+ RectangleF rectangleA when b is RectangleF rectangleB => rectangleA.Intersects(rectangleB),
+ RectangleF rectangleA when b is OrientedRectangle orientedRectangleB => Intersects(rectangleA, orientedRectangleB).Intersects,
+
+ OrientedRectangle orientedRectangleA when b is CircleF circleB => Intersects(circleB, orientedRectangleA),
+ OrientedRectangle orientedRectangleA when b is RectangleF rectangleB => Intersects(rectangleB, orientedRectangleA).Intersects,
+ OrientedRectangle orientedRectangleA when b is OrientedRectangle orientedRectangleB
+ => OrientedRectangle.Intersects(orientedRectangleA, orientedRectangleB).Intersects,
+
+ _ => throw new ArgumentOutOfRangeException(nameof(a))
+ };
+ }
+
+ /// <summary>
+ /// Checks if a circle and rectangle intersect.
+ /// </summary>
+ /// <param name="circle">Circle to check intersection with rectangle.</param>
+ /// <param name="rectangle">Rectangle to check intersection with circle.</param>
+ /// <returns>True if the circle and rectangle intersect.</returns>
+ public static bool Intersects(CircleF circle, RectangleF rectangle)
+ {
+ var closestPoint = rectangle.ClosestPointTo(circle.Center);
+ return circle.Contains(closestPoint);
+ }
+
+ /// <summary>
+ /// Checks whether a <see cref="CircleF"/> and <see cref="OrientedRectangle"/> intersects.
+ /// </summary>
+ /// <param name="circle"><see cref="CircleF"/>to use in intersection test.</param>
+ /// <param name="orientedRectangle"><see cref="OrientedRectangle"/>to use in intersection test.</param>
+ /// <returns>True if the circle and oriented bounded rectangle intersects, otherwise false.</returns>
+ public static bool Intersects(CircleF circle, OrientedRectangle orientedRectangle)
+ {
+ var rotation = Matrix2.CreateRotationZ(orientedRectangle.Orientation.Rotation);
+ var circleCenterInRectangleSpace = rotation.Transform(circle.Center - orientedRectangle.Center);
+ var circleInRectangleSpace = new CircleF(circleCenterInRectangleSpace, circle.Radius);
+ var boundingRectangle = new BoundingRectangle(new Point2(), orientedRectangle.Radii);
+ return circleInRectangleSpace.Intersects(boundingRectangle);
+ }
+
+ /// <summary>
+ /// Checks if a <see cref="RectangleF"/> and <see cref="OrientedRectangle"/> intersects.
+ /// </summary>
+ /// <param name="rectangleF"></param>
+ /// <param name="orientedRectangle"></param>
+ /// <returns>True if objects are intersecting, otherwise false.</returns>
+ public static (bool Intersects, Vector2 MinimumTranslationVector) Intersects(RectangleF rectangleF, OrientedRectangle orientedRectangle)
+ {
+ return OrientedRectangle.Intersects(orientedRectangle, (OrientedRectangle)rectangleF);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size.cs
new file mode 100644
index 0000000..be2122e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size.cs
@@ -0,0 +1,252 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// A two dimensional size defined by two real numbers, a width and a height.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// A size is a subspace of two-dimensional space, the area of which is described in terms of a two-dimensional
+ /// coordinate system, given by a reference point and two coordinate axes.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="IEquatable{T}" />
+ /// <seealso cref="IEquatableByRef{Size}" />
+ public struct Size : IEquatable<Size>, IEquatableByRef<Size>
+ {
+ /// <summary>
+ /// Returns a <see cref="Size" /> with <see cref="Width" /> and <see cref="Height" /> equal to <c>0.0f</c>.
+ /// </summary>
+ public static readonly Size Empty = new Size();
+
+ /// <summary>
+ /// The horizontal component of this <see cref="Size" />.
+ /// </summary>
+ public int Width;
+
+ /// <summary>
+ /// The vertical component of this <see cref="Size" />.
+ /// </summary>
+ public int Height;
+
+ /// <summary>
+ /// Gets a value that indicates whether this <see cref="Size" /> is empty.
+ /// </summary>
+ public bool IsEmpty => Width == 0 && Height == 0;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Size" /> structure from the specified dimensions.
+ /// </summary>
+ /// <param name="width">The width.</param>
+ /// <param name="height">The height.</param>
+ public Size(int width, int height)
+ {
+ Width = width;
+ Height = height;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Size" /> structures. The result specifies
+ /// whether the values of the <see cref="Width" /> and <see cref="Height" />
+ /// fields of the two <see cref="Point" /> structures are equal.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Width" /> and <see cref="Height" />
+ /// fields of the two <see cref="Point" /> structures are equal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(Size first, Size second)
+ {
+ return first.Equals(ref second);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Size" /> is equal to another <see cref="Size" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point" /> is equal to the <paramref name="size" /> parameter; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(Size size)
+ {
+ return Equals(ref size);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Size" /> is equal to another <see cref="Size" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point" /> is equal to the <paramref name="size" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(ref Size size)
+ {
+ return Width == size.Width && Height == size.Height;
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="Size" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object to make the comparison with.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Size" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ if (obj is Size)
+ return Equals((Size) obj);
+ return false;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Size" /> structures. The result specifies
+ /// whether the values of the <see cref="Width" /> or <see cref="Height" />
+ /// fields of the two <see cref="Size" /> structures are unequal.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Width" /> or <see cref="Height" />
+ /// fields of the two <see cref="Size" /> structures are unequal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(Size first, Size second)
+ {
+ return !(first == second);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size" /> representing the vector addition of two <see cref="Size" /> structures as if
+ /// they
+ /// were <see cref="Vector2" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size" /> representing the vector addition of two <see cref="Size" /> structures as if they
+ /// were <see cref="Vector2" /> structures.
+ /// </returns>
+ public static Size operator +(Size first, Size second)
+ {
+ return Add(first, second);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size" /> representing the vector addition of two <see cref="Size" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size" /> representing the vector addition of two <see cref="Size" /> structures.
+ /// </returns>
+ public static Size Add(Size first, Size second)
+ {
+ Size size;
+ size.Width = first.Width + second.Width;
+ size.Height = first.Height + second.Height;
+ return size;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size" /> representing the vector subtraction of two <see cref="Size" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size" /> representing the vector subtraction of two <see cref="Size" /> structures.
+ /// </returns>
+ public static Size operator -(Size first, Size second)
+ {
+ return Subtract(first, second);
+ }
+
+ public static Size operator /(Size size, int value)
+ {
+ return new Size(size.Width / value, size.Height / value);
+ }
+
+ public static Size operator *(Size size, int value)
+ {
+ return new Size(size.Width * value, size.Height * value);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size" /> representing the vector subtraction of two <see cref="Size" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size" /> representing the vector subtraction of two <see cref="Size" /> structures.
+ /// </returns>
+ public static Size Subtract(Size first, Size second)
+ {
+ Size size;
+ size.Width = first.Width - second.Width;
+ size.Height = first.Height - second.Height;
+ return size;
+ }
+
+ /// <summary>
+ /// Returns a hash code of this <see cref="Size" /> suitable for use in hashing algorithms and data
+ /// structures like a hash table.
+ /// </summary>
+ /// <returns>
+ /// A hash code of this <see cref="Point" />.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ // ReSharper disable NonReadonlyMemberInGetHashCode
+ return (Width.GetHashCode()*397) ^ Height.GetHashCode();
+ // ReSharper restore NonReadonlyMemberInGetHashCode
+ }
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Point" /> to a <see cref="Size" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// The resulting <see cref="Size" />.
+ /// </returns>
+ public static implicit operator Size(Point point)
+ {
+ return new Size(point.X, point.Y);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Point" /> to a <see cref="Size" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The resulting <see cref="Point" />.
+ /// </returns>
+ public static implicit operator Point(Size size)
+ {
+ return new Point(size.Width, size.Height);
+ }
+
+ public static explicit operator Size(Size2 size)
+ {
+ return new Size((int) size.Width, (int) size.Height);
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="Size" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="Size" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"Width: {Width}, Height: {Height}";
+ }
+
+ internal string DebugDisplayString => ToString();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size2.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size2.cs
new file mode 100644
index 0000000..19b22a2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size2.cs
@@ -0,0 +1,311 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// A two dimensional size defined by two real numbers, a width and a height.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// A size is a subspace of two-dimensional space, the area of which is described in terms of a two-dimensional
+ /// coordinate system, given by a reference point and two coordinate axes.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="IEquatable{T}" />
+ /// <seealso cref="IEquatableByRef{Size2}" />
+ public struct Size2 : IEquatable<Size2>, IEquatableByRef<Size2>
+ {
+ /// <summary>
+ /// Returns a <see cref="Size2" /> with <see cref="Width" /> and <see cref="Height" /> equal to <c>0.0f</c>.
+ /// </summary>
+ public static readonly Size2 Empty = new Size2();
+
+ /// <summary>
+ /// The horizontal component of this <see cref="Size2" />.
+ /// </summary>
+ public float Width;
+
+ /// <summary>
+ /// The vertical component of this <see cref="Size2" />.
+ /// </summary>
+ public float Height;
+
+ /// <summary>
+ /// Gets a value that indicates whether this <see cref="Size2" /> is empty.
+ /// </summary>
+ // ReSharper disable CompareOfFloatsByEqualityOperator
+ public bool IsEmpty => (Width == 0) && (Height == 0);
+
+ // ReSharper restore CompareOfFloatsByEqualityOperator
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Size2" /> structure from the specified dimensions.
+ /// </summary>
+ /// <param name="width">The width.</param>
+ /// <param name="height">The height.</param>
+ public Size2(float width, float height)
+ {
+ Width = width;
+ Height = height;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Size2" /> structures. The result specifies
+ /// whether the values of the <see cref="Width" /> and <see cref="Height" />
+ /// fields of the two <see cref="Point2" /> structures are equal.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Width" /> and <see cref="Height" />
+ /// fields of the two <see cref="Point2" /> structures are equal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(Size2 first, Size2 second)
+ {
+ return first.Equals(ref second);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Size2" /> is equal to another <see cref="Size2" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point2" /> is equal to the <paramref name="size" /> parameter; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(Size2 size)
+ {
+ return Equals(ref size);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Size2" /> is equal to another <see cref="Size2" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point2" /> is equal to the <paramref name="size" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(ref Size2 size)
+ {
+ // ReSharper disable CompareOfFloatsByEqualityOperator
+ return (Width == size.Width) && (Height == size.Height);
+ // ReSharper restore CompareOfFloatsByEqualityOperator
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="Size2" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object to make the comparison with.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Size2" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ if (obj is Size2)
+ return Equals((Size2) obj);
+ return false;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Size2" /> structures. The result specifies
+ /// whether the values of the <see cref="Width" /> or <see cref="Height" />
+ /// fields of the two <see cref="Size2" /> structures are unequal.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Width" /> or <see cref="Height" />
+ /// fields of the two <see cref="Size2" /> structures are unequal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(Size2 first, Size2 second)
+ {
+ return !(first == second);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size2" /> representing the vector addition of two <see cref="Size2" /> structures as if
+ /// they
+ /// were <see cref="Vector2" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size2" /> representing the vector addition of two <see cref="Size2" /> structures as if they
+ /// were <see cref="Vector2" /> structures.
+ /// </returns>
+ public static Size2 operator +(Size2 first, Size2 second)
+ {
+ return Add(first, second);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size2" /> representing the vector addition of two <see cref="Size2" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size2" /> representing the vector addition of two <see cref="Size2" /> structures.
+ /// </returns>
+ public static Size2 Add(Size2 first, Size2 second)
+ {
+ Size2 size;
+ size.Width = first.Width + second.Width;
+ size.Height = first.Height + second.Height;
+ return size;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size2" /> representing the vector subtraction of two <see cref="Size2" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size2" /> representing the vector subtraction of two <see cref="Size2" /> structures.
+ /// </returns>
+ public static Size2 operator -(Size2 first, Size2 second)
+ {
+ return Subtract(first, second);
+ }
+
+ public static Size2 operator /(Size2 size, float value)
+ {
+ return new Size2(size.Width / value, size.Height / value);
+ }
+
+ public static Size2 operator *(Size2 size, float value)
+ {
+ return new Size2(size.Width * value, size.Height * value);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size2" /> representing the vector subtraction of two <see cref="Size2" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size2" /> representing the vector subtraction of two <see cref="Size2" /> structures.
+ /// </returns>
+ public static Size2 Subtract(Size2 first, Size2 second)
+ {
+ Size2 size;
+ size.Width = first.Width - second.Width;
+ size.Height = first.Height - second.Height;
+ return size;
+ }
+
+ /// <summary>
+ /// Returns a hash code of this <see cref="Size2" /> suitable for use in hashing algorithms and data
+ /// structures like a hash table.
+ /// </summary>
+ /// <returns>
+ /// A hash code of this <see cref="Point2" />.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (Width.GetHashCode()*397) ^ Height.GetHashCode();
+ }
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Point2" /> to a <see cref="Size2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// The resulting <see cref="Size2" />.
+ /// </returns>
+ public static implicit operator Size2(Point2 point)
+ {
+ return new Size2(point.X, point.Y);
+ }
+
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Point" /> to a <see cref="Size2" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// The resulting <see cref="Size2" />.
+ /// </returns>
+ public static implicit operator Size2(Point point)
+ {
+ return new Size2(point.X, point.Y);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Point2" /> to a <see cref="Size2" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The resulting <see cref="Point2" />.
+ /// </returns>
+ public static implicit operator Point2(Size2 size)
+ {
+ return new Point2(size.Width, size.Height);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Size2" /> to a <see cref="Vector2" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The resulting <see cref="Vector2" />.
+ /// </returns>
+ public static implicit operator Vector2(Size2 size)
+ {
+ return new Vector2(size.Width, size.Height);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Vector2" /> to a <see cref="Size2" />.
+ /// </summary>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The resulting <see cref="Size2" />.
+ /// </returns>
+ public static implicit operator Size2(Vector2 vector)
+ {
+ return new Size2(vector.X, vector.Y);
+ }
+
+ ///// <summary>
+ ///// Performs an implicit conversion from a <see cref="Size" /> to a <see cref="Size2" />.
+ ///// </summary>
+ ///// <param name="size">The size.</param>
+ ///// <returns>
+ ///// The resulting <see cref="Size2" />.
+ ///// </returns>
+ //public static implicit operator Size2(Size size)
+ //{
+ // return new Size2(size.Width, size.Height);
+ //}
+
+ /// <summary>
+ /// Performs an explicit conversion from a <see cref="Size2" /> to a <see cref="Point" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The resulting <see cref="Size2" />.
+ /// </returns>
+ public static explicit operator Point(Size2 size)
+ {
+ return new Point((int)size.Width, (int)size.Height);
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="Size2" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="Size2" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"Width: {Width}, Height: {Height}";
+ }
+
+ internal string DebugDisplayString => ToString();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size3.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size3.cs
new file mode 100644
index 0000000..9872aee
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size3.cs
@@ -0,0 +1,289 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ /// <summary>
+ /// A three dimensional size defined by two real numbers, a width a height and a depth.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// A size is a subspace of three-dimensional space, the area of which is described in terms of a three-dimensional
+ /// coordinate system, given by a reference point and three coordinate axes.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="IEquatable{T}" />
+ /// <seealso cref="IEquatableByRef{Size3}" />
+ public struct Size3 : IEquatable<Size3>, IEquatableByRef<Size3>
+ {
+ /// <summary>
+ /// Returns a <see cref="Size3" /> with <see cref="Width" /> <see cref="Height" /> and <see cref="Depth" /> equal to <c>0.0f</c>.
+ /// </summary>
+ public static readonly Size3 Empty = new Size3();
+
+ /// <summary>
+ /// The horizontal component of this <see cref="Size3" />.
+ /// </summary>
+ public float Width;
+
+ /// <summary>
+ /// The vertical component of this <see cref="Size3" />.
+ /// </summary>
+ public float Height;
+
+ /// <summary>
+ /// The vertical component of this <see cref="Size3" />.
+ /// </summary>
+ public float Depth;
+
+ /// <summary>
+ /// Gets a value that indicates whether this <see cref="Size3" /> is empty.
+ /// </summary>
+ // ReSharper disable CompareOfFloatsByEqualityOperator
+ public bool IsEmpty => (Width == 0) && (Height == 0) && (Depth == 0);
+
+ // ReSharper restore CompareOfFloatsByEqualityOperator
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Size3" /> structure from the specified dimensions.
+ /// </summary>
+ /// <param name="width">The width.</param>
+ /// <param name="height">The height.</param>
+ /// <param name="depth">The depth.</param>
+ public Size3(float width, float height, float depth)
+ {
+ Width = width;
+ Height = height;
+ Depth = depth;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Size3" /> structures. The result specifies
+ /// whether the values of the <see cref="Width" /> <see cref="Height" /> and <see cref="Depth" />
+ /// fields of the two <see cref="Point3" /> structures are equal.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Width" /> <see cref="Height" /> and <see cref="Depth" />
+ /// fields of the two <see cref="Point3" /> structures are equal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator ==(Size3 first, Size3 second)
+ {
+ return first.Equals(ref second);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Size3" /> is equal to another <see cref="Size3" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point3" /> is equal to the <paramref name="size" /> parameter; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(Size3 size)
+ {
+ return Equals(ref size);
+ }
+
+ /// <summary>
+ /// Indicates whether this <see cref="Size3" /> is equal to another <see cref="Size3" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Point3" /> is equal to the <paramref name="size" />; otherwise,
+ /// <c>false</c>.
+ /// </returns>
+ public bool Equals(ref Size3 size)
+ {
+ // ReSharper disable CompareOfFloatsByEqualityOperator
+ return (Width == size.Width) && (Height == size.Height) && (Depth == size.Depth);
+ // ReSharper restore CompareOfFloatsByEqualityOperator
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="Size3" /> is equal to a specified object.
+ /// </summary>
+ /// <param name="obj">The object to make the comparison with.</param>
+ /// <returns>
+ /// <c>true</c> if this <see cref="Size3" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ if (obj is Size3)
+ return Equals((Size3) obj);
+ return false;
+ }
+
+ /// <summary>
+ /// Compares two <see cref="Size3" /> structures. The result specifies
+ /// whether the values of the <see cref="Width" /> <see cref="Height" /> or <see cref="Depth" />
+ /// fields of the two <see cref="Size3" /> structures are unequal.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// <c>true</c> if the <see cref="Width" /> <see cref="Height" /> or <see cref="Depth" />
+ /// fields of the two <see cref="Size3" /> structures are unequal; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool operator !=(Size3 first, Size3 second)
+ {
+ return !(first == second);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size3" /> representing the vector addition of two <see cref="Size3" /> structures as if
+ /// they were <see cref="Vector3" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size3" /> representing the vector addition of two <see cref="Size3" /> structures as if they
+ /// were <see cref="Vector3" /> structures.
+ /// </returns>
+ public static Size3 operator +(Size3 first, Size3 second)
+ {
+ return Add(first, second);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size3" /> representing the vector addition of two <see cref="Size3" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size3" /> representing the vector addition of two <see cref="Size3" /> structures.
+ /// </returns>
+ public static Size3 Add(Size3 first, Size3 second)
+ {
+ Size3 size;
+ size.Width = first.Width + second.Width;
+ size.Height = first.Height + second.Height;
+ size.Depth = first.Depth + second.Depth;
+ return size;
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size3" /> representing the vector subtraction of two <see cref="Size3" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size3" /> representing the vector subtraction of two <see cref="Size3" /> structures.
+ /// </returns>
+ public static Size3 operator -(Size3 first, Size3 second)
+ {
+ return Subtract(first, second);
+ }
+
+ public static Size3 operator /(Size3 size, float value)
+ {
+ return new Size3(size.Width / value, size.Height / value, size.Depth / value);
+ }
+
+ public static Size3 operator *(Size3 size, float value)
+ {
+ return new Size3(size.Width * value, size.Height * value, size.Depth * value);
+ }
+
+ /// <summary>
+ /// Calculates the <see cref="Size3" /> representing the vector subtraction of two <see cref="Size3" /> structures.
+ /// </summary>
+ /// <param name="first">The first size.</param>
+ /// <param name="second">The second size.</param>
+ /// <returns>
+ /// The <see cref="Size3" /> representing the vector subtraction of two <see cref="Size3" /> structures.
+ /// </returns>
+ public static Size3 Subtract(Size3 first, Size3 second)
+ {
+ Size3 size;
+ size.Width = first.Width - second.Width;
+ size.Height = first.Height - second.Height;
+ size.Depth = first.Depth - second.Depth;
+ return size;
+ }
+
+ /// <summary>
+ /// Returns a hash code of this <see cref="Size3" /> suitable for use in hashing algorithms and data
+ /// structures like a hash table.
+ /// </summary>
+ /// <returns>
+ /// A hash code of this <see cref="Point3" />.
+ /// </returns>
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ unchecked
+ {
+ int hash = 17;
+ hash = hash * 23 + Width.GetHashCode();
+ hash = hash * 23 + Height.GetHashCode();
+ hash = hash * 23 + Depth.GetHashCode();
+ return hash;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Point3" /> to a <see cref="Size3" />.
+ /// </summary>
+ /// <param name="point">The point.</param>
+ /// <returns>
+ /// The resulting <see cref="Size3" />.
+ /// </returns>
+ public static implicit operator Size3(Point3 point)
+ {
+ return new Size3(point.X, point.Y, point.Z);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Point3" /> to a <see cref="Size3" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The resulting <see cref="Point3" />.
+ /// </returns>
+ public static implicit operator Point3(Size3 size)
+ {
+ return new Point3(size.Width, size.Height, size.Depth);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Size3" /> to a <see cref="Vector3" />.
+ /// </summary>
+ /// <param name="size">The size.</param>
+ /// <returns>
+ /// The resulting <see cref="Vector3" />.
+ /// </returns>
+ public static implicit operator Vector3(Size3 size)
+ {
+ return new Vector3(size.Width, size.Height, size.Depth);
+ }
+
+ /// <summary>
+ /// Performs an implicit conversion from a <see cref="Vector3" /> to a <see cref="Size3" />.
+ /// </summary>
+ /// <param name="vector">The vector.</param>
+ /// <returns>
+ /// The resulting <see cref="Size3" />.
+ /// </returns>
+ public static implicit operator Size3(Vector3 vector)
+ {
+ return new Size3(vector.X, vector.Y, vector.Z);
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this <see cref="Size3" />.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="string" /> that represents this <see cref="Size3" />.
+ /// </returns>
+ public override string ToString()
+ {
+ return $"Width: {Width}, Height: {Height}, Depth: {Depth}";
+ }
+
+ internal string DebugDisplayString => ToString();
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Thickness.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Thickness.cs
new file mode 100644
index 0000000..dfecbc9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Thickness.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Linq;
+
+namespace MonoGame.Extended
+{
+ public struct Thickness : IEquatable<Thickness>
+ {
+ public Thickness(int all)
+ : this(all, all, all, all)
+ {
+ }
+
+ public Thickness(int leftRight, int topBottom)
+ : this(leftRight, topBottom, leftRight, topBottom)
+ {
+ }
+
+ public Thickness(int left, int top, int right, int bottom)
+ : this()
+ {
+ Left = left;
+ Top = top;
+ Right = right;
+ Bottom = bottom;
+ }
+
+ public int Left { get; set; }
+ public int Top { get; set; }
+ public int Right { get; set; }
+ public int Bottom { get; set; }
+ public int Width => Left + Right;
+ public int Height => Top + Bottom;
+ public Size Size => new Size(Width, Height);
+
+ public static implicit operator Thickness(int value)
+ {
+ return new Thickness(value);
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj is Thickness)
+ {
+ var other = (Thickness)obj;
+ return Equals(other);
+ }
+
+ return base.Equals(obj);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = Left;
+ hashCode = (hashCode * 397) ^ Top;
+ hashCode = (hashCode * 397) ^ Right;
+ hashCode = (hashCode * 397) ^ Bottom;
+ return hashCode;
+ }
+ }
+
+ public bool Equals(Thickness other)
+ {
+ return Left == other.Left && Right == other.Right && Top == other.Top && Bottom == other.Bottom;
+ }
+
+ public static Thickness FromValues(int[] values)
+ {
+ switch (values.Length)
+ {
+ case 1:
+ return new Thickness(values[0]);
+ case 2:
+ return new Thickness(values[0], values[1]);
+ case 4:
+ return new Thickness(values[0], values[1], values[2], values[3]);
+ default:
+ throw new FormatException("Invalid thickness");
+ }
+ }
+
+ public static Thickness Parse(string value)
+ {
+ var values = value
+ .Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(int.Parse)
+ .ToArray();
+
+ return FromValues(values);
+ }
+
+ public override string ToString()
+ {
+ if (Left == Right && Top == Bottom)
+ return Left == Top ? $"{Left}" : $"{Left} {Top}";
+
+ return $"{Left}, {Right}, {Top}, {Bottom}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/CyclicalList.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/CyclicalList.cs
new file mode 100644
index 0000000..551e510
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/CyclicalList.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MonoGame.Extended.Triangulation
+{
+ /// <summary>
+ /// Implements a List structure as a cyclical list where indices are wrapped.
+ /// </summary>
+ /// MIT Licensed: https://github.com/nickgravelyn/Triangulator
+ /// <typeparam name="T">The Type to hold in the list.</typeparam>
+ class CyclicalList<T> : List<T>
+ {
+ public new T this[int index]
+ {
+ get
+ {
+ //perform the index wrapping
+ while (index < 0)
+ index = Count + index;
+ if (index >= Count)
+ index %= Count;
+
+ return base[index];
+ }
+ set
+ {
+ //perform the index wrapping
+ while (index < 0)
+ index = Count + index;
+ if (index >= Count)
+ index %= Count;
+
+ base[index] = value;
+ }
+ }
+
+ public CyclicalList() { }
+
+ public CyclicalList(IEnumerable<T> collection)
+ : base(collection)
+ {
+ }
+
+ public new void RemoveAt(int index)
+ {
+ Remove(this[index]);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/IndexableCyclicalLinkedList.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/IndexableCyclicalLinkedList.cs
new file mode 100644
index 0000000..d84ee82
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/IndexableCyclicalLinkedList.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MonoGame.Extended.Triangulation
+{
+ /// <summary>
+ /// Implements a LinkedList that is both indexable as well as cyclical. Thus
+ /// indexing into the list with an out-of-bounds index will automatically cycle
+ /// around the list to find a valid node.
+ /// </summary>
+ /// MIT Licensed: https://github.com/nickgravelyn/Triangulator
+ class IndexableCyclicalLinkedList<T> : LinkedList<T>
+ {
+ /// <summary>
+ /// Gets the LinkedListNode at a particular index.
+ /// </summary>
+ /// <param name="index">The index of the node to retrieve.</param>
+ /// <returns>The LinkedListNode found at the index given.</returns>
+ public LinkedListNode<T> this[int index]
+ {
+ get
+ {
+ //perform the index wrapping
+ while (index < 0)
+ index = Count + index;
+ if (index >= Count)
+ index %= Count;
+
+ //find the proper node
+ LinkedListNode<T> node = First;
+ for (int i = 0; i < index; i++)
+ node = node.Next;
+
+ return node;
+ }
+ }
+
+ /// <summary>
+ /// Removes the node at a given index.
+ /// </summary>
+ /// <param name="index">The index of the node to remove.</param>
+ public void RemoveAt(int index)
+ {
+ Remove(this[index]);
+ }
+
+ /// <summary>
+ /// Finds the index of a given item.
+ /// </summary>
+ /// <param name="item">The item to find.</param>
+ /// <returns>The index of the item if found; -1 if the item is not found.</returns>
+ public int IndexOf(T item)
+ {
+ for (int i = 0; i < Count; i++)
+ if (this[i].Value.Equals(item))
+ return i;
+
+ return -1;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/LineSegment.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/LineSegment.cs
new file mode 100644
index 0000000..bbe0c04
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/LineSegment.cs
@@ -0,0 +1,61 @@
+using Microsoft.Xna.Framework;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MonoGame.Extended.Triangulation
+{
+ /// <summary>
+ /// MIT Licensed: https://github.com/nickgravelyn/Triangulator
+ /// </summary>
+ struct LineSegment
+ {
+ public Vertex A;
+ public Vertex B;
+
+ public LineSegment(Vertex a, Vertex b)
+ {
+ A = a;
+ B = b;
+ }
+
+ public float? IntersectsWithRay(Vector2 origin, Vector2 direction)
+ {
+ float largestDistance = MathHelper.Max(A.Position.X - origin.X, B.Position.X - origin.X) * 2f;
+ LineSegment raySegment = new LineSegment(new Vertex(origin, 0), new Vertex(origin + (direction * largestDistance), 0));
+
+ Vector2? intersection = FindIntersection(this, raySegment);
+ float? value = null;
+
+ if (intersection != null)
+ value = Vector2.Distance(origin, intersection.Value);
+
+ return value;
+ }
+
+ public static Vector2? FindIntersection(LineSegment a, LineSegment b)
+ {
+ float x1 = a.A.Position.X;
+ float y1 = a.A.Position.Y;
+ float x2 = a.B.Position.X;
+ float y2 = a.B.Position.Y;
+ float x3 = b.A.Position.X;
+ float y3 = b.A.Position.Y;
+ float x4 = b.B.Position.X;
+ float y4 = b.B.Position.Y;
+
+ float denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
+
+ float uaNum = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3);
+ float ubNum = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3);
+
+ float ua = uaNum / denom;
+ float ub = ubNum / denom;
+
+ if (MathHelper.Clamp(ua, 0f, 1f) != ua || MathHelper.Clamp(ub, 0f, 1f) != ub)
+ return null;
+
+ return a.A.Position + (a.B.Position - a.A.Position) * ua;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangle.cs
new file mode 100644
index 0000000..8c1d35c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangle.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MonoGame.Extended.Triangulation
+{
+ /// <summary>
+ /// A basic triangle structure that holds the three vertices that make up a given triangle.
+ /// MIT Licensed: https://github.com/nickgravelyn/Triangulator
+ /// </summary>
+ struct Triangle
+ {
+ public readonly Vertex A;
+ public readonly Vertex B;
+ public readonly Vertex C;
+
+ public Triangle(Vertex a, Vertex b, Vertex c)
+ {
+ A = a;
+ B = b;
+ C = c;
+ }
+
+ public bool ContainsPoint(Vertex point)
+ {
+ //return true if the point to test is one of the vertices
+ if (point.Equals(A) || point.Equals(B) || point.Equals(C))
+ return true;
+
+ bool oddNodes = false;
+
+ if (checkPointToSegment(C, A, point))
+ oddNodes = !oddNodes;
+ if (checkPointToSegment(A, B, point))
+ oddNodes = !oddNodes;
+ if (checkPointToSegment(B, C, point))
+ oddNodes = !oddNodes;
+
+ return oddNodes;
+ }
+
+ public static bool ContainsPoint(Vertex a, Vertex b, Vertex c, Vertex point)
+ {
+ return new Triangle(a, b, c).ContainsPoint(point);
+ }
+
+ static bool checkPointToSegment(Vertex sA, Vertex sB, Vertex point)
+ {
+ if ((sA.Position.Y < point.Position.Y && sB.Position.Y >= point.Position.Y) ||
+ (sB.Position.Y < point.Position.Y && sA.Position.Y >= point.Position.Y))
+ {
+ float x =
+ sA.Position.X +
+ (point.Position.Y - sA.Position.Y) /
+ (sB.Position.Y - sA.Position.Y) *
+ (sB.Position.X - sA.Position.X);
+
+ if (x < point.Position.X)
+ return true;
+ }
+
+ return false;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj.GetType() != typeof(Triangle))
+ return false;
+ return Equals((Triangle)obj);
+ }
+
+ public bool Equals(Triangle obj)
+ {
+ return obj.A.Equals(A) && obj.B.Equals(B) && obj.C.Equals(C);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int result = A.GetHashCode();
+ result = (result * 397) ^ B.GetHashCode();
+ result = (result * 397) ^ C.GetHashCode();
+ return result;
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangulator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangulator.cs
new file mode 100644
index 0000000..5e03089
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangulator.cs
@@ -0,0 +1,567 @@
+using Microsoft.Xna.Framework;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+
+namespace MonoGame.Extended.Triangulation
+{
+ /// <summary>
+ /// MIT Licensed: https://github.com/nickgravelyn/Triangulator
+ ///
+ /// A static class exposing methods for triangulating 2D polygons. This is the sole public
+ /// class in the entire library; all other classes/structures are intended as internal-only
+ /// objects used only to assist in triangulation.
+ ///
+ /// This class makes use of the DEBUG conditional and produces quite verbose output when built
+ /// in Debug mode. This is quite useful for debugging purposes, but can slow the process down
+ /// quite a bit. For optimal performance, build the library in Release mode.
+ ///
+ /// The triangulation is also not optimized for garbage sensitive processing. The point of the
+ /// library is a robust, yet simple, system for triangulating 2D shapes. It is intended to be
+ /// used as part of your content pipeline or at load-time. It is not something you want to be
+ /// using each and every frame unless you really don't care about garbage.
+ /// </summary>
+ public static class Triangulator
+ {
+ #region Fields
+
+ static readonly IndexableCyclicalLinkedList<Vertex> polygonVertices = new IndexableCyclicalLinkedList<Vertex>();
+ static readonly IndexableCyclicalLinkedList<Vertex> earVertices = new IndexableCyclicalLinkedList<Vertex>();
+ static readonly CyclicalList<Vertex> convexVertices = new CyclicalList<Vertex>();
+ static readonly CyclicalList<Vertex> reflexVertices = new CyclicalList<Vertex>();
+
+ #endregion
+
+ #region Public Methods
+
+ #region Triangulate
+
+ /// <summary>
+ /// Triangulates a 2D polygon produced the indexes required to render the points as a triangle list.
+ /// </summary>
+ /// <param name="inputVertices">The polygon vertices in counter-clockwise winding order.</param>
+ /// <param name="desiredWindingOrder">The desired output winding order.</param>
+ /// <param name="outputVertices">The resulting vertices that include any reversals of winding order and holes.</param>
+ /// <param name="indices">The resulting indices for rendering the shape as a triangle list.</param>
+ public static void Triangulate(
+ Vector2[] inputVertices,
+ WindingOrder desiredWindingOrder,
+ out Vector2[] outputVertices,
+ out int[] indices)
+ {
+ Log("\nBeginning triangulation...");
+
+ List<Triangle> triangles = new List<Triangle>();
+
+ //make sure we have our vertices wound properly
+ if (DetermineWindingOrder(inputVertices) == WindingOrder.Clockwise)
+ outputVertices = ReverseWindingOrder(inputVertices);
+ else
+ outputVertices = (Vector2[])inputVertices.Clone();
+
+ //clear all of the lists
+ polygonVertices.Clear();
+ earVertices.Clear();
+ convexVertices.Clear();
+ reflexVertices.Clear();
+
+ //generate the cyclical list of vertices in the polygon
+ for (int i = 0; i < outputVertices.Length; i++)
+ polygonVertices.AddLast(new Vertex(outputVertices[i], i));
+
+ //categorize all of the vertices as convex, reflex, and ear
+ FindConvexAndReflexVertices();
+ FindEarVertices();
+
+ //clip all the ear vertices
+ while (polygonVertices.Count > 3 && earVertices.Count > 0)
+ ClipNextEar(triangles);
+
+ //if there are still three points, use that for the last triangle
+ if (polygonVertices.Count == 3)
+ triangles.Add(new Triangle(
+ polygonVertices[0].Value,
+ polygonVertices[1].Value,
+ polygonVertices[2].Value));
+
+ //add all of the triangle indices to the output array
+ indices = new int[triangles.Count * 3];
+
+ //move the if statement out of the loop to prevent all the
+ //redundant comparisons
+ if (desiredWindingOrder == WindingOrder.CounterClockwise)
+ {
+ for (int i = 0; i < triangles.Count; i++)
+ {
+ indices[(i * 3)] = triangles[i].A.Index;
+ indices[(i * 3) + 1] = triangles[i].B.Index;
+ indices[(i * 3) + 2] = triangles[i].C.Index;
+ }
+ }
+ else
+ {
+ for (int i = 0; i < triangles.Count; i++)
+ {
+ indices[(i * 3)] = triangles[i].C.Index;
+ indices[(i * 3) + 1] = triangles[i].B.Index;
+ indices[(i * 3) + 2] = triangles[i].A.Index;
+ }
+ }
+ }
+
+ #endregion
+
+ #region CutHoleInShape
+
+ /// <summary>
+ /// Cuts a hole into a shape.
+ /// </summary>
+ /// <param name="shapeVerts">An array of vertices for the primary shape.</param>
+ /// <param name="holeVerts">An array of vertices for the hole to be cut. It is assumed that these vertices lie completely within the shape verts.</param>
+ /// <returns>The new array of vertices that can be passed to Triangulate to properly triangulate the shape with the hole.</returns>
+ public static Vector2[] CutHoleInShape(Vector2[] shapeVerts, Vector2[] holeVerts)
+ {
+ Log("\nCutting hole into shape...");
+
+ //make sure the shape vertices are wound counter clockwise and the hole vertices clockwise
+ shapeVerts = EnsureWindingOrder(shapeVerts, WindingOrder.CounterClockwise);
+ holeVerts = EnsureWindingOrder(holeVerts, WindingOrder.Clockwise);
+
+ //clear all of the lists
+ polygonVertices.Clear();
+ earVertices.Clear();
+ convexVertices.Clear();
+ reflexVertices.Clear();
+
+ //generate the cyclical list of vertices in the polygon
+ for (int i = 0; i < shapeVerts.Length; i++)
+ polygonVertices.AddLast(new Vertex(shapeVerts[i], i));
+
+ CyclicalList<Vertex> holePolygon = new CyclicalList<Vertex>();
+ for (int i = 0; i < holeVerts.Length; i++)
+ holePolygon.Add(new Vertex(holeVerts[i], i + polygonVertices.Count));
+
+#if DEBUG
+ StringBuilder vString = new StringBuilder();
+ foreach (Vertex v in polygonVertices)
+ vString.Append(string.Format("{0}, ", v));
+ Log("Shape Vertices: {0}", vString);
+
+ vString = new StringBuilder();
+ foreach (Vertex v in holePolygon)
+ vString.Append(string.Format("{0}, ", v));
+ Log("Hole Vertices: {0}", vString);
+#endif
+
+ FindConvexAndReflexVertices();
+ FindEarVertices();
+
+ //find the hole vertex with the largest X value
+ Vertex rightMostHoleVertex = holePolygon[0];
+ foreach (Vertex v in holePolygon)
+ if (v.Position.X > rightMostHoleVertex.Position.X)
+ rightMostHoleVertex = v;
+
+ //construct a list of all line segments where at least one vertex
+ //is to the right of the rightmost hole vertex with one vertex
+ //above the hole vertex and one below
+ List<LineSegment> segmentsToTest = new List<LineSegment>();
+ for (int i = 0; i < polygonVertices.Count; i++)
+ {
+ Vertex a = polygonVertices[i].Value;
+ Vertex b = polygonVertices[i + 1].Value;
+
+ if ((a.Position.X > rightMostHoleVertex.Position.X || b.Position.X > rightMostHoleVertex.Position.X) &&
+ ((a.Position.Y >= rightMostHoleVertex.Position.Y && b.Position.Y <= rightMostHoleVertex.Position.Y) ||
+ (a.Position.Y <= rightMostHoleVertex.Position.Y && b.Position.Y >= rightMostHoleVertex.Position.Y)))
+ segmentsToTest.Add(new LineSegment(a, b));
+ }
+
+ //now we try to find the closest intersection point heading to the right from
+ //our hole vertex.
+ float? closestPoint = null;
+ LineSegment closestSegment = new LineSegment();
+ foreach (LineSegment segment in segmentsToTest)
+ {
+ float? intersection = segment.IntersectsWithRay(rightMostHoleVertex.Position, Vector2.UnitX);
+ if (intersection != null)
+ {
+ if (closestPoint == null || closestPoint.Value > intersection.Value)
+ {
+ closestPoint = intersection;
+ closestSegment = segment;
+ }
+ }
+ }
+
+ //if closestPoint is null, there were no collisions (likely from improper input data),
+ //but we'll just return without doing anything else
+ if (closestPoint == null)
+ return shapeVerts;
+
+ //otherwise we can find our mutually visible vertex to split the polygon
+ Vector2 I = rightMostHoleVertex.Position + Vector2.UnitX * closestPoint.Value;
+ Vertex P = (closestSegment.A.Position.X > closestSegment.B.Position.X)
+ ? closestSegment.A
+ : closestSegment.B;
+
+ //construct triangle MIP
+ Triangle mip = new Triangle(rightMostHoleVertex, new Vertex(I, 1), P);
+
+ //see if any of the reflex vertices lie inside of the MIP triangle
+ List<Vertex> interiorReflexVertices = new List<Vertex>();
+ foreach (Vertex v in reflexVertices)
+ if (mip.ContainsPoint(v))
+ interiorReflexVertices.Add(v);
+
+ //if there are any interior reflex vertices, find the one that, when connected
+ //to our rightMostHoleVertex, forms the line closest to Vector2.UnitX
+ if (interiorReflexVertices.Count > 0)
+ {
+ float closestDot = -1f;
+ foreach (Vertex v in interiorReflexVertices)
+ {
+ //compute the dot product of the vector against the UnitX
+ Vector2 d = Vector2.Normalize(v.Position - rightMostHoleVertex.Position);
+ float dot = Vector2.Dot(Vector2.UnitX, d);
+
+ //if this line is the closest we've found
+ if (dot > closestDot)
+ {
+ //save the value and save the vertex as P
+ closestDot = dot;
+ P = v;
+ }
+ }
+ }
+
+ //now we just form our output array by injecting the hole vertices into place
+ //we know we have to inject the hole into the main array after point P going from
+ //rightMostHoleVertex around and then back to P.
+ int mIndex = holePolygon.IndexOf(rightMostHoleVertex);
+ int injectPoint = polygonVertices.IndexOf(P);
+
+ Log("Inserting hole at injection point {0} starting at hole vertex {1}.",
+ P,
+ rightMostHoleVertex);
+ for (int i = mIndex; i <= mIndex + holePolygon.Count; i++)
+ {
+ Log("Inserting vertex {0} after vertex {1}.", holePolygon[i], polygonVertices[injectPoint].Value);
+ polygonVertices.AddAfter(polygonVertices[injectPoint++], holePolygon[i]);
+ }
+ polygonVertices.AddAfter(polygonVertices[injectPoint], P);
+
+#if DEBUG
+ vString = new StringBuilder();
+ foreach (Vertex v in polygonVertices)
+ vString.Append(string.Format("{0}, ", v));
+ Log("New Shape Vertices: {0}\n", vString);
+#endif
+
+ //finally we write out the new polygon vertices and return them out
+ Vector2[] newShapeVerts = new Vector2[polygonVertices.Count];
+ for (int i = 0; i < polygonVertices.Count; i++)
+ newShapeVerts[i] = polygonVertices[i].Value.Position;
+
+ return newShapeVerts;
+ }
+
+ #endregion
+
+ #region EnsureWindingOrder
+
+ /// <summary>
+ /// Ensures that a set of vertices are wound in a particular order, reversing them if necessary.
+ /// </summary>
+ /// <param name="vertices">The vertices of the polygon.</param>
+ /// <param name="windingOrder">The desired winding order.</param>
+ /// <returns>A new set of vertices if the winding order didn't match; otherwise the original set.</returns>
+ public static Vector2[] EnsureWindingOrder(Vector2[] vertices, WindingOrder windingOrder)
+ {
+ Log("\nEnsuring winding order of {0}...", windingOrder);
+ if (DetermineWindingOrder(vertices) != windingOrder)
+ {
+ Log("Reversing vertices...");
+ return ReverseWindingOrder(vertices);
+ }
+
+ Log("No reversal needed.");
+ return vertices;
+ }
+
+ #endregion
+
+ #region ReverseWindingOrder
+
+ /// <summary>
+ /// Reverses the winding order for a set of vertices.
+ /// </summary>
+ /// <param name="vertices">The vertices of the polygon.</param>
+ /// <returns>The new vertices for the polygon with the opposite winding order.</returns>
+ public static Vector2[] ReverseWindingOrder(Vector2[] vertices)
+ {
+ Log("\nReversing winding order...");
+ Vector2[] newVerts = new Vector2[vertices.Length];
+
+#if DEBUG
+ StringBuilder vString = new StringBuilder();
+ foreach (Vector2 v in vertices)
+ vString.Append(string.Format("{0}, ", v));
+ Log("Original Vertices: {0}", vString);
+#endif
+
+ newVerts[0] = vertices[0];
+ for (int i = 1; i < newVerts.Length; i++)
+ newVerts[i] = vertices[vertices.Length - i];
+
+#if DEBUG
+ vString = new StringBuilder();
+ foreach (Vector2 v in newVerts)
+ vString.Append(string.Format("{0}, ", v));
+ Log("New Vertices After Reversal: {0}\n", vString);
+#endif
+
+ return newVerts;
+ }
+
+ #endregion
+
+ #region DetermineWindingOrder
+
+ /// <summary>
+ /// Determines the winding order of a polygon given a set of vertices.
+ /// </summary>
+ /// <param name="vertices">The vertices of the polygon.</param>
+ /// <returns>The calculated winding order of the polygon.</returns>
+ public static WindingOrder DetermineWindingOrder(Vector2[] vertices)
+ {
+ int clockWiseCount = 0;
+ int counterClockWiseCount = 0;
+ Vector2 p1 = vertices[0];
+
+ for (int i = 1; i < vertices.Length; i++)
+ {
+ Vector2 p2 = vertices[i];
+ Vector2 p3 = vertices[(i + 1) % vertices.Length];
+
+ Vector2 e1 = p1 - p2;
+ Vector2 e2 = p3 - p2;
+
+ if (e1.X * e2.Y - e1.Y * e2.X >= 0)
+ clockWiseCount++;
+ else
+ counterClockWiseCount++;
+
+ p1 = p2;
+ }
+
+ return (clockWiseCount > counterClockWiseCount)
+ ? WindingOrder.Clockwise
+ : WindingOrder.CounterClockwise;
+ }
+
+ #endregion
+
+ #endregion
+
+ #region Private Methods
+
+ #region ClipNextEar
+
+ private static void ClipNextEar(ICollection<Triangle> triangles)
+ {
+ //find the triangle
+ Vertex ear = earVertices[0].Value;
+ Vertex prev = polygonVertices[polygonVertices.IndexOf(ear) - 1].Value;
+ Vertex next = polygonVertices[polygonVertices.IndexOf(ear) + 1].Value;
+ triangles.Add(new Triangle(ear, next, prev));
+
+ //remove the ear from the shape
+ earVertices.RemoveAt(0);
+ polygonVertices.RemoveAt(polygonVertices.IndexOf(ear));
+ Log("\nRemoved Ear: {0}", ear);
+
+ //validate the neighboring vertices
+ ValidateAdjacentVertex(prev);
+ ValidateAdjacentVertex(next);
+
+ //write out the states of each of the lists
+#if DEBUG
+ StringBuilder rString = new StringBuilder();
+ foreach (Vertex v in reflexVertices)
+ rString.Append(string.Format("{0}, ", v.Index));
+ Log("Reflex Vertices: {0}", rString);
+
+ StringBuilder cString = new StringBuilder();
+ foreach (Vertex v in convexVertices)
+ cString.Append(string.Format("{0}, ", v.Index));
+ Log("Convex Vertices: {0}", cString);
+
+ StringBuilder eString = new StringBuilder();
+ foreach (Vertex v in earVertices)
+ eString.Append(string.Format("{0}, ", v.Index));
+ Log("Ear Vertices: {0}", eString);
+#endif
+ }
+
+ #endregion
+
+ #region ValidateAdjacentVertex
+
+ private static void ValidateAdjacentVertex(Vertex vertex)
+ {
+ Log("Validating: {0}...", vertex);
+
+ if (reflexVertices.Contains(vertex))
+ {
+ if (IsConvex(vertex))
+ {
+ reflexVertices.Remove(vertex);
+ convexVertices.Add(vertex);
+ Log("Vertex: {0} now convex", vertex);
+ }
+ else
+ {
+ Log("Vertex: {0} still reflex", vertex);
+ }
+ }
+
+ if (convexVertices.Contains(vertex))
+ {
+ bool wasEar = earVertices.Contains(vertex);
+ bool isEar = IsEar(vertex);
+
+ if (wasEar && !isEar)
+ {
+ earVertices.Remove(vertex);
+ Log("Vertex: {0} no longer ear", vertex);
+ }
+ else if (!wasEar && isEar)
+ {
+ earVertices.AddFirst(vertex);
+ Log("Vertex: {0} now ear", vertex);
+ }
+ else
+ {
+ Log("Vertex: {0} still ear", vertex);
+ }
+ }
+ }
+
+ #endregion
+
+ #region FindConvexAndReflexVertices
+
+ private static void FindConvexAndReflexVertices()
+ {
+ for (int i = 0; i < polygonVertices.Count; i++)
+ {
+ Vertex v = polygonVertices[i].Value;
+
+ if (IsConvex(v))
+ {
+ convexVertices.Add(v);
+ Log("Convex: {0}", v);
+ }
+ else
+ {
+ reflexVertices.Add(v);
+ Log("Reflex: {0}", v);
+ }
+ }
+ }
+
+ #endregion
+
+ #region FindEarVertices
+
+ private static void FindEarVertices()
+ {
+ for (int i = 0; i < convexVertices.Count; i++)
+ {
+ Vertex c = convexVertices[i];
+
+ if (IsEar(c))
+ {
+ earVertices.AddLast(c);
+ Log("Ear: {0}", c);
+ }
+ }
+ }
+
+ #endregion
+
+ #region IsEar
+
+ private static bool IsEar(Vertex c)
+ {
+ Vertex p = polygonVertices[polygonVertices.IndexOf(c) - 1].Value;
+ Vertex n = polygonVertices[polygonVertices.IndexOf(c) + 1].Value;
+
+ Log("Testing vertex {0} as ear with triangle {1}, {0}, {2}...", c, p, n);
+
+ foreach (Vertex t in reflexVertices)
+ {
+ if (t.Equals(p) || t.Equals(c) || t.Equals(n))
+ continue;
+
+ if (Triangle.ContainsPoint(p, c, n, t))
+ {
+ Log("\tTriangle contains vertex {0}...", t);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ #endregion
+
+ #region IsConvex
+
+ private static bool IsConvex(Vertex c)
+ {
+ Vertex p = polygonVertices[polygonVertices.IndexOf(c) - 1].Value;
+ Vertex n = polygonVertices[polygonVertices.IndexOf(c) + 1].Value;
+
+ Vector2 d1 = Vector2.Normalize(c.Position - p.Position);
+ Vector2 d2 = Vector2.Normalize(n.Position - c.Position);
+ Vector2 n2 = new Vector2(-d2.Y, d2.X);
+
+ return (Vector2.Dot(d1, n2) <= 0f);
+ }
+
+ #endregion
+
+ #region IsReflex
+
+ private static bool IsReflex(Vertex c)
+ {
+ return !IsConvex(c);
+ }
+
+ #endregion
+
+ #region Log
+
+ [Conditional("DEBUG")]
+ private static void Log(string format, params object[] parameters)
+ {
+ //System.Console.WriteLine(format, parameters);
+ }
+
+ #endregion
+
+ #endregion
+ }
+
+ /// <summary>
+ /// Specifies a desired winding order for the shape vertices.
+ /// </summary>
+ public enum WindingOrder
+ {
+ Clockwise,
+ CounterClockwise
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Vertex.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Vertex.cs
new file mode 100644
index 0000000..2630b07
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Vertex.cs
@@ -0,0 +1,47 @@
+using Microsoft.Xna.Framework;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MonoGame.Extended.Triangulation
+{
+ /// <summary>
+ /// MIT Licensed: https://github.com/nickgravelyn/Triangulator
+ /// </summary>
+ struct Vertex
+ {
+ public readonly Vector2 Position;
+ public readonly int Index;
+
+ public Vertex(Vector2 position, int index)
+ {
+ Position = position;
+ Index = index;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj.GetType() != typeof(Vertex))
+ return false;
+ return Equals((Vertex)obj);
+ }
+
+ public bool Equals(Vertex obj)
+ {
+ return obj.Position.Equals(Position) && obj.Index == Index;
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (Position.GetHashCode() * 397) ^ Index;
+ }
+ }
+
+ public override string ToString()
+ {
+ return string.Format("{0} ({1})", Position, Index);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Vector2Extensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Vector2Extensions.cs
new file mode 100644
index 0000000..b50a366
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Vector2Extensions.cs
@@ -0,0 +1,249 @@
+using System;
+using System.Runtime.CompilerServices;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public static class Vector2Extensions
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 SetX(this Vector2 vector2, float x) => new Vector2(x, vector2.Y);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 SetY(this Vector2 vector2, float y) => new Vector2(vector2.X, y);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 Translate(this Vector2 vector2, float x, float y) => new Vector2(vector2.X + x, vector2.Y + y);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Size2 ToSize(this Vector2 value) => new Size2(value.X, value.Y);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Size2 ToAbsoluteSize(this Vector2 value)
+ {
+ var x = Math.Abs(value.X);
+ var y = Math.Abs(value.Y);
+ return new Size2(x, y);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 Round(this Vector2 value, int digits, MidpointRounding mode)
+ {
+ var x = (float)Math.Round(value.X, digits, mode);
+ var y = (float)Math.Round(value.Y, digits, mode);
+ return new Vector2(x, y);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 Round(this Vector2 value, int digits)
+ {
+ var x = (float)Math.Round(value.X, digits);
+ var y = (float)Math.Round(value.Y, digits);
+ return new Vector2(x, y);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 Round(this Vector2 value)
+ {
+ var x = (float)Math.Round(value.X);
+ var y = (float)Math.Round(value.Y);
+ return new Vector2(x, y);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool EqualsWithTolerence(this Vector2 value, Vector2 otherValue, float tolerance = 0.00001f)
+ {
+ return Math.Abs(value.X - otherValue.X) <= tolerance && (Math.Abs(value.Y - otherValue.Y) <= tolerance);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 Rotate(this Vector2 value, float radians)
+ {
+ var cos = (float) Math.Cos(radians);
+ var sin = (float) Math.Sin(radians);
+ return new Vector2(value.X*cos - value.Y*sin, value.X*sin + value.Y*cos);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 NormalizedCopy(this Vector2 value)
+ {
+ var newVector2 = new Vector2(value.X, value.Y);
+ newVector2.Normalize();
+ return newVector2;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 PerpendicularClockwise(this Vector2 value) => new Vector2(value.Y, -value.X);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 PerpendicularCounterClockwise(this Vector2 value) => new Vector2(-value.Y, value.X);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 Truncate(this Vector2 value, float maxLength)
+ {
+ if (value.LengthSquared() > maxLength*maxLength)
+ return value.NormalizedCopy()*maxLength;
+
+ return value;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsNaN(this Vector2 value) => float.IsNaN(value.X) || float.IsNaN(value.Y);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float ToAngle(this Vector2 value) => (float) Math.Atan2(value.X, -value.Y);
+
+ /// <summary>
+ /// Calculates the dot product of two vectors. If the two vectors are unit vectors, the dot product returns a floating
+ /// point value between -1 and 1 that can be used to determine some properties of the angle between two vectors. For
+ /// example, it can show whether the vectors are orthogonal, parallel, or have an acute or obtuse angle between them.
+ /// </summary>
+ /// <param name="vector1">The first vector.</param>
+ /// <param name="vector2">The second vector.</param>
+ /// <returns>The dot product of the two vectors.</returns>
+ /// <remarks>
+ /// <para>The dot product is also known as the inner product.</para>
+ /// <para>
+ /// For any two vectors, the dot product is defined as: <c>(vector1.X * vector2.X) + (vector1.Y * vector2.Y).</c>
+ /// The result of this calculation, plus or minus some margin to account for floating point error, is equal to:
+ /// <c>Length(vector1) * Length(vector2) * System.Math.Cos(theta)</c>, where <c>theta</c> is the angle between the
+ /// two vectors.
+ /// </para>
+ /// <para>
+ /// If <paramref name="vector1" /> and <paramref name="vector2" /> are unit vectors, the length of each
+ /// vector will be equal to 1. So, when <paramref name="vector1" /> and <paramref name="vector2" /> are unit
+ /// vectors, the dot product is simply equal to the cosine of the angle between the two vectors. For example, both
+ /// <c>cos</c> values in the following calcuations would be equal in value:
+ /// <c>vector1.Normalize(); vector2.Normalize(); var cos = vector1.Dot(vector2)</c>,
+ /// <c>var cos = System.Math.Cos(theta)</c>, where <c>theta</c> is angle in radians betwen the two vectors.
+ /// </para>
+ /// <para>
+ /// If <paramref name="vector1" /> and <paramref name="vector2" /> are unit vectors, without knowing the value of
+ /// <c>theta</c> or using a potentially processor-intensive trigonometric function, the value of the dot product
+ /// can tell us the
+ /// following things:
+ /// <list type="bullet">
+ /// <item>
+ /// <description>
+ /// If <c>vector1.Dot(vector2) &gt; 0</c>, the angle between the two vectors
+ /// is less than 90 degrees.
+ /// </description>
+ /// </item>
+ /// <item>
+ /// <description>
+ /// If <c>vector1.Dot(vector2) &lt; 0</c>, the angle between the two vectors
+ /// is more than 90 degrees.
+ /// </description>
+ /// </item>
+ /// <item>
+ /// <description>
+ /// If <c>vector1.Dot(vector2) == 0</c>, the angle between the two vectors
+ /// is 90 degrees; that is, the vectors are othogonal.
+ /// </description>
+ /// </item>
+ /// <item>
+ /// <description>
+ /// If <c>vector1.Dot(vector2) == 1</c>, the angle between the two vectors
+ /// is 0 degrees; that is, the vectors point in the same direction and are parallel.
+ /// </description>
+ /// </item>
+ /// <item>
+ /// <description>
+ /// If <c>vector1.Dot(vector2) == -1</c>, the angle between the two vectors
+ /// is 180 degrees; that is, the vectors point in opposite directions and are parallel.
+ /// </description>
+ /// </item>
+ /// </list>
+ /// </para>
+ /// <note type="caution">
+ /// Because of floating point error, two orthogonal vectors may not return a dot product that is exactly zero. It
+ /// might be zero plus some amount of floating point error. In your code, you will want to determine what amount of
+ /// error is acceptable in your calculation, and take that into account when you do your comparisons.
+ /// </note>
+ /// </remarks>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float Dot(this Vector2 vector1, Vector2 vector2)
+ {
+ return vector1.X*vector2.X + vector1.Y*vector2.Y;
+ }
+
+ /// <summary>
+ /// Calculates the scalar projection of one vector onto another. The scalar projection returns the length of the
+ /// orthogonal projection of the first vector onto a straight line parallel to the second vector, with a negative value
+ /// if the projection has an opposite direction with respect to the second vector.
+ /// </summary>
+ /// <param name="vector1">The first vector.</param>
+ /// <param name="vector2">The second vector.</param>
+ /// <returns>The scalar projection of <paramref name="vector1" /> onto <paramref name="vector2" />.</returns>
+ /// <remarks>
+ /// <para>
+ /// The scalar projection is also known as the scalar resolute of the first vector in the direction of the second
+ /// vector.
+ /// </para>
+ /// <para>
+ /// For any two vectors, the scalar projection is defined as: <c>vector1.Dot(vector2) / Length(vector2)</c>. The
+ /// result of this calculation, plus or minus some margin to account for floating point error, is equal to:
+ /// <c>Length(vector1) * System.Math.Cos(theta)</c>, where <c>theta</c> is the angle in radians between
+ /// <paramref name="vector1" /> and <paramref name="vector2" />.
+ /// </para>
+ /// <para>
+ /// The value of the scalar projection can tell us the following things:
+ /// <list type="bullet">
+ /// <item>
+ /// <description>
+ /// If <c>vector1.ScalarProjectOnto(vector2) &gt;= 0</c>, the angle between <paramref name="vector1" />
+ /// and <paramref name="vector2" /> is between 0 degrees (exclusive) and 90 degrees (inclusive).
+ /// </description>
+ /// </item>
+ /// <item>
+ /// <description>
+ /// If <c>vector1.ScalarProjectOnto(vector2) &lt; 0</c>, the angle between <paramref name="vector1" />
+ /// and <paramref name="vector2" /> is between 90 degrees (exclusive) and 180 degrees (inclusive).
+ /// </description>
+ /// </item>
+ /// </list>
+ /// </para>
+ /// </remarks>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float ScalarProjectOnto(this Vector2 vector1, Vector2 vector2)
+ {
+ var dotNumerator = vector1.X*vector2.X + vector1.Y*vector2.Y;
+ var lengthSquaredDenominator = vector2.X*vector2.X + vector2.Y*vector2.Y;
+ return dotNumerator/(float) Math.Sqrt(lengthSquaredDenominator);
+ }
+
+ /// <summary>
+ /// Calculates the vector projection of one vector onto another. The vector projection returns the orthogonal
+ /// projection of the first vector onto a straight line parallel to the second vector.
+ /// </summary>
+ /// <param name="vector1">The first vector.</param>
+ /// <param name="vector2">The second vector.</param>
+ /// <returns>The vector projection of <paramref name="vector1" /> onto <paramref name="vector2" />.</returns>
+ /// <remarks>
+ /// <para>
+ /// The vector projection is also known as the vector component or vector resolute of the first vector in the
+ /// direction of the second vector.
+ /// </para>
+ /// <para>
+ /// For any two vectors, the vector projection is defined as:
+ /// <c>( vector1.Dot(vector2) / Length(vector2)^2 ) * vector2</c>.
+ /// The
+ /// result of this calculation, plus or minus some margin to account for floating point error, is equal to:
+ /// <c>( Length(vector1) * System.Math.Cos(theta) ) * vector2 / Length(vector2)</c>, where <c>theta</c> is the
+ /// angle in radians between <paramref name="vector1" /> and <paramref name="vector2" />.
+ /// </para>
+ /// <para>
+ /// This function is easier to compute than <see cref="ScalarProjectOnto" /> since it does not use a square root.
+ /// When the vector projection and the scalar projection is required, consider using this function; the scalar
+ /// projection can be obtained by taking the length of the projection vector.
+ /// </para>
+ /// </remarks>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 ProjectOnto(this Vector2 vector1, Vector2 vector2)
+ {
+ var dotNumerator = vector1.X*vector2.X + vector1.Y*vector2.Y;
+ var lengthSquaredDenominator = vector2.X*vector2.X + vector2.Y*vector2.Y;
+ return dotNumerator/lengthSquaredDenominator*vector2;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj
new file mode 100644
index 0000000..e99bbe5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj
@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Description>It makes MonoGame more awesome.</Description>
+ <PackageTags>monogame extended pipeline bmfont tiled texture atlas input viewport fps shapes sprite</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="MonoGame.Framework.Content.Pipeline"
+ Version="3.8.1.303"
+ PrivateAssets="All" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj.DotSettings b/Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj.DotSettings
new file mode 100644
index 0000000..66c073e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj.DotSettings
@@ -0,0 +1,2 @@
+<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+ <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=math/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/OrthographicCamera.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/OrthographicCamera.cs
new file mode 100644
index 0000000..ef98c99
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/OrthographicCamera.cs
@@ -0,0 +1,209 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.ViewportAdapters;
+
+namespace MonoGame.Extended
+{
+ public sealed class OrthographicCamera : Camera<Vector2>, IMovable, IRotatable
+ {
+ private readonly ViewportAdapter _viewportAdapter;
+ private float _maximumZoom = float.MaxValue;
+ private float _minimumZoom;
+ private float _zoom;
+
+ public OrthographicCamera(GraphicsDevice graphicsDevice)
+ : this(new DefaultViewportAdapter(graphicsDevice))
+ {
+ }
+
+ public OrthographicCamera(ViewportAdapter viewportAdapter)
+ {
+ _viewportAdapter = viewportAdapter;
+
+ Rotation = 0;
+ Zoom = 1;
+ Origin = new Vector2(viewportAdapter.VirtualWidth/2f, viewportAdapter.VirtualHeight/2f);
+ Position = Vector2.Zero;
+ }
+
+ public override Vector2 Position { get; set; }
+ public override float Rotation { get; set; }
+ public override Vector2 Origin { get; set; }
+ public override Vector2 Center => Position + Origin;
+
+ public override float Zoom
+ {
+ get => _zoom;
+ set
+ {
+ if ((value < MinimumZoom) || (value > MaximumZoom))
+ throw new ArgumentException("Zoom must be between MinimumZoom and MaximumZoom");
+
+ _zoom = value;
+ }
+ }
+
+ public override float MinimumZoom
+ {
+ get => _minimumZoom;
+ set
+ {
+ if (value < 0)
+ throw new ArgumentException("MinimumZoom must be greater than zero");
+
+ if (Zoom < value)
+ Zoom = MinimumZoom;
+
+ _minimumZoom = value;
+ }
+ }
+
+ public override float MaximumZoom
+ {
+ get => _maximumZoom;
+ set
+ {
+ if (value < 0)
+ throw new ArgumentException("MaximumZoom must be greater than zero");
+
+ if (Zoom > value)
+ Zoom = value;
+
+ _maximumZoom = value;
+ }
+ }
+
+ public override RectangleF BoundingRectangle
+ {
+ get
+ {
+ var frustum = GetBoundingFrustum();
+ var corners = frustum.GetCorners();
+ var topLeft = corners[0];
+ var bottomRight = corners[2];
+ var width = bottomRight.X - topLeft.X;
+ var height = bottomRight.Y - topLeft.Y;
+ return new RectangleF(topLeft.X, topLeft.Y, width, height);
+ }
+ }
+
+ public override void Move(Vector2 direction)
+ {
+ Position += Vector2.Transform(direction, Matrix.CreateRotationZ(-Rotation));
+ }
+
+ public override void Rotate(float deltaRadians)
+ {
+ Rotation += deltaRadians;
+ }
+
+ public override void ZoomIn(float deltaZoom)
+ {
+ ClampZoom(Zoom + deltaZoom);
+ }
+
+ public override void ZoomOut(float deltaZoom)
+ {
+ ClampZoom(Zoom - deltaZoom);
+ }
+
+ private void ClampZoom(float value)
+ {
+ if (value < MinimumZoom)
+ Zoom = MinimumZoom;
+ else
+ Zoom = value > MaximumZoom ? MaximumZoom : value;
+ }
+
+ public override void LookAt(Vector2 position)
+ {
+ Position = position - new Vector2(_viewportAdapter.VirtualWidth/2f, _viewportAdapter.VirtualHeight/2f);
+ }
+
+ public Vector2 WorldToScreen(float x, float y)
+ {
+ return WorldToScreen(new Vector2(x, y));
+ }
+
+ public override Vector2 WorldToScreen(Vector2 worldPosition)
+ {
+ var viewport = _viewportAdapter.Viewport;
+ return Vector2.Transform(worldPosition + new Vector2(viewport.X, viewport.Y), GetViewMatrix());
+ }
+
+ public Vector2 ScreenToWorld(float x, float y)
+ {
+ return ScreenToWorld(new Vector2(x, y));
+ }
+
+ public override Vector2 ScreenToWorld(Vector2 screenPosition)
+ {
+ var viewport = _viewportAdapter.Viewport;
+ return Vector2.Transform(screenPosition - new Vector2(viewport.X, viewport.Y),
+ Matrix.Invert(GetViewMatrix()));
+ }
+
+ public Matrix GetViewMatrix(Vector2 parallaxFactor)
+ {
+ return GetVirtualViewMatrix(parallaxFactor)*_viewportAdapter.GetScaleMatrix();
+ }
+
+ private Matrix GetVirtualViewMatrix(Vector2 parallaxFactor)
+ {
+ return
+ Matrix.CreateTranslation(new Vector3(-Position*parallaxFactor, 0.0f))*
+ Matrix.CreateTranslation(new Vector3(-Origin, 0.0f))*
+ Matrix.CreateRotationZ(Rotation)*
+ Matrix.CreateScale(Zoom, Zoom, 1)*
+ Matrix.CreateTranslation(new Vector3(Origin, 0.0f));
+ }
+
+ private Matrix GetVirtualViewMatrix()
+ {
+ return GetVirtualViewMatrix(Vector2.One);
+ }
+
+ public override Matrix GetViewMatrix()
+ {
+ return GetViewMatrix(Vector2.One);
+ }
+
+ public override Matrix GetInverseViewMatrix()
+ {
+ return Matrix.Invert(GetViewMatrix());
+ }
+
+ private Matrix GetProjectionMatrix(Matrix viewMatrix)
+ {
+ var projection = Matrix.CreateOrthographicOffCenter(0, _viewportAdapter.VirtualWidth, _viewportAdapter.VirtualHeight, 0, -1, 0);
+ Matrix.Multiply(ref viewMatrix, ref projection, out projection);
+ return projection;
+ }
+
+ public override BoundingFrustum GetBoundingFrustum()
+ {
+ var viewMatrix = GetVirtualViewMatrix();
+ var projectionMatrix = GetProjectionMatrix(viewMatrix);
+ return new BoundingFrustum(projectionMatrix);
+ }
+
+ public ContainmentType Contains(Point point)
+ {
+ return Contains(point.ToVector2());
+ }
+
+ public override ContainmentType Contains(Vector2 vector2)
+ {
+ return GetBoundingFrustum().Contains(new Vector3(vector2.X, vector2.Y, 0));
+ }
+
+ public override ContainmentType Contains(Rectangle rectangle)
+ {
+ var max = new Vector3(rectangle.X + rectangle.Width, rectangle.Y + rectangle.Height, 0.5f);
+ var min = new Vector3(rectangle.X, rectangle.Y, 0.5f);
+ var boundingBox = new BoundingBox(min, max);
+ return GetBoundingFrustum().Contains(boundingBox);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/GameScreen.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/GameScreen.cs
new file mode 100644
index 0000000..7752c02
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/GameScreen.cs
@@ -0,0 +1,19 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Screens
+{
+ public abstract class GameScreen : Screen
+ {
+ protected GameScreen(Game game)
+ {
+ Game = game;
+ }
+
+ public Game Game { get; }
+ public ContentManager Content => Game.Content;
+ public GraphicsDevice GraphicsDevice => Game.GraphicsDevice;
+ public GameServiceContainer Services => Game.Services;
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Screen.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Screen.cs
new file mode 100644
index 0000000..59137f4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Screen.cs
@@ -0,0 +1,17 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Screens
+{
+ public abstract class Screen : IDisposable
+ {
+ public ScreenManager ScreenManager { get; internal set; }
+
+ public virtual void Dispose() { }
+ public virtual void Initialize() { }
+ public virtual void LoadContent() { }
+ public virtual void UnloadContent() { }
+ public abstract void Update(GameTime gameTime);
+ public abstract void Draw(GameTime gameTime);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/ScreenManager.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/ScreenManager.cs
new file mode 100644
index 0000000..ad441c6
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/ScreenManager.cs
@@ -0,0 +1,78 @@
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Screens.Transitions;
+
+namespace MonoGame.Extended.Screens
+{
+ public class ScreenManager : SimpleDrawableGameComponent
+ {
+ public ScreenManager()
+ {
+ }
+
+ private Screen _activeScreen;
+ //private bool _isInitialized;
+ //private bool _isLoaded;
+ private Transition _activeTransition;
+
+ public void LoadScreen(Screen screen, Transition transition)
+ {
+ if(_activeTransition != null)
+ return;
+
+ _activeTransition = transition;
+ _activeTransition.StateChanged += (sender, args) => LoadScreen(screen);
+ _activeTransition.Completed += (sender, args) =>
+ {
+ _activeTransition.Dispose();
+ _activeTransition = null;
+ };
+ }
+
+ public void LoadScreen(Screen screen)
+ {
+ _activeScreen?.UnloadContent();
+ _activeScreen?.Dispose();
+
+ screen.ScreenManager = this;
+
+ screen.Initialize();
+
+ screen.LoadContent();
+
+ _activeScreen = screen;
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ _activeScreen?.Initialize();
+ //_isInitialized = true;
+ }
+
+ protected override void LoadContent()
+ {
+ base.LoadContent();
+ _activeScreen?.LoadContent();
+ //_isLoaded = true;
+ }
+
+ protected override void UnloadContent()
+ {
+ base.UnloadContent();
+ _activeScreen?.UnloadContent();
+ //_isLoaded = false;
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ _activeScreen?.Update(gameTime);
+ _activeTransition?.Update(gameTime);
+ }
+
+ public override void Draw(GameTime gameTime)
+ {
+ _activeScreen?.Draw(gameTime);
+ _activeTransition?.Draw(gameTime);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/ExpandTransition.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/ExpandTransition.cs
new file mode 100644
index 0000000..32a8cca
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/ExpandTransition.cs
@@ -0,0 +1,42 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Screens.Transitions
+{
+ public class ExpandTransition : Transition
+ {
+ private readonly GraphicsDevice _graphicsDevice;
+ private readonly SpriteBatch _spriteBatch;
+
+ public ExpandTransition(GraphicsDevice graphicsDevice, Color color, float duration = 1.0f)
+ : base(duration)
+ {
+ Color = color;
+
+ _graphicsDevice = graphicsDevice;
+ _spriteBatch = new SpriteBatch(graphicsDevice);
+ }
+
+ public override void Dispose()
+ {
+ _spriteBatch.Dispose();
+ }
+
+ public Color Color { get; }
+
+ public override void Draw(GameTime gameTime)
+ {
+ var halfWidth = _graphicsDevice.Viewport.Width / 2f;
+ var halfHeight = _graphicsDevice.Viewport.Height / 2f;
+ var x = halfWidth * (1.0f - Value);
+ var y = halfHeight * (1.0f - Value);
+ var width = _graphicsDevice.Viewport.Width * Value;
+ var height = _graphicsDevice.Viewport.Height * Value;
+ var rectangle = new RectangleF(x, y, width, height);
+
+ _spriteBatch.Begin(samplerState: SamplerState.PointClamp);
+ _spriteBatch.FillRectangle(rectangle, Color);
+ _spriteBatch.End();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/FadeTransition.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/FadeTransition.cs
new file mode 100644
index 0000000..49fab4a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/FadeTransition.cs
@@ -0,0 +1,34 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Screens.Transitions
+{
+ public class FadeTransition : Transition
+ {
+ private readonly GraphicsDevice _graphicsDevice;
+ private readonly SpriteBatch _spriteBatch;
+
+ public FadeTransition(GraphicsDevice graphicsDevice, Color color, float duration = 1.0f)
+ : base(duration)
+ {
+ Color = color;
+
+ _graphicsDevice = graphicsDevice;
+ _spriteBatch = new SpriteBatch(graphicsDevice);
+ }
+
+ public override void Dispose()
+ {
+ _spriteBatch.Dispose();
+ }
+
+ public Color Color { get; }
+
+ public override void Draw(GameTime gameTime)
+ {
+ _spriteBatch.Begin(samplerState: SamplerState.PointClamp);
+ _spriteBatch.FillRectangle(0, 0, _graphicsDevice.Viewport.Width, _graphicsDevice.Viewport.Height, Color * Value);
+ _spriteBatch.End();
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/Transition.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/Transition.cs
new file mode 100644
index 0000000..8336a04
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/Transition.cs
@@ -0,0 +1,58 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Screens.Transitions
+{
+ public enum TransitionState { In, Out }
+
+ public abstract class Transition : IDisposable
+ {
+ private readonly float _halfDuration;
+ private float _currentSeconds;
+
+ protected Transition(float duration)
+ {
+ Duration = duration;
+ _halfDuration = Duration / 2f;
+ }
+
+ public abstract void Dispose();
+
+ public TransitionState State { get; private set; } = TransitionState.Out;
+ public float Duration { get; }
+ public float Value => MathHelper.Clamp(_currentSeconds / _halfDuration, 0f, 1f);
+
+ public event EventHandler StateChanged;
+ public event EventHandler Completed;
+
+ public void Update(GameTime gameTime)
+ {
+ var elapsedSeconds = gameTime.GetElapsedSeconds();
+
+ switch (State)
+ {
+ case TransitionState.Out:
+ _currentSeconds += elapsedSeconds;
+
+ if (_currentSeconds >= _halfDuration)
+ {
+ State = TransitionState.In;
+ StateChanged?.Invoke(this, EventArgs.Empty);
+ }
+ break;
+ case TransitionState.In:
+ _currentSeconds -= elapsedSeconds;
+
+ if (_currentSeconds <= 0.0f)
+ {
+ Completed?.Invoke(this, EventArgs.Empty);
+ }
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ public abstract void Draw(GameTime gameTime);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/BaseTypeJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/BaseTypeJsonConverter.cs
new file mode 100644
index 0000000..cdbec1e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/BaseTypeJsonConverter.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Serialization
+{
+ public abstract class BaseTypeJsonConverter<T> : JsonConverter<T>
+ {
+ private readonly string _suffix;
+ private readonly Dictionary<string, Type> _namesToTypes;
+ private readonly Dictionary<Type, string> _typesToNames;
+ private readonly JsonSerializerOptions _serializerOptions;
+ private readonly JsonNamingPolicy _namingPolicy = JsonNamingPolicy.CamelCase;
+
+ protected BaseTypeJsonConverter(IEnumerable<TypeInfo> supportedTypes, string suffix)
+ {
+ _suffix = suffix;
+ _namesToTypes = supportedTypes
+ .ToDictionary(t => TrimSuffix(t.Name, suffix), t => t.AsType(), StringComparer.OrdinalIgnoreCase);
+ _typesToNames = _namesToTypes.ToDictionary(i => i.Value, i => i.Key);
+
+ _serializerOptions = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = _namingPolicy,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ };
+ _serializerOptions.Converters.Add(this);
+ }
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) =>
+ _namesToTypes.ContainsValue(typeToConvert) || typeof(T) == typeToConvert;
+
+ /// <inheritdoc />
+ /// <exception cref="InvalidOperationException" />
+ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ using (JsonDocument doc = JsonDocument.ParseValue(ref reader))
+ {
+ var jObject = doc.RootElement;
+ var key = jObject.GetProperty("type").GetString();
+
+ if (_namesToTypes.TryGetValue(key, out Type type))
+ {
+ var value = JsonSerializer.Deserialize(jObject.GetRawText(), type, options);
+ return (T)value;
+ }
+
+ throw new InvalidOperationException($"Unknown {_suffix} type '{key}'");
+ }
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
+ {
+ var type = value.GetType();
+ var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
+
+ writer.WriteStartObject();
+ writer.WriteString("type", _typesToNames[type]);
+
+ foreach (var property in properties)
+ {
+ var propertyName = _namingPolicy.ConvertName(property.Name);
+ writer.WritePropertyName(propertyName);
+ JsonSerializer.Serialize(writer, property.GetValue(value), property.PropertyType, options);
+ }
+
+ writer.WriteEndObject();
+ }
+
+ private static string TrimSuffix(string input, string suffix)
+ {
+ if (input.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
+ return input.Substring(0, input.Length - suffix.Length);
+
+ return input;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ColorJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ColorJsonConverter.cs
new file mode 100644
index 0000000..41ceabe
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ColorJsonConverter.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Converts a <see cref="Color"/> value to or from JSON.
+/// </summary>
+public class ColorJsonConverter : JsonConverter<Color>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Color);
+
+ /// <inheritdoc />
+ public override Color Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var value = reader.GetString();
+ return value[0] == '#' ? ColorHelper.FromHex(value) : ColorHelper.FromName(value);
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, Color value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ var hexValue = ColorHelper.ToHex(value);
+ writer.WriteStringValue(hexValue);
+ }
+}
+
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ContentManagerJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ContentManagerJsonConverter.cs
new file mode 100644
index 0000000..6b57bfe
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ContentManagerJsonConverter.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework.Content;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Loads content from a JSON file into the <see cref="ContentManager"/> using the asset name
+/// </summary>
+/// <typeparam name="T">The type of content to load</typeparam>
+public class ContentManagerJsonConverter<T> : JsonConverter<T>
+{
+ private readonly ContentManager _contentManager;
+ private readonly Func<T, string> _getAssetName;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ContentManagerJsonConverter{T}"/> class.
+ /// </summary>
+ /// <param name="contentManager">The <see cref="ContentManager"/> used to load content.</param>
+ /// <param name="getAssetName">A function that returns the asset name for a given instance of <typeparamref name="T"/>.</param>
+ public ContentManagerJsonConverter(ContentManager contentManager, Func<T, string> getAssetName)
+ {
+ _contentManager = contentManager;
+ _getAssetName = getAssetName;
+ }
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(T);
+
+ /// <inheritdoc />
+ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var assetName = reader.GetString();
+ return _contentManager.Load<T>(assetName);
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ var asset = (T)value;
+ var assetName = _getAssetName(asset);
+ writer.WriteStringValue(assetName);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/FloatStringConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/FloatStringConverter.cs
new file mode 100644
index 0000000..a19c589
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/FloatStringConverter.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Serialization;
+
+public class FloatStringConverter : JsonConverter<float>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(float) || typeToConvert == typeof(string);
+
+ /// <inheritdoc />
+ public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ if (float.TryParse(reader.GetString(), out float value))
+ return value;
+ }
+ else if (reader.TokenType == JsonTokenType.Number)
+ {
+ return reader.GetSingle();
+ }
+
+ throw new JsonException($"Unable to convert value of type {reader.TokenType} to {typeof(float)}");
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteNumberValue(value);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/HslColorJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/HslColorJsonConverter.cs
new file mode 100644
index 0000000..6d63a15
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/HslColorJsonConverter.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Converts a <see cref="HslColor"/> value to or from JSON.
+/// </summary>
+public class HslColorJsonConverter : JsonConverter<HslColor>
+{
+ private readonly ColorJsonConverter _colorConverter = new ColorJsonConverter();
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(HslColor);
+
+ /// <inheritdoc />
+ public override HslColor Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var color = _colorConverter.Read(ref reader, typeToConvert, options);
+ return HslColor.FromRgb(color);
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, HslColor value, JsonSerializerOptions options)
+ {
+ var color = ((HslColor)value).ToRgb();
+ _colorConverter.Write(writer, color, options);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentLoader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentLoader.cs
new file mode 100644
index 0000000..69a1212
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentLoader.cs
@@ -0,0 +1,18 @@
+using System.Text.Json;
+using Microsoft.Xna.Framework.Content;
+using MonoGame.Extended.Content;
+
+
+namespace MonoGame.Extended.Serialization
+{
+ public class JsonContentLoader : IContentLoader
+ {
+ public T Load<T>(ContentManager contentManager, string path)
+ {
+
+ using var stream = contentManager.OpenStream(path);
+ var monoGameSerializerOptions = MonoGameJsonSerializerOptionsProvider.GetOptions(contentManager, path);
+ return JsonSerializer.Deserialize<T>(stream, monoGameSerializerOptions);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentTypeReader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentTypeReader.cs
new file mode 100644
index 0000000..1d5609e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentTypeReader.cs
@@ -0,0 +1,16 @@
+using System.IO;
+using System.Text.Json;
+using Microsoft.Xna.Framework.Content;
+
+
+namespace MonoGame.Extended.Serialization
+{
+ public class JsonContentTypeReader<T> : ContentTypeReader<T>
+ {
+ protected override T Read(ContentReader reader, T existingInstance)
+ {
+ var json = reader.ReadString();
+ return JsonSerializer.Deserialize<T>(json);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/MonoGameJsonSerializerOptionsProvider.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/MonoGameJsonSerializerOptionsProvider.cs
new file mode 100644
index 0000000..32a099f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/MonoGameJsonSerializerOptionsProvider.cs
@@ -0,0 +1,29 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework.Content;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Serialization;
+
+public static class MonoGameJsonSerializerOptionsProvider
+{
+ public static JsonSerializerOptions GetOptions(ContentManager contentManager, string contentPath)
+ {
+ var options = new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ options.Converters.Add(new RangeJsonConverter<int>());
+ options.Converters.Add(new RangeJsonConverter<float>());
+ options.Converters.Add(new RangeJsonConverter<HslColor>());
+ options.Converters.Add(new ThicknessJsonConverter());
+ options.Converters.Add(new RectangleFJsonConverter());
+ options.Converters.Add(new TextureAtlasJsonConverter(contentManager, contentPath));
+ options.Converters.Add(new Size2JsonConverter());
+
+ return options;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/NinePatchRegion2DJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/NinePatchRegion2DJsonConverter.cs
new file mode 100644
index 0000000..44bbbf7
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/NinePatchRegion2DJsonConverter.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Converts a <see cref="NinePatchRegion2D"/> value to or from JSON.
+/// </summary>
+public class NinePatchRegion2DJsonConverter : JsonConverter<NinePatchRegion2D>
+{
+ private readonly ITextureRegionService _textureRegionService;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NinePatchRegion2DJsonConverter"/> class.
+ /// </summary>
+ /// <param name="textureRegionService">The texture region service used to retrieve texture regions.</param>
+ public NinePatchRegion2DJsonConverter(ITextureRegionService textureRegionService)
+ {
+ _textureRegionService = textureRegionService;
+ }
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(NinePatchRegion2D);
+
+ /// <inheritdoc />
+ /// <exception cref="JsonException">
+ /// Thrown if the JSON property does not contain a properly formatted <see cref="NinePatchRegion2D"/> value
+ /// </exception>
+ public override NinePatchRegion2D Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.StartObject)
+ {
+ throw new JsonException($"Expected {nameof(JsonTokenType.StartObject)} token");
+ }
+
+ string padding = string.Empty;
+ string regionName = string.Empty;
+
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndObject)
+ {
+ break;
+ }
+
+ if (reader.TokenType == JsonTokenType.PropertyName)
+ {
+ var propertyName = reader.GetString();
+ reader.Read();
+
+ if (propertyName.Equals("Padding", StringComparison.Ordinal))
+ {
+ padding = reader.GetString();
+ }
+ else if (propertyName.Equals("TextureRegion", StringComparison.Ordinal))
+ {
+ regionName = reader.GetString();
+ }
+ }
+ }
+
+ if (string.IsNullOrEmpty(padding) || string.IsNullOrEmpty(regionName))
+ {
+ throw new JsonException($"Missing required properties \"Padding\" and \"TextureRegion\"");
+ }
+
+ var thickness = Thickness.Parse(padding);
+ var region = _textureRegionService.GetTextureRegion(regionName);
+
+ return new NinePatchRegion2D(region, thickness.Left, thickness.Top, thickness.Right, thickness.Bottom);
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, NinePatchRegion2D value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+
+ if (value is null)
+ {
+ writer.WriteNullValue();
+ return;
+ }
+
+ writer.WriteStartObject();
+ writer.WriteString("TextureRegion", value.Name);
+ writer.WriteString("Padding", value.Padding.ToString());
+ writer.WriteEndObject();
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RangeJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RangeJsonConverter.cs
new file mode 100644
index 0000000..ef5b5cb
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RangeJsonConverter.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Converts a <see cref="Range{T}"/> value to or from JSON.
+/// </summary>
+public class RangeJsonConverter<T> : JsonConverter<Range<T>> where T : IComparable<T>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Range<T>);
+
+ /// <inheritdoc />
+ public override Range<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ Span<T> values = reader.ReadAsMultiDimensional<T>();
+
+ if (values.Length == 2)
+ {
+ if (values[0].CompareTo(values[1]) < 0)
+ {
+ return new Range<T>(values[0], values[1]);
+ }
+
+ return new Range<T>(values[1], values[0]);
+ }
+
+ if (values.Length == 1)
+ {
+ return new Range<T>(values[0], values[0]);
+ }
+
+ throw new InvalidOperationException("Invalid range");
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, Range<T> value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteStartArray();
+ JsonSerializer.Serialize(writer, value.Min, options);
+ JsonSerializer.Serialize(writer, value.Max, options);
+ writer.WriteEndArray();
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RectangleFJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RectangleFJsonConverter.cs
new file mode 100644
index 0000000..080400a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RectangleFJsonConverter.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Converts a <see cref="RectangleF"/> value to or from JSON.
+/// </summary>
+public class RectangleFJsonConverter : JsonConverter<RectangleF>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(RectangleF);
+
+ /// <inheritdoc />
+ public override RectangleF Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var values = reader.ReadAsMultiDimensional<float>();
+ return new RectangleF(values[0], values[1], values[2], values[3]);
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, RectangleF value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteStringValue($"{value.Left} {value.Top} {value.Width} {value.Height}");
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Size2JsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Size2JsonConverter.cs
new file mode 100644
index 0000000..9eeb329
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Size2JsonConverter.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Converts a <see cref="Size2"/> value to or from JSON.
+/// </summary>
+public class Size2JsonConverter : JsonConverter<Size2>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Size2);
+
+ /// <inheritdoc />
+ /// <exception cref="JsonException">
+ /// Thrown if the JSON property does not contain a properly formatted <see cref="Size2"/> value
+ /// </exception>
+ public override Size2 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var values = reader.ReadAsMultiDimensional<float>();
+
+ if (values.Length == 2)
+ {
+ return new Size2(values[0], values[1]);
+ }
+
+ if (values.Length == 1)
+ {
+ return new Size2(values[0], values[0]);
+ }
+
+ throw new JsonException("Invalid Size2 property value");
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, Size2 value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteStringValue($"{value.Width} {value.Height}");
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/SizeJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/SizeJsonConverter.cs
new file mode 100644
index 0000000..3b454a1
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/SizeJsonConverter.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Converts a <see cref="Size"/> value to or from JSON.
+/// </summary>
+public class SizeJsonConverter : JsonConverter<Size>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Size);
+
+ /// <inheritdoc />
+ /// <exception cref="JsonException">
+ /// Thrown if the JSON property does not contain a properly formatted <see cref="Size"/> value
+ /// </exception>
+ public override Size Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var values = reader.ReadAsMultiDimensional<int>();
+
+ if (values.Length == 2)
+ {
+ return new Size(values[0], values[1]);
+ }
+
+ if (values.Length == 1)
+ {
+ return new Size(values[0], values[0]);
+ }
+
+ throw new JsonException("Invalid Size property value");
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, Size value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteStringValue($"{value.Width} {value.Height}");
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextContentLoader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextContentLoader.cs
new file mode 100644
index 0000000..273aaed
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextContentLoader.cs
@@ -0,0 +1,18 @@
+using System.IO;
+using Microsoft.Xna.Framework.Content;
+using MonoGame.Extended.Content;
+
+namespace MonoGame.Extended.Serialization
+{
+ public class TextContentLoader : IContentLoader<string>
+ {
+ public string Load(ContentManager contentManager, string path)
+ {
+ using (var stream = contentManager.OpenStream(path))
+ using (var reader = new StreamReader(stream))
+ {
+ return reader.ReadToEnd();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegion2DJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegion2DJsonConverter.cs
new file mode 100644
index 0000000..dbcb66c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegion2DJsonConverter.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Converts a <see cref="TextureRegion2D"/> value to or from JSON.
+/// </summary>
+public class TextureRegion2DJsonConverter : JsonConverter<TextureRegion2D>
+{
+ private readonly ITextureRegionService _textureRegionService;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TextureRegion2DJsonConverter"/> class.
+ /// </summary>
+ /// <param name="textureRegionService">The texture region service to use for retrieving texture regions.</param>
+ /// <exception cref="ArgumentNullException">
+ /// Thrown if <paramref name="textureRegionService"/> is <see langword="null"/>.
+ /// </exception>
+ public TextureRegion2DJsonConverter(ITextureRegionService textureRegionService)
+ {
+ ArgumentNullException.ThrowIfNull(textureRegionService);
+ _textureRegionService = textureRegionService;
+ }
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(TextureRegion2D);
+
+ /// <inheritdoc />
+ public override TextureRegion2D Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var regionName = reader.GetString();
+ return string.IsNullOrEmpty(regionName) ? null : _textureRegionService.GetTextureRegion(regionName);
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ ///
+ /// -or-
+ ///
+ /// Thrown if <paramref name="value"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, TextureRegion2D value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ ArgumentNullException.ThrowIfNull(value);
+ writer.WriteStringValue(value.Name);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegionService.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegionService.cs
new file mode 100644
index 0000000..3914730
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegionService.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Linq;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Serialization
+{
+ public interface ITextureRegionService
+ {
+ TextureRegion2D GetTextureRegion(string name);
+ }
+
+ public class TextureRegionService : ITextureRegionService
+ {
+ public TextureRegionService()
+ {
+ TextureAtlases = new List<TextureAtlas>();
+ NinePatches = new List<NinePatchRegion2D>();
+ }
+
+ public IList<TextureAtlas> TextureAtlases { get; }
+ public IList<NinePatchRegion2D> NinePatches { get; }
+
+ public TextureRegion2D GetTextureRegion(string name)
+ {
+ var ninePatch = NinePatches.FirstOrDefault(p => p.Name == name);
+
+ if (ninePatch != null)
+ return ninePatch;
+
+ return TextureAtlases
+ .Select(textureAtlas => textureAtlas.GetRegion(name))
+ .FirstOrDefault(region => region != null);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ThicknessJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ThicknessJsonConverter.cs
new file mode 100644
index 0000000..1a60573
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ThicknessJsonConverter.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Converts a <see cref="Thickness"/> value to or from JSON.
+/// </summary>
+public class ThicknessJsonConverter : JsonConverter<Thickness>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Thickness);
+
+ /// <inheritdoc />
+ public override Thickness Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var values = reader.ReadAsMultiDimensional<int>();
+ return Thickness.FromValues(values);
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, Thickness value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteStringValue($"{value.Left} {value.Top} {value.Right} {value.Bottom}");
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Utf8JsonReaderExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Utf8JsonReaderExtensions.cs
new file mode 100644
index 0000000..73ab532
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Utf8JsonReaderExtensions.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text.Json;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Provides extension methods for working with <see cref="Utf8JsonReader"/>.
+/// </summary>
+public static class Utf8JsonReaderExtensions
+{
+ private static readonly Dictionary<Type, Func<string, object>> s_stringParsers = new Dictionary<Type, Func<string, object>>
+ {
+ {typeof(int), s => int.Parse(s, CultureInfo.InvariantCulture.NumberFormat)},
+ {typeof(float), s => float.Parse(s, CultureInfo.InvariantCulture.NumberFormat)},
+ {typeof(HslColor), s => ColorExtensions.FromHex(s).ToHsl() }
+ };
+
+ /// <summary>
+ /// Reads a multi-dimensional JSON array and converts it to an array of the specified type.
+ /// </summary>
+ /// <typeparam name="T">The type of the array elements.</typeparam>
+ /// <param name="reader">The <see cref="Utf8JsonReader"/> to read from.</param>
+ /// <returns>An array of the specified type.</returns>
+ /// <exception cref="NotSupportedException">Thrown when the token type is not supported.</exception>
+ public static T[] ReadAsMultiDimensional<T>(this ref Utf8JsonReader reader)
+ {
+ var tokenType = reader.TokenType;
+
+ switch (tokenType)
+ {
+ case JsonTokenType.StartArray:
+ return reader.ReadAsJArray<T>();
+
+ case JsonTokenType.String:
+ return reader.ReadAsDelimitedString<T>();
+
+ case JsonTokenType.Number:
+ return reader.ReadAsSingleValue<T>();
+
+ default:
+ throw new NotSupportedException($"{tokenType} is not currently supported in the multi-dimensional parser");
+ }
+ }
+
+ private static T[] ReadAsSingleValue<T>(this ref Utf8JsonReader reader)
+ {
+ var token = JsonDocument.ParseValue(ref reader).RootElement;
+ var value = JsonSerializer.Deserialize<T>(token.GetRawText());
+ return new T[] { value };
+ }
+
+ private static T[] ReadAsJArray<T>(this ref Utf8JsonReader reader)
+ {
+ var items = new List<T>();
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
+ {
+ if (reader.TokenType == JsonTokenType.EndArray)
+ {
+ break;
+ }
+
+ items.Add(JsonSerializer.Deserialize<T>(ref reader));
+ }
+
+ return items.ToArray();
+ }
+
+ private static T[] ReadAsDelimitedString<T>(this ref Utf8JsonReader reader)
+ {
+ var value = reader.GetString();
+ if (string.IsNullOrEmpty(value))
+ {
+ return Array.Empty<T>();
+ }
+
+ Span<string> values = value.Split(' ');
+ var result = new T[values.Length];
+ var parser = s_stringParsers[typeof(T)];
+
+ for (int i = 0; i < values.Length; i++)
+ {
+ result[i] = (T)parser(values[i]);
+ }
+
+ return result;
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Vector2JsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Vector2JsonConverter.cs
new file mode 100644
index 0000000..e10c72b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Vector2JsonConverter.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Serialization;
+
+/// <summary>
+/// Converts a <see cref="Vector2"/> value to or from JSON.
+/// </summary>
+public class Vector2JsonConverter : JsonConverter<Vector2>
+{
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Vector2);
+
+ /// <inheritdoc />
+ /// <exception cref="JsonException">
+ /// Thrown if the JSON property does not contain a properly formatted <see cref="Vector2"/> value
+ /// </exception>
+ public override Vector2 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var values = reader.ReadAsMultiDimensional<float>();
+
+ if (values.Length == 2)
+ {
+ return new Vector2(values[0], values[1]);
+ }
+
+ if (values.Length == 1)
+ {
+ return new Vector2(values[0]);
+ }
+
+ throw new JsonException("Invalid Size2 property value");
+ }
+
+ /// <inheritdoc />
+ /// <exception cref="ArgumentNullException">
+ /// Throw if <paramref name="writer"/> is <see langword="null"/>.
+ /// </exception>
+ public override void Write(Utf8JsonWriter writer, Vector2 value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteStringValue($"{value.X} {value.Y}");
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polygon.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polygon.cs
new file mode 100644
index 0000000..841ce2b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polygon.cs
@@ -0,0 +1,179 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Shapes
+{
+ public class Polygon : IEquatable<Polygon>
+ {
+ public Polygon(IEnumerable<Vector2> vertices)
+ {
+ _localVertices = vertices.ToArray();
+ _transformedVertices = _localVertices;
+ _offset = Vector2.Zero;
+ _rotation = 0;
+ _scale = Vector2.One;
+ _isDirty = false;
+ }
+
+ private readonly Vector2[] _localVertices;
+ private Vector2[] _transformedVertices;
+ private Vector2 _offset;
+ private float _rotation;
+ private Vector2 _scale;
+ private bool _isDirty;
+
+ public Vector2[] Vertices
+ {
+ get
+ {
+ if (_isDirty)
+ {
+ _transformedVertices = GetTransformedVertices();
+ _isDirty = false;
+ }
+
+ return _transformedVertices;
+ }
+ }
+
+ public float Left
+ {
+ get { return Vertices.Min(v => v.X); }
+ }
+
+ public float Right
+ {
+ get { return Vertices.Max(v => v.X); }
+ }
+
+ public float Top
+ {
+ get { return Vertices.Min(v => v.Y); }
+ }
+
+ public float Bottom
+ {
+ get { return Vertices.Max(v => v.Y); }
+ }
+
+ public void Offset(Vector2 amount)
+ {
+ _offset += amount;
+ _isDirty = true;
+ }
+
+ public void Rotate(float amount)
+ {
+ _rotation += amount;
+ _isDirty = true;
+ }
+
+ public void Scale(Vector2 amount)
+ {
+ _scale += amount;
+ _isDirty = true;
+ }
+
+ private Vector2[] GetTransformedVertices()
+ {
+ var newVertices = new Vector2[_localVertices.Length];
+ var isScaled = _scale != Vector2.One;
+
+ for (var i = 0; i < _localVertices.Length; i++)
+ {
+ var p = _localVertices[i];
+
+ if (isScaled)
+ p *= _scale;
+
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if (_rotation != 0)
+ {
+ var cos = (float) Math.Cos(_rotation);
+ var sin = (float) Math.Sin(_rotation);
+ p = new Vector2(cos*p.X - sin*p.Y, sin*p.X + cos*p.Y);
+ }
+
+ newVertices[i] = p + _offset;
+ }
+
+ return newVertices;
+ }
+
+ public Polygon TransformedCopy(Vector2 offset, float rotation, Vector2 scale)
+ {
+ var polygon = new Polygon(_localVertices);
+ polygon.Offset(offset);
+ polygon.Rotate(rotation);
+ polygon.Scale(scale - Vector2.One);
+ return new Polygon(polygon.Vertices);
+ }
+
+ public RectangleF BoundingRectangle
+ {
+ get
+ {
+ var minX = Left;
+ var minY = Top;
+ var maxX = Right;
+ var maxY = Bottom;
+ return new RectangleF(minX, minY, maxX - minX, maxY - minY);
+ }
+ }
+
+ public bool Contains(Vector2 point)
+ {
+ return Contains(point.X, point.Y);
+ }
+
+ public bool Contains(float x, float y)
+ {
+ var intersects = 0;
+ var vertices = Vertices;
+
+ for (var i = 0; i < vertices.Length; i++)
+ {
+ var x1 = vertices[i].X;
+ var y1 = vertices[i].Y;
+ var x2 = vertices[(i + 1)%vertices.Length].X;
+ var y2 = vertices[(i + 1)%vertices.Length].Y;
+
+ if ((((y1 <= y) && (y < y2)) || ((y2 <= y) && (y < y1))) && (x < (x2 - x1)/(y2 - y1)*(y - y1) + x1))
+ intersects++;
+ }
+
+ return (intersects & 1) == 1;
+ }
+
+ public static bool operator ==(Polygon a, Polygon b)
+ {
+ return a.Equals(b);
+ }
+
+ public static bool operator !=(Polygon a, Polygon b)
+ {
+ return !(a == b);
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ return obj is Polygon && Equals((Polygon) obj);
+ }
+
+ public bool Equals(Polygon other)
+ {
+ return Vertices.SequenceEqual(other.Vertices);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return Vertices.Aggregate(27, (current, v) => current + 13*current + v.GetHashCode());
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polyline.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polyline.cs
new file mode 100644
index 0000000..97b34ff
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polyline.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Shapes
+{
+ public class Polyline
+ {
+ public Polyline(IEnumerable<Vector2> points)
+ {
+ Points = points;
+ }
+
+ public IEnumerable<Vector2> Points { get; private set; }
+ public float Left => Points.Min(p => p.X);
+ public float Top => Points.Min(p => p.Y);
+ public float Right => Points.Max(p => p.X);
+ public float Bottom => Points.Max(p => p.Y);
+
+ public RectangleF BoundingRectangle
+ {
+ get
+ {
+ var minX = Left;
+ var minY = Top;
+ var maxX = Right;
+ var maxY = Bottom;
+ return new RectangleF(minX, minY, maxX - minX, maxY - minY);
+ }
+ }
+
+ public bool Contains(float x, float y)
+ {
+ return false;
+ }
+
+ public bool Contains(Vector2 point)
+ {
+ return Contains(point.X, point.Y);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleDrawableGameComponent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleDrawableGameComponent.cs
new file mode 100644
index 0000000..5487c64
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleDrawableGameComponent.cs
@@ -0,0 +1,47 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public abstract class SimpleDrawableGameComponent : SimpleGameComponent, IDrawable
+ {
+ protected SimpleDrawableGameComponent()
+ {
+ }
+
+ private bool _isVisible = true;
+ public bool Visible
+ {
+ get => _isVisible;
+ set
+ {
+ if (_isVisible == value)
+ return;
+
+ _isVisible = value;
+ VisibleChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ bool IDrawable.Visible => _isVisible;
+
+ private int _drawOrder;
+ public int DrawOrder
+ {
+ get => _drawOrder;
+ set
+ {
+ if (_drawOrder == value)
+ return;
+
+ _drawOrder = value;
+ DrawOrderChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ public event EventHandler<EventArgs> DrawOrderChanged;
+ public event EventHandler<EventArgs> VisibleChanged;
+
+ public abstract void Draw(GameTime gameTime);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleGameComponent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleGameComponent.cs
new file mode 100644
index 0000000..57cccfc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleGameComponent.cs
@@ -0,0 +1,80 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ public abstract class SimpleGameComponent : IGameComponent, IUpdateable, IDisposable, IComparable<GameComponent>, IComparable<SimpleGameComponent>
+ {
+ private bool _isInitialized;
+
+ protected SimpleGameComponent()
+ {
+ }
+
+ public virtual void Dispose()
+ {
+ if (_isInitialized)
+ {
+ UnloadContent();
+ _isInitialized = false;
+ }
+ }
+
+ private bool _isEnabled = true;
+ public bool IsEnabled
+ {
+ get => _isEnabled;
+ set
+ {
+ if (_isEnabled == value)
+ return;
+
+ _isEnabled = value;
+ EnabledChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ public virtual void Initialize()
+ {
+ if (!_isInitialized)
+ {
+ LoadContent();
+ _isInitialized = true;
+ }
+ }
+
+ protected virtual void LoadContent() { }
+ protected virtual void UnloadContent() { }
+
+ bool IUpdateable.Enabled => _isEnabled;
+
+ private int _updateOrder;
+ public int UpdateOrder
+ {
+ get => _updateOrder;
+ set
+ {
+ if (_updateOrder == value)
+ return;
+
+ _updateOrder = value;
+ UpdateOrderChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ public event EventHandler<EventArgs> EnabledChanged;
+ public event EventHandler<EventArgs> UpdateOrderChanged;
+
+ public abstract void Update(GameTime gameTime);
+
+ public int CompareTo(GameComponent other)
+ {
+ return other.UpdateOrder - UpdateOrder;
+ }
+
+ public int CompareTo(SimpleGameComponent other)
+ {
+ return other.UpdateOrder - UpdateOrder;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/AnimatedSprite.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/AnimatedSprite.cs
new file mode 100644
index 0000000..a6bf393
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/AnimatedSprite.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Linq;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Sprites
+{
+ public class AnimatedSprite : Sprite
+ {
+ private readonly SpriteSheet _spriteSheet;
+ private SpriteSheetAnimation _currentAnimation;
+
+ public AnimatedSprite(SpriteSheet spriteSheet, string playAnimation = null)
+ : base(spriteSheet.TextureAtlas[0])
+ {
+ _spriteSheet = spriteSheet;
+
+ if (playAnimation != null)
+ Play(playAnimation);
+ }
+
+ public SpriteSheetAnimation Play(string name, Action onCompleted = null)
+ {
+ if (_currentAnimation == null || _currentAnimation.IsComplete || _currentAnimation.Name != name)
+ {
+ var cycle = _spriteSheet.Cycles[name];
+ var keyFrames = cycle.Frames.Select(f => _spriteSheet.TextureAtlas[f.Index]).ToArray();
+ _currentAnimation = new SpriteSheetAnimation(name, keyFrames, cycle.FrameDuration, cycle.IsLooping, cycle.IsReversed, cycle.IsPingPong);
+
+ if(_currentAnimation != null)
+ _currentAnimation.OnCompleted = onCompleted;
+ }
+
+ return _currentAnimation;
+ }
+
+ public void Update(float deltaTime)
+ {
+ if (_currentAnimation != null && !_currentAnimation.IsComplete)
+ {
+ _currentAnimation.Update(deltaTime);
+ TextureRegion = _currentAnimation.CurrentFrame;
+ }
+ }
+
+ public void Update(GameTime gameTime)
+ {
+ Update(gameTime.GetElapsedSeconds());
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Animation.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Animation.cs
new file mode 100644
index 0000000..cd6e092
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Animation.cs
@@ -0,0 +1,86 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Sprites
+{
+ public abstract class Animation : IUpdate, IDisposable
+ {
+ private readonly bool _disposeOnComplete;
+ private readonly Action _onCompleteAction;
+ private bool _isComplete;
+
+ protected Animation(Action onCompleteAction, bool disposeOnComplete)
+ {
+ _onCompleteAction = onCompleteAction;
+ _disposeOnComplete = disposeOnComplete;
+ IsPaused = false;
+ }
+
+ public bool IsComplete
+ {
+ get { return _isComplete; }
+ protected set
+ {
+ if (_isComplete != value)
+ {
+ _isComplete = value;
+
+ if (_isComplete)
+ {
+ _onCompleteAction?.Invoke();
+
+ if (_disposeOnComplete)
+ Dispose();
+ }
+ }
+ }
+ }
+
+ public bool IsDisposed { get; private set; }
+ public bool IsPlaying => !IsPaused && !IsComplete;
+ public bool IsPaused { get; private set; }
+ public float CurrentTime { get; protected set; }
+
+ public virtual void Dispose()
+ {
+ IsDisposed = true;
+ }
+
+ public void Update(GameTime gameTime)
+ {
+ Update(gameTime.GetElapsedSeconds());
+ }
+
+ public void Play()
+ {
+ IsPaused = false;
+ }
+
+ public void Pause()
+ {
+ IsPaused = true;
+ }
+
+ public void Stop()
+ {
+ Pause();
+ Rewind();
+ }
+
+ public void Rewind()
+ {
+ CurrentTime = 0;
+ }
+
+ protected abstract bool OnUpdate(float deltaTime);
+
+ public void Update(float deltaTime)
+ {
+ if (!IsPlaying)
+ return;
+
+ CurrentTime += deltaTime;
+ IsComplete = OnUpdate(deltaTime);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/ISpriteBatchDrawable.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/ISpriteBatchDrawable.cs
new file mode 100644
index 0000000..ab31417
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/ISpriteBatchDrawable.cs
@@ -0,0 +1,18 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Sprites
+{
+ public interface ISpriteBatchDrawable
+ {
+ bool IsVisible { get; }
+ TextureRegion2D TextureRegion { get; }
+ Vector2 Position { get; }
+ float Rotation { get; }
+ Vector2 Scale { get; }
+ Color Color { get; }
+ Vector2 Origin { get; }
+ SpriteEffects Effect { get; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Sprite.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Sprite.cs
new file mode 100644
index 0000000..2174c8a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Sprite.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Sprites
+{
+ public class Sprite : IColorable
+ {
+ private TextureRegion2D _textureRegion;
+
+ public Sprite(TextureRegion2D textureRegion)
+ {
+ if (textureRegion == null) throw new ArgumentNullException(nameof(textureRegion));
+
+ _textureRegion = textureRegion;
+
+ Alpha = 1.0f;
+ Color = Color.White;
+ IsVisible = true;
+ Effect = SpriteEffects.None;
+ OriginNormalized = new Vector2(0.5f, 0.5f);
+ Depth = 0.0f;
+ }
+
+ public Sprite(Texture2D texture)
+ : this(new TextureRegion2D(texture))
+ {
+ }
+
+ public float Alpha { get; set; }
+ public float Depth { get; set; }
+ public object Tag { get; set; }
+
+ public Vector2 OriginNormalized
+ {
+ get => new Vector2(Origin.X/TextureRegion.Width, Origin.Y/TextureRegion.Height);
+ set => Origin = new Vector2(value.X*TextureRegion.Width, value.Y*TextureRegion.Height);
+ }
+
+ public Color Color { get; set; }
+
+ public RectangleF GetBoundingRectangle(Transform2 transform)
+ {
+ return GetBoundingRectangle(transform.Position, transform.Rotation, transform.Scale);
+ }
+
+ public RectangleF GetBoundingRectangle(Vector2 position, float rotation, Vector2 scale)
+ {
+ var corners = GetCorners(position, rotation, scale);
+ var min = new Vector2(corners.Min(i => i.X), corners.Min(i => i.Y));
+ var max = new Vector2(corners.Max(i => i.X), corners.Max(i => i.Y));
+ return new RectangleF(min.X, min.Y, max.X - min.X, max.Y - min.Y);
+ }
+
+ public bool IsVisible { get; set; }
+ public Vector2 Origin { get; set; }
+ public SpriteEffects Effect { get; set; }
+
+ public TextureRegion2D TextureRegion
+ {
+ get => _textureRegion;
+ set
+ {
+ if (value == null)
+ throw new InvalidOperationException("TextureRegion cannot be null");
+
+ // preserve the origin if the texture size changes
+ var originNormalized = OriginNormalized;
+ _textureRegion = value;
+ OriginNormalized = originNormalized;
+ }
+ }
+
+ public Vector2[] GetCorners(Vector2 position, float rotation, Vector2 scale)
+ {
+ var min = -Origin;
+ var max = min + new Vector2(TextureRegion.Width, TextureRegion.Height);
+ var offset = position;
+
+ if (scale != Vector2.One)
+ {
+ min *= scale;
+ max = max * scale;
+ }
+
+ var corners = new Vector2[4];
+ corners[0] = min;
+ corners[1] = new Vector2(max.X, min.Y);
+ corners[2] = max;
+ corners[3] = new Vector2(min.X, max.Y);
+
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if (rotation != 0)
+ {
+ var matrix = Matrix.CreateRotationZ(rotation);
+
+ for (var i = 0; i < 4; i++)
+ corners[i] = Vector2.Transform(corners[i], matrix);
+ }
+
+ for (var i = 0; i < 4; i++)
+ corners[i] += offset;
+
+ return corners;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteExtensions.cs
new file mode 100644
index 0000000..983065b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteExtensions.cs
@@ -0,0 +1,36 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Sprites
+{
+ public static class SpriteExtensions
+ {
+ public static void Draw(this Sprite sprite, SpriteBatch spriteBatch, Vector2 position, float rotation, Vector2 scale)
+ {
+ Draw(spriteBatch, sprite, position, rotation, scale);
+ }
+
+ public static void Draw(this SpriteBatch spriteBatch, Sprite sprite, Transform2 transform)
+ {
+ Draw(spriteBatch, sprite, transform.Position, transform.Rotation, transform.Scale);
+ }
+
+ public static void Draw(this SpriteBatch spriteBatch, Sprite sprite, Vector2 position, float rotation = 0)
+ {
+ Draw(spriteBatch, sprite, position, rotation, Vector2.One);
+ }
+
+ public static void Draw(this SpriteBatch spriteBatch, Sprite sprite, Vector2 position, float rotation, Vector2 scale)
+ {
+ if (sprite == null) throw new ArgumentNullException(nameof(sprite));
+
+ if (sprite.IsVisible)
+ {
+ var texture = sprite.TextureRegion.Texture;
+ var sourceRectangle = sprite.TextureRegion.Bounds;
+ spriteBatch.Draw(texture, position, sourceRectangle, sprite.Color*sprite.Alpha, rotation, sprite.Origin, scale, sprite.Effect, sprite.Depth);
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheet.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheet.cs
new file mode 100644
index 0000000..6a8acb1
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheet.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+using System.Linq;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Sprites
+{
+ public class SpriteSheet
+ {
+ public SpriteSheet()
+ {
+ Cycles = new Dictionary<string, SpriteSheetAnimationCycle>();
+ }
+
+ public TextureAtlas TextureAtlas { get; set; }
+ public Dictionary<string, SpriteSheetAnimationCycle> Cycles { get; set; }
+
+ public SpriteSheetAnimation CreateAnimation(string name)
+ {
+ var cycle = Cycles[name];
+ var keyFrames = cycle.Frames
+ .Select(f => TextureAtlas[f.Index])
+ .ToArray();
+
+ return new SpriteSheetAnimation(name, keyFrames, cycle.FrameDuration, cycle.IsLooping, cycle.IsReversed, cycle.IsPingPong);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimation.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimation.cs
new file mode 100644
index 0000000..3b60e5e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimation.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Linq;
+using MonoGame.Extended.TextureAtlases;
+
+namespace MonoGame.Extended.Sprites
+{
+ public class SpriteSheetAnimation : Animation
+ {
+ public const float DefaultFrameDuration = 0.2f;
+
+ public SpriteSheetAnimation(string name, TextureAtlas textureAtlas, float frameDuration = DefaultFrameDuration,
+ bool isLooping = true, bool isReversed = false, bool isPingPong = false)
+ : this(name, textureAtlas.Regions.ToArray(), frameDuration, isLooping, isReversed, isPingPong)
+ {
+ }
+
+ public SpriteSheetAnimation(string name, TextureRegion2D[] keyFrames, float frameDuration = DefaultFrameDuration,
+ bool isLooping = true, bool isReversed = false, bool isPingPong = false)
+ : base(null, false)
+ {
+ Name = name;
+ KeyFrames = keyFrames;
+ FrameDuration = frameDuration;
+ IsLooping = isLooping;
+ IsReversed = isReversed;
+ IsPingPong = isPingPong;
+ CurrentFrameIndex = IsReversed ? KeyFrames.Length - 1 : 0;
+ }
+
+ public SpriteSheetAnimation(string name, TextureRegion2D[] keyFrames, SpriteSheetAnimationData data)
+ : this(name, keyFrames, data.FrameDuration, data.IsLooping, data.IsReversed, data.IsPingPong)
+ {
+ }
+
+ public string Name { get; }
+ public TextureRegion2D[] KeyFrames { get; }
+ public float FrameDuration { get; set; }
+ public bool IsLooping { get; set; }
+ public bool IsReversed { get; set; }
+ public bool IsPingPong { get; set; }
+ public new bool IsComplete => CurrentTime >= AnimationDuration;
+
+ public float AnimationDuration => IsPingPong
+ ? (KeyFrames.Length*2 - (IsLooping ? 2 : 1))*FrameDuration
+ : KeyFrames.Length*FrameDuration;
+
+ public TextureRegion2D CurrentFrame => KeyFrames[CurrentFrameIndex];
+ public int CurrentFrameIndex { get; private set; }
+
+ public float FramesPerSecond
+ {
+ get => 1.0f/FrameDuration;
+ set => FrameDuration = value/1.0f;
+ }
+
+ public Action OnCompleted { get; set; }
+
+ protected override bool OnUpdate(float deltaTime)
+ {
+ if (IsComplete)
+ {
+ if (IsLooping)
+ CurrentTime %= AnimationDuration;
+ else
+ OnCompleted?.Invoke();
+ }
+
+ if (KeyFrames.Length == 1)
+ {
+ CurrentFrameIndex = 0;
+ return IsComplete;
+ }
+
+ var frameIndex = (int) (CurrentTime/FrameDuration);
+ var length = KeyFrames.Length;
+
+ if (IsPingPong)
+ {
+ if (IsComplete)
+ frameIndex = 0;
+ else
+ {
+ frameIndex = frameIndex % (length * 2 - 2);
+
+ if (frameIndex >= length)
+ frameIndex = length - 2 - (frameIndex - length);
+ }
+ }
+
+ if (IsLooping)
+ {
+ if (IsReversed)
+ {
+ frameIndex = frameIndex%length;
+ frameIndex = length - frameIndex - 1;
+ }
+ else
+ frameIndex = frameIndex%length;
+ }
+ else
+ frameIndex = IsReversed ? Math.Max(length - frameIndex - 1, 0) : Math.Min(length - 1, frameIndex);
+
+ CurrentFrameIndex = frameIndex;
+ return IsComplete;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationCycle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationCycle.cs
new file mode 100644
index 0000000..d9e8674
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationCycle.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Sprites
+{
+ public class SpriteSheetAnimationCycle
+ {
+ public SpriteSheetAnimationCycle()
+ {
+ Frames = new List<SpriteSheetAnimationFrame>();
+ }
+
+ public float FrameDuration { get; set; } = 0.2f;
+ public List<SpriteSheetAnimationFrame> Frames { get; set; }
+ public bool IsLooping { get; set; }
+ public bool IsReversed { get; set; }
+ public bool IsPingPong { get; set; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationData.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationData.cs
new file mode 100644
index 0000000..0bdf89d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationData.cs
@@ -0,0 +1,21 @@
+namespace MonoGame.Extended.Sprites
+{
+ public class SpriteSheetAnimationData
+ {
+ public SpriteSheetAnimationData(int[] frameIndicies, float frameDuration = 0.2f, bool isLooping = true,
+ bool isReversed = false, bool isPingPong = false)
+ {
+ FrameIndicies = frameIndicies;
+ FrameDuration = frameDuration;
+ IsLooping = isLooping;
+ IsReversed = isReversed;
+ IsPingPong = isPingPong;
+ }
+
+ public int[] FrameIndicies { get; }
+ public float FrameDuration { get; }
+ public bool IsLooping { get; }
+ public bool IsReversed { get; }
+ public bool IsPingPong { get; }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationFrame.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationFrame.cs
new file mode 100644
index 0000000..2de8e39
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationFrame.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Diagnostics;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+
+namespace MonoGame.Extended.Sprites
+{
+ [JsonConverter(typeof(SpriteSheetAnimationFrameJsonConverter))]
+ [DebuggerDisplay("{Index} {Duration}")]
+ public class SpriteSheetAnimationFrame
+ {
+ public SpriteSheetAnimationFrame(int index, float duration = 0.2f)
+ {
+ Index = index;
+ Duration = duration;
+ }
+
+ public int Index { get; set; }
+ public float Duration { get; set; }
+ }
+
+ public class SpriteSheetAnimationFrameJsonConverter : JsonConverter<SpriteSheetAnimationFrame>
+ {
+ /// <inheritdoc />
+ public override SpriteSheetAnimationFrame Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ switch (reader.TokenType)
+ {
+ case JsonTokenType.Number:
+ var index = reader.GetInt32();
+ return new SpriteSheetAnimationFrame(index);
+
+ case JsonTokenType.StartObject:
+ var frame = JsonSerializer.Deserialize<SpriteSheetAnimationFrame>(ref reader, options);
+ return frame;
+
+ case JsonTokenType.Null:
+ return null;
+
+ default:
+ throw new JsonException();
+ }
+ }
+
+ public override void Write(Utf8JsonWriter writer, SpriteSheetAnimationFrame value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ JsonSerializer.Serialize(writer, value, options);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/NinePatchRegion2D.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/NinePatchRegion2D.cs
new file mode 100644
index 0000000..49323e0
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/NinePatchRegion2D.cs
@@ -0,0 +1,84 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class NinePatchRegion2D : TextureRegion2D
+ {
+ public Rectangle[] SourcePatches { get; } = new Rectangle[9];
+ public Thickness Padding { get; }
+ public int LeftPadding => Padding.Left;
+ public int TopPadding => Padding.Top;
+ public int RightPadding => Padding.Right;
+ public int BottomPadding => Padding.Bottom;
+
+ public NinePatchRegion2D(TextureRegion2D textureRegion, Thickness padding)
+ : base(textureRegion.Name, textureRegion.Texture, textureRegion.X, textureRegion.Y, textureRegion.Width, textureRegion.Height)
+ {
+ Padding = padding;
+ CachePatches(textureRegion.Bounds, SourcePatches);
+ }
+
+ public NinePatchRegion2D(TextureRegion2D textureRegion, int padding)
+ : this(textureRegion, padding, padding, padding, padding)
+ {
+ }
+
+ public NinePatchRegion2D(TextureRegion2D textureRegion, int leftRightPadding, int topBottomPadding)
+ : this(textureRegion, leftRightPadding, topBottomPadding, leftRightPadding, topBottomPadding)
+ {
+ }
+
+ public NinePatchRegion2D(TextureRegion2D textureRegion, int leftPadding, int topPadding, int rightPadding, int bottomPadding)
+ : this(textureRegion, new Thickness(leftPadding, topPadding, rightPadding, bottomPadding))
+ {
+ }
+
+ public NinePatchRegion2D(Texture2D texture, Thickness thickness)
+ : this(new TextureRegion2D(texture), thickness)
+ {
+ }
+
+ public const int TopLeft = 0;
+ public const int TopMiddle = 1;
+ public const int TopRight = 2;
+ public const int MiddleLeft = 3;
+ public const int Middle = 4;
+ public const int MiddleRight = 5;
+ public const int BottomLeft = 6;
+ public const int BottomMiddle = 7;
+ public const int BottomRight = 8;
+
+ private readonly Rectangle[] _destinationPatches = new Rectangle[9];
+
+ public Rectangle[] CreatePatches(Rectangle rectangle)
+ {
+ CachePatches(rectangle, _destinationPatches);
+ return _destinationPatches;
+ }
+
+ private void CachePatches(Rectangle sourceRectangle, Rectangle[] patchCache)
+ {
+ var x = sourceRectangle.X;
+ var y = sourceRectangle.Y;
+ var w = sourceRectangle.Width;
+ var h = sourceRectangle.Height;
+ var middleWidth = w - LeftPadding - RightPadding;
+ var middleHeight = h - TopPadding - BottomPadding;
+ var bottomY = y + h - BottomPadding;
+ var rightX = x + w - RightPadding;
+ var leftX = x + LeftPadding;
+ var topY = y + TopPadding;
+
+ patchCache[TopLeft] = new Rectangle(x, y, LeftPadding, TopPadding);
+ patchCache[TopMiddle] = new Rectangle(leftX, y, middleWidth, TopPadding);
+ patchCache[TopRight] = new Rectangle(rightX, y, RightPadding, TopPadding);
+ patchCache[MiddleLeft] = new Rectangle(x, topY, LeftPadding, middleHeight);
+ patchCache[Middle] = new Rectangle(leftX, topY, middleWidth, middleHeight);
+ patchCache[MiddleRight] = new Rectangle(rightX, topY, RightPadding, middleHeight);
+ patchCache[BottomLeft] = new Rectangle(x, bottomY, LeftPadding, BottomPadding);
+ patchCache[BottomMiddle] = new Rectangle(leftX, bottomY, middleWidth, BottomPadding);
+ patchCache[BottomRight] = new Rectangle(rightX, bottomY, RightPadding, BottomPadding);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlas.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlas.cs
new file mode 100644
index 0000000..d9fd41d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlas.cs
@@ -0,0 +1,258 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ /// <summary>
+ /// Defines a texture atlas which stores a source image and contains regions specifying its sub-images.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// Texture atlas (also called a tile map, tile engine, or sprite sheet) is a large image containing a collection,
+ /// or "atlas", of sub-images, each of which is a texture map for some part of a 2D or 3D model.
+ /// The sub-textures can be rendered by modifying the texture coordinates of the object's uvmap on the atlas,
+ /// essentially telling it which part of the image its texture is in.
+ /// In an application where many small textures are used frequently, it is often more efficient to store the
+ /// textures in a texture atlas which is treated as a single unit by the graphics hardware.
+ /// This saves memory and because there are less rendering state changes by binding once, it can be faster to bind
+ /// one large texture once than to bind many smaller textures as they are drawn.
+ /// Careful alignment may be needed to avoid bleeding between sub textures when used with mipmapping, and artefacts
+ /// between tiles for texture compression.
+ /// </para>
+ /// </remarks>
+ public class TextureAtlas : IEnumerable<TextureRegion2D>
+ {
+ /// <summary>
+ /// Initializes a new texture atlas with an empty list of regions.
+ /// </summary>
+ /// <param name="name">The asset name of this texture atlas</param>
+ /// <param name="texture">Source <see cref="Texture2D " /> image used to draw on screen.</param>
+ public TextureAtlas(string name, Texture2D texture)
+ {
+ Name = name;
+ Texture = texture;
+
+ _regionsByName = new Dictionary<string, TextureRegion2D>();
+ _regionsByIndex = new List<TextureRegion2D>();
+ }
+
+ /// <inheritdoc />
+ /// <summary>
+ /// Initializes a new texture atlas and populates it with regions.
+ /// </summary>
+ /// <param name="name">The asset name of this texture atlas</param>
+ /// <param name="texture">Source <see cref="!:Texture2D " /> image used to draw on screen.</param>
+ /// <param name="regions">A collection of regions to populate the atlas with.</param>
+ public TextureAtlas(string name, Texture2D texture, Dictionary<string, Rectangle> regions)
+ : this(name, texture)
+ {
+ foreach (var region in regions)
+ CreateRegion(region.Key, region.Value.X, region.Value.Y, region.Value.Width, region.Value.Height);
+ }
+
+ private readonly Dictionary<string, TextureRegion2D> _regionsByName;
+ private readonly List<TextureRegion2D> _regionsByIndex;
+
+ public string Name { get; }
+
+ /// <summary>
+ /// Gets a source <see cref="Texture2D" /> image.
+ /// </summary>
+ public Texture2D Texture { get; }
+
+ /// <summary>
+ /// Gets a list of regions in the <see cref="TextureAtlas" />.
+ /// </summary>
+ public IEnumerable<TextureRegion2D> Regions => _regionsByIndex;
+
+ /// <summary>
+ /// Gets the number of regions in the <see cref="TextureAtlas" />.
+ /// </summary>
+ public int RegionCount => _regionsByIndex.Count;
+
+ public TextureRegion2D this[string name] => GetRegion(name);
+ public TextureRegion2D this[int index] => GetRegion(index);
+
+ /// <summary>
+ /// Gets the enumerator of the <see cref="TextureAtlas" />' list of regions.
+ /// </summary>
+ /// <returns>The <see cref="IEnumerator" /> of regions.</returns>
+ public IEnumerator<TextureRegion2D> GetEnumerator()
+ {
+ return _regionsByIndex.GetEnumerator();
+ }
+
+ /// <summary>
+ /// Gets the enumerator of the <see cref="TextureAtlas" />' list of regions.
+ /// </summary>
+ /// <returns>The <see cref="IEnumerator" /> of regions</returns>
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ /// <summary>
+ /// Determines whether the texture atlas contains a region
+ /// </summary>
+ /// <param name="name">Name of the texture region.</param>
+ /// <returns></returns>
+ public bool ContainsRegion(string name)
+ {
+ return _regionsByName.ContainsKey(name);
+ }
+
+ /// <summary>
+ /// Internal method for adding region
+ /// </summary>
+ /// <param name="region">Texture region.</param>
+ private void AddRegion(TextureRegion2D region)
+ {
+ _regionsByIndex.Add(region);
+ _regionsByName.Add(region.Name, region);
+ }
+
+ /// <summary>
+ /// Creates a new texture region and adds it to the list of the <see cref="TextureAtlas" />' regions.
+ /// </summary>
+ /// <param name="name">Name of the texture region.</param>
+ /// <param name="x">X coordinate of the region's top left corner.</param>
+ /// <param name="y">Y coordinate of the region's top left corner.</param>
+ /// <param name="width">Width of the texture region.</param>
+ /// <param name="height">Height of the texture region.</param>
+ /// <returns>Created texture region.</returns>
+ public TextureRegion2D CreateRegion(string name, int x, int y, int width, int height)
+ {
+ if (_regionsByName.ContainsKey(name))
+ throw new InvalidOperationException($"Region {name} already exists in the texture atlas");
+
+ var region = new TextureRegion2D(name, Texture, x, y, width, height);
+ AddRegion(region);
+ return region;
+ }
+
+ /// <summary>
+ /// Creates a new nine patch texture region and adds it to the list of the <see cref="TextureAtlas" />' regions.
+ /// </summary>
+ /// <param name="name">Name of the texture region.</param>
+ /// <param name="x">X coordinate of the region's top left corner.</param>
+ /// <param name="y">Y coordinate of the region's top left corner.</param>
+ /// <param name="width">Width of the texture region.</param>
+ /// <param name="height">Height of the texture region.</param>
+ /// <param name="thickness">Thickness of the nine patch region.</param>
+ /// <returns>Created texture region.</returns>
+ public NinePatchRegion2D CreateNinePatchRegion(string name, int x, int y, int width, int height, Thickness thickness)
+ {
+ if (_regionsByName.ContainsKey(name))
+ throw new InvalidOperationException($"Region {name} already exists in the texture atlas");
+
+ var textureRegion = new TextureRegion2D(name, Texture, x, y, width, height);
+ var ninePatchRegion = new NinePatchRegion2D(textureRegion, thickness);
+ AddRegion(ninePatchRegion);
+ return ninePatchRegion;
+ }
+
+ /// <summary>
+ /// Removes a texture region from the <see cref="TextureAtlas" />
+ /// </summary>
+ /// <param name="index">An index of the <see cref="TextureRegion2D" /> in <see cref="Region" /> to remove</param>
+ public void RemoveRegion(int index)
+ {
+ var region = _regionsByIndex[index];
+ _regionsByIndex.RemoveAt(index);
+
+ if(region.Name != null)
+ _regionsByName.Remove(region.Name);
+ }
+
+ /// <summary>
+ /// Removes a texture region from the <see cref="TextureAtlas" />
+ /// </summary>
+ /// <param name="name">Name of the <see cref="TextureRegion2D" /> to remove</param>
+ public void RemoveRegion(string name)
+ {
+ if (_regionsByName.TryGetValue(name, out var region))
+ {
+ _regionsByName.Remove(name);
+ _regionsByIndex.Remove(region);
+ }
+ }
+
+ /// <summary>
+ /// Gets a <see cref="TextureRegion2D" /> from the <see cref="TextureAtlas" />' list.
+ /// </summary>
+ /// <param name="index">An index of the <see cref="TextureRegion2D" /> in <see cref="Region" /> to get.</param>
+ /// <returns>The <see cref="TextureRegion2D" />.</returns>
+ public TextureRegion2D GetRegion(int index)
+ {
+ if (index < 0 || index >= _regionsByIndex.Count)
+ throw new IndexOutOfRangeException();
+
+ return _regionsByIndex[index];
+ }
+
+ /// <summary>
+ /// Gets a <see cref="TextureRegion2D" /> from the <see cref="TextureAtlas" />' list.
+ /// </summary>
+ /// <param name="name">Name of the <see cref="TextureRegion2D" /> to get.</param>
+ /// <returns>The <see cref="TextureRegion2D" />.</returns>
+ public TextureRegion2D GetRegion(string name)
+ {
+ return GetRegion<TextureRegion2D>(name);
+ }
+
+ /// <summary>
+ /// Gets a texture region from the <see cref="TextureAtlas" /> of a specified type.
+ /// This is can be useful if the atlas contains <see cref="NinePatchRegion2D"/>'s.
+ /// </summary>
+ /// <typeparam name="T">Type of the region to get</typeparam>
+ /// <param name="name">Name of the region to get</param>
+ /// <returns>The texture region</returns>
+ public T GetRegion<T>(string name) where T : TextureRegion2D
+ {
+ if (_regionsByName.TryGetValue(name, out var region))
+ return (T)region;
+
+ throw new KeyNotFoundException(name);
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="TextureAtlas" /> and populates it with a grid of <see cref="TextureRegion2D" />.
+ /// </summary>
+ /// <param name="name">The name of this texture atlas</param>
+ /// <param name="texture">Source <see cref="Texture2D" /> image used to draw on screen</param>
+ /// <param name="regionWidth">Width of the <see cref="TextureRegion2D" />.</param>
+ /// <param name="regionHeight">Height of the <see cref="TextureRegion2D" />.</param>
+ /// <param name="maxRegionCount">The number of <see cref="TextureRegion2D" /> to create.</param>
+ /// <param name="margin">Minimum distance of the regions from the border of the source <see cref="Texture2D" /> image.</param>
+ /// <param name="spacing">Horizontal and vertical space between regions.</param>
+ /// <returns>A created and populated <see cref="TextureAtlas" />.</returns>
+ public static TextureAtlas Create(string name, Texture2D texture, int regionWidth, int regionHeight,
+ int maxRegionCount = int.MaxValue, int margin = 0, int spacing = 0)
+ {
+ var textureAtlas = new TextureAtlas(name, texture);
+ var count = 0;
+ var width = texture.Width - margin;
+ var height = texture.Height - margin;
+ var xIncrement = regionWidth + spacing;
+ var yIncrement = regionHeight + spacing;
+
+ for (var y = margin; y < height; y += yIncrement)
+ {
+ for (var x = margin; x < width; x += xIncrement)
+ {
+ var regionName = $"{texture.Name ?? "region"}{count}";
+ textureAtlas.CreateRegion(regionName, x, y, regionWidth, regionHeight);
+ count++;
+
+ if (count >= maxRegionCount)
+ return textureAtlas;
+ }
+ }
+
+ return textureAtlas;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasExtensions.cs
new file mode 100644
index 0000000..0ec6b86
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasExtensions.cs
@@ -0,0 +1,113 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public static class TextureAtlasExtensions
+ {
+ public static void Draw(this SpriteBatch spriteBatch, TextureRegion2D textureRegion, Vector2 position, Color color, Rectangle? clippingRectangle = null)
+ {
+ Draw(spriteBatch, textureRegion, position, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0, clippingRectangle);
+ }
+
+ public static void Draw(this SpriteBatch spriteBatch, TextureRegion2D textureRegion, Vector2 position, Color color,
+ float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth, Rectangle? clippingRectangle = null)
+ {
+ var sourceRectangle = textureRegion.Bounds;
+
+ if (clippingRectangle.HasValue)
+ {
+ var x = (int)(position.X - origin.X);
+ var y = (int)(position.Y - origin.Y);
+ var width = (int)(textureRegion.Width * scale.X);
+ var height = (int)(textureRegion.Height * scale.Y);
+ var destinationRectangle = new Rectangle(x, y, width, height);
+
+ sourceRectangle = ClipSourceRectangle(textureRegion.Bounds, destinationRectangle, clippingRectangle.Value);
+ position.X += sourceRectangle.X - textureRegion.Bounds.X;
+ position.Y += sourceRectangle.Y - textureRegion.Bounds.Y;
+
+ if(sourceRectangle.Width <= 0 || sourceRectangle.Height <= 0)
+ return;
+ }
+
+ spriteBatch.Draw(textureRegion.Texture, position, sourceRectangle, color, rotation, origin, scale, effects, layerDepth);
+ }
+
+ public static void Draw(this SpriteBatch spriteBatch, TextureRegion2D textureRegion, Rectangle destinationRectangle, Color color, Rectangle? clippingRectangle = null)
+ {
+ var ninePatchRegion = textureRegion as NinePatchRegion2D;
+
+ if (ninePatchRegion != null)
+ Draw(spriteBatch, ninePatchRegion, destinationRectangle, color, clippingRectangle);
+ else
+ Draw(spriteBatch, textureRegion.Texture, textureRegion.Bounds, destinationRectangle, color, clippingRectangle);
+ }
+
+ public static void Draw(this SpriteBatch spriteBatch, NinePatchRegion2D ninePatchRegion, Rectangle destinationRectangle, Color color, Rectangle? clippingRectangle = null)
+ {
+ var destinationPatches = ninePatchRegion.CreatePatches(destinationRectangle);
+ var sourcePatches = ninePatchRegion.SourcePatches;
+
+ for (var i = 0; i < sourcePatches.Length; i++)
+ {
+ var sourcePatch = sourcePatches[i];
+ var destinationPatch = destinationPatches[i];
+
+ if (clippingRectangle.HasValue)
+ {
+ sourcePatch = ClipSourceRectangle(sourcePatch, destinationPatch, clippingRectangle.Value);
+ destinationPatch = ClipDestinationRectangle(destinationPatch, clippingRectangle.Value);
+ Draw(spriteBatch, ninePatchRegion.Texture, sourcePatch, destinationPatch, color, clippingRectangle);
+ }
+ else
+ {
+ if (destinationPatch.Width > 0 && destinationPatch.Height > 0)
+ spriteBatch.Draw(ninePatchRegion.Texture, sourceRectangle: sourcePatch, destinationRectangle: destinationPatch, color: color);
+ }
+ }
+ }
+
+ public static void Draw(this SpriteBatch spriteBatch, Texture2D texture, Rectangle sourceRectangle, Rectangle destinationRectangle, Color color, Rectangle? clippingRectangle)
+ {
+ if (clippingRectangle.HasValue)
+ {
+ sourceRectangle = ClipSourceRectangle(sourceRectangle, destinationRectangle, clippingRectangle.Value);
+ destinationRectangle = ClipDestinationRectangle(destinationRectangle, clippingRectangle.Value);
+ }
+
+ if (destinationRectangle.Width > 0 && destinationRectangle.Height > 0)
+ spriteBatch.Draw(texture, destinationRectangle, sourceRectangle, color);
+ }
+
+ private static Rectangle ClipSourceRectangle(Rectangle sourceRectangle, Rectangle destinationRectangle, Rectangle clippingRectangle)
+ {
+ var left = (float)(clippingRectangle.Left - destinationRectangle.Left);
+ var right = (float)(destinationRectangle.Right - clippingRectangle.Right);
+ var top = (float)(clippingRectangle.Top - destinationRectangle.Top);
+ var bottom = (float)(destinationRectangle.Bottom - clippingRectangle.Bottom);
+ var x = left > 0 ? left : 0;
+ var y = top > 0 ? top : 0;
+ var w = (right > 0 ? right : 0) + x;
+ var h = (bottom > 0 ? bottom : 0) + y;
+
+ var scaleX = (float)destinationRectangle.Width / sourceRectangle.Width;
+ var scaleY = (float)destinationRectangle.Height / sourceRectangle.Height;
+ x /= scaleX;
+ y /= scaleY;
+ w /= scaleX;
+ h /= scaleY;
+
+ return new Rectangle((int)(sourceRectangle.X + x), (int)(sourceRectangle.Y + y), (int)(sourceRectangle.Width - w), (int)(sourceRectangle.Height - h));
+ }
+
+ private static Rectangle ClipDestinationRectangle(Rectangle destinationRectangle, Rectangle clippingRectangle)
+ {
+ var left = clippingRectangle.Left < destinationRectangle.Left ? destinationRectangle.Left : clippingRectangle.Left;
+ var top = clippingRectangle.Top < destinationRectangle.Top ? destinationRectangle.Top : clippingRectangle.Top;
+ var bottom = clippingRectangle.Bottom < destinationRectangle.Bottom ? clippingRectangle.Bottom : destinationRectangle.Bottom;
+ var right = clippingRectangle.Right < destinationRectangle.Right ? clippingRectangle.Right : destinationRectangle.Right;
+ return new Rectangle(left, top, right - left, bottom - top);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonContentTypeReader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonContentTypeReader.cs
new file mode 100644
index 0000000..20a7ae6
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonContentTypeReader.cs
@@ -0,0 +1,41 @@
+using System.IO;
+using System.Text.Json;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.Content;
+using MonoGame.Extended.Serialization;
+using ContentReaderExtensions = MonoGame.Extended.Content.ContentReaderExtensions;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class TextureAtlasJsonContentTypeReader : JsonContentTypeReader<TextureAtlas>
+ {
+ private static TexturePackerFile Load(ContentReader reader)
+ {
+ var json = reader.ReadString();
+ return JsonSerializer.Deserialize<TexturePackerFile>(json);
+ }
+
+ protected override TextureAtlas Read(ContentReader reader, TextureAtlas existingInstance)
+ {
+ var texturePackerFile = Load(reader);
+ var assetName = reader.GetRelativeAssetName(texturePackerFile.Metadata.Image);
+ var texture = reader.ContentManager.Load<Texture2D>(assetName);
+ var atlas = new TextureAtlas(assetName, texture);
+
+ var regionCount = texturePackerFile.Regions.Count;
+
+ for (var i = 0; i < regionCount; i++)
+ {
+ atlas.CreateRegion(
+ ContentReaderExtensions.RemoveExtension(texturePackerFile.Regions[i].Filename),
+ texturePackerFile.Regions[i].Frame.X,
+ texturePackerFile.Regions[i].Frame.Y,
+ texturePackerFile.Regions[i].Frame.Width,
+ texturePackerFile.Regions[i].Frame.Height);
+ }
+
+ return atlas;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonConverter.cs
new file mode 100644
index 0000000..2d88709
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonConverter.cs
@@ -0,0 +1,84 @@
+using System;
+using System.IO;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.Content;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class TextureAtlasJsonConverter : JsonConverter<TextureAtlas>
+ {
+ private readonly ContentManager _contentManager;
+ private readonly string _path;
+
+ public TextureAtlasJsonConverter(ContentManager contentManager, string path)
+ {
+ _contentManager = contentManager;
+ _path = path;
+ }
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(TextureAtlas);
+
+ public override TextureAtlas Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if(reader.TokenType == JsonTokenType.String)
+ {
+ // TODO: (Aristurtle 05/20/2024) What is this for? It's just an if block that throws an exception. Need
+ // to investigate.
+ var textureAtlasAssetName = reader.GetString();
+ var contentPath = GetContentPath(textureAtlasAssetName);
+ var texturePackerFile = _contentManager.Load<TexturePackerFile>(contentPath, new JsonContentLoader());
+ var texture = _contentManager.Load<Texture2D>(texturePackerFile.Metadata.Image);
+ //return TextureAtlas.Create(texturePackerFile.Metadata.Image, texture );
+ throw new NotImplementedException();
+ }
+ else
+ {
+ var metadata = JsonSerializer.Deserialize<InlineTextureAtlas>(ref reader, options);
+
+ // TODO: When we get to .NET Standard 2.1 it would be more robust to use
+ // [Path.GetRelativePath](https://docs.microsoft.com/en-us/dotnet/api/system.io.path.getrelativepath?view=netstandard-2.1)
+ var textureName = Path.GetFileNameWithoutExtension(metadata.Texture);
+ var textureDirectory = Path.GetDirectoryName(metadata.Texture);
+ var directory = Path.GetDirectoryName(_path);
+ var relativePath = Path.Combine(_contentManager.RootDirectory, directory, textureDirectory, textureName);
+ var resolvedAssetName = Path.GetFullPath(relativePath);
+ Texture2D texture;
+ try
+ {
+ texture = _contentManager.Load<Texture2D>(resolvedAssetName);
+ }
+ catch (Exception ex)
+ {
+ if (textureDirectory == null || textureDirectory == "")
+ texture = _contentManager.Load<Texture2D>(textureName);
+ else
+ texture = _contentManager.Load<Texture2D>(textureDirectory + "/" + textureName);
+ }
+ return TextureAtlas.Create(resolvedAssetName, texture, metadata.RegionWidth, metadata.RegionHeight);
+ }
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, TextureAtlas value, JsonSerializerOptions options) { }
+
+
+ // ReSharper disable once ClassNeverInstantiated.Local
+ private class InlineTextureAtlas
+ {
+ public string Texture { get; set; }
+ public int RegionWidth { get; set; }
+ public int RegionHeight { get; set; }
+ }
+
+ private string GetContentPath(string relativePath)
+ {
+ var directory = Path.GetDirectoryName(_path);
+ return Path.Combine(directory, relativePath);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasReader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasReader.cs
new file mode 100644
index 0000000..bfc7934
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasReader.cs
@@ -0,0 +1,30 @@
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.Content;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class TextureAtlasReader : ContentTypeReader<TextureAtlas>
+ {
+ protected override TextureAtlas Read(ContentReader reader, TextureAtlas existingInstance)
+ {
+ var assetName = reader.GetRelativeAssetName(reader.ReadString());
+ var texture = reader.ContentManager.Load<Texture2D>(assetName);
+ var atlas = new TextureAtlas(assetName, texture);
+
+ var regionCount = reader.ReadInt32();
+
+ for (var i = 0; i < regionCount; i++)
+ {
+ atlas.CreateRegion(
+ reader.ReadString(),
+ reader.ReadInt32(),
+ reader.ReadInt32(),
+ reader.ReadInt32(),
+ reader.ReadInt32());
+ }
+
+ return atlas;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerFile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerFile.cs
new file mode 100644
index 0000000..b2cc243
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerFile.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class TexturePackerFile
+ {
+ [JsonPropertyName("frames")]
+ public List<TexturePackerRegion> Regions { get; set; }
+
+ [JsonPropertyName("meta")]
+ public TexturePackerMeta Metadata { get; set; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerMeta.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerMeta.cs
new file mode 100644
index 0000000..5cdf21d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerMeta.cs
@@ -0,0 +1,36 @@
+using System.Text.Json.Serialization;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class TexturePackerMeta
+ {
+ [JsonPropertyName("app")]
+ public string App { get; set; }
+
+ [JsonPropertyName("version")]
+ public string Version { get; set; }
+
+ [JsonPropertyName("image")]
+ public string Image { get; set; }
+
+ [JsonPropertyName("format")]
+ public string Format { get; set; }
+
+ [JsonPropertyName("size")]
+ public TexturePackerSize Size { get; set; }
+
+ [JsonPropertyName("scale")]
+ [JsonConverter(typeof(FloatStringConverter))]
+ public float Scale { get; set; }
+
+ [JsonPropertyName("smartupdate")]
+ public string SmartUpdate { get; set; }
+
+ public override string ToString()
+ {
+ return Image;
+ }
+ }
+}
+
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerPoint.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerPoint.cs
new file mode 100644
index 0000000..c4b5acd
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerPoint.cs
@@ -0,0 +1,18 @@
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class TexturePackerPoint
+ {
+ [JsonPropertyName("x")]
+ public double X { get; set; }
+
+ [JsonPropertyName("y")]
+ public double Y { get; set; }
+
+ public override string ToString()
+ {
+ return string.Format("{0} {1}", X, Y);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRectangle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRectangle.cs
new file mode 100644
index 0000000..21f3d3b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRectangle.cs
@@ -0,0 +1,24 @@
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class TexturePackerRectangle
+ {
+ [JsonPropertyName("x")]
+ public int X { get; set; }
+
+ [JsonPropertyName("y")]
+ public int Y { get; set; }
+
+ [JsonPropertyName("w")]
+ public int Width { get; set; }
+
+ [JsonPropertyName("h")]
+ public int Height { get; set; }
+
+ public override string ToString()
+ {
+ return string.Format("{0} {1} {2} {3}", X, Y, Width, Height);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRegion.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRegion.cs
new file mode 100644
index 0000000..8664aa6
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRegion.cs
@@ -0,0 +1,33 @@
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class TexturePackerRegion
+ {
+ [JsonPropertyName("filename")]
+ public string Filename { get; set; }
+
+ [JsonPropertyName("frame")]
+ public TexturePackerRectangle Frame { get; set; }
+
+ [JsonPropertyName("rotated")]
+ public bool IsRotated { get; set; }
+
+ [JsonPropertyName("trimmed")]
+ public bool IsTrimmed { get; set; }
+
+ [JsonPropertyName("spriteSourceSize")]
+ public TexturePackerRectangle SourceRectangle { get; set; }
+
+ [JsonPropertyName("sourceSize")]
+ public TexturePackerSize SourceSize { get; set; }
+
+ [JsonPropertyName("pivot")]
+ public TexturePackerPoint PivotPoint { get; set; }
+
+ public override string ToString()
+ {
+ return $"{Filename} {Frame}";
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerSize.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerSize.cs
new file mode 100644
index 0000000..05351d0
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerSize.cs
@@ -0,0 +1,18 @@
+using System.Text.Json.Serialization;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class TexturePackerSize
+ {
+ [JsonPropertyName("w")]
+ public int Width { get; set; }
+
+ [JsonPropertyName("h")]
+ public int Height { get; set; }
+
+ public override string ToString()
+ {
+ return $"{Width} {Height}";
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureRegion2D.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureRegion2D.cs
new file mode 100644
index 0000000..05da555
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureRegion2D.cs
@@ -0,0 +1,54 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.TextureAtlases
+{
+ public class TextureRegion2D
+ {
+ public TextureRegion2D(Texture2D texture, int x, int y, int width, int height)
+ : this(null, texture, x, y, width, height)
+ {
+ }
+
+ public TextureRegion2D(Texture2D texture, Rectangle region)
+ : this(null, texture, region.X, region.Y, region.Width, region.Height)
+ {
+ }
+
+ public TextureRegion2D(string name, Texture2D texture, Rectangle region)
+ : this(name, texture, region.X, region.Y, region.Width, region.Height)
+ {
+ }
+
+ public TextureRegion2D(Texture2D texture)
+ : this(texture.Name, texture, 0, 0, texture.Width, texture.Height)
+ {
+ }
+
+ public TextureRegion2D(string name, Texture2D texture, int x, int y, int width, int height)
+ {
+ Name = name;
+ Texture = texture;
+ X = x;
+ Y = y;
+ Width = width;
+ Height = height;
+ }
+
+ public string Name { get; }
+ public Texture2D Texture { get; protected set; }
+ public int X { get; }
+ public int Y { get; }
+ public int Width { get; }
+ public int Height { get; }
+ public Size2 Size => new Size2(Width, Height);
+ public object Tag { get; set; }
+ public Rectangle Bounds => new Rectangle(X, Y, Width, Height);
+
+ public override string ToString()
+ {
+ return $"{Name ?? string.Empty} {Bounds}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/ContinuousClock.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/ContinuousClock.cs
new file mode 100644
index 0000000..c31930b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/ContinuousClock.cs
@@ -0,0 +1,36 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Timers
+{
+ public class ContinuousClock : GameTimer
+ {
+ public ContinuousClock(double intervalSeconds)
+ : base(intervalSeconds)
+ {
+ }
+
+ public ContinuousClock(TimeSpan interval)
+ : base(interval)
+ {
+ }
+
+ public TimeSpan NextTickTime { get; protected set; }
+
+ public event EventHandler Tick;
+
+ protected override void OnStopped()
+ {
+ NextTickTime = CurrentTime + Interval;
+ }
+
+ protected override void OnUpdate(GameTime gameTime)
+ {
+ if (CurrentTime >= NextTickTime)
+ {
+ NextTickTime = CurrentTime + Interval;
+ Tick?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/CountdownTimer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/CountdownTimer.cs
new file mode 100644
index 0000000..fea7659
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/CountdownTimer.cs
@@ -0,0 +1,42 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Timers
+{
+ public class CountdownTimer : GameTimer
+ {
+ public CountdownTimer(double intervalSeconds)
+ : base(intervalSeconds)
+ {
+ }
+
+ public CountdownTimer(TimeSpan interval)
+ : base(interval)
+ {
+ }
+
+ public TimeSpan TimeRemaining { get; private set; }
+
+ public event EventHandler TimeRemainingChanged;
+ public event EventHandler Completed;
+
+ protected override void OnStopped()
+ {
+ CurrentTime = TimeSpan.Zero;
+ }
+
+ protected override void OnUpdate(GameTime gameTime)
+ {
+ TimeRemaining = Interval - CurrentTime;
+ TimeRemainingChanged?.Invoke(this, EventArgs.Empty);
+
+ if (CurrentTime >= Interval)
+ {
+ State = TimerState.Completed;
+ CurrentTime = Interval;
+ TimeRemaining = TimeSpan.Zero;
+ Completed?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/GameTimer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/GameTimer.cs
new file mode 100644
index 0000000..6a3acfe
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/GameTimer.cs
@@ -0,0 +1,65 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Timers
+{
+ public abstract class GameTimer : IUpdate
+ {
+ protected GameTimer(double intervalSeconds)
+ : this(TimeSpan.FromSeconds(intervalSeconds))
+ {
+ }
+
+ protected GameTimer(TimeSpan interval)
+ {
+ Interval = interval;
+ Restart();
+ }
+
+ public TimeSpan Interval { get; set; }
+ public TimeSpan CurrentTime { get; protected set; }
+ public TimerState State { get; protected set; }
+
+ public void Update(GameTime gameTime)
+ {
+ if (State != TimerState.Started)
+ return;
+
+ CurrentTime += gameTime.ElapsedGameTime;
+ OnUpdate(gameTime);
+ }
+
+ public event EventHandler Started;
+ public event EventHandler Stopped;
+ public event EventHandler Paused;
+
+ public void Start()
+ {
+ State = TimerState.Started;
+ Started?.Invoke(this, EventArgs.Empty);
+ }
+
+ public void Stop()
+ {
+ State = TimerState.Stopped;
+ CurrentTime = TimeSpan.Zero;
+ OnStopped();
+ Stopped?.Invoke(this, EventArgs.Empty);
+ }
+
+ public void Restart()
+ {
+ Stop();
+ Start();
+ }
+
+ public void Pause()
+ {
+ State = TimerState.Paused;
+ Paused?.Invoke(this, EventArgs.Empty);
+ }
+
+ protected abstract void OnStopped();
+ protected abstract void OnUpdate(GameTime gameTime);
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/TimerState.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/TimerState.cs
new file mode 100644
index 0000000..1ab74ce
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/TimerState.cs
@@ -0,0 +1,10 @@
+namespace MonoGame.Extended.Timers
+{
+ public enum TimerState
+ {
+ Started,
+ Stopped,
+ Paused,
+ Completed
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Transform.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Transform.cs
new file mode 100644
index 0000000..b0a0be4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Transform.cs
@@ -0,0 +1,456 @@
+using System;
+using System.ComponentModel;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended
+{
+ // Code derived from top answer: http://gamedev.stackexchange.com/questions/113977/should-i-store-local-forward-right-up-vector-or-calculate-when-necessary
+
+ [Flags]
+ internal enum TransformFlags : byte
+ {
+ WorldMatrixIsDirty = 1 << 0,
+ LocalMatrixIsDirty = 1 << 1,
+ All = WorldMatrixIsDirty | LocalMatrixIsDirty
+ }
+
+ /// <summary>
+ /// Represents the base class for the position, rotation, and scale of a game object in two-dimensions or
+ /// three-dimensions.
+ /// </summary>
+ /// <typeparam name="TMatrix">The type of the matrix.</typeparam>
+ /// <remarks>
+ /// <para>
+ /// Every game object has a transform which is used to store and manipulate the position, rotation and scale
+ /// of the object. Every transform can have a parent, which allows to apply position, rotation and scale to game
+ /// objects hierarchically.
+ /// </para>
+ /// <para>
+ /// This class shouldn't be used directly. Instead use either of the derived classes; <see cref="Transform2" /> or
+ /// Transform3D.
+ /// </para>
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public abstract class BaseTransform<TMatrix>
+ where TMatrix : struct
+ {
+ private TransformFlags _flags = TransformFlags.All; // dirty flags, set all dirty flags when created
+ private TMatrix _localMatrix; // model space to local space
+ private BaseTransform<TMatrix> _parent; // parent
+ private TMatrix _worldMatrix; // local space to world space
+
+ // internal contructor because people should not be using this class directly; they should use Transform2D or Transform3D
+ internal BaseTransform()
+ {
+ }
+
+ /// <summary>
+ /// Gets the model-to-local space <see cref="Matrix2" />.
+ /// </summary>
+ /// <value>
+ /// The model-to-local space <see cref="Matrix2" />.
+ /// </value>
+ public TMatrix LocalMatrix
+ {
+ get
+ {
+ RecalculateLocalMatrixIfNecessary(); // attempt to update local matrix upon request if it is dirty
+ return _localMatrix;
+ }
+ }
+
+ /// <summary>
+ /// Gets the local-to-world space <see cref="Matrix2" />.
+ /// </summary>
+ /// <value>
+ /// The local-to-world space <see cref="Matrix2" />.
+ /// </value>
+ public TMatrix WorldMatrix
+ {
+ get
+ {
+ RecalculateWorldMatrixIfNecessary(); // attempt to update world matrix upon request if it is dirty
+ return _worldMatrix;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the parent instance.
+ /// </summary>
+ /// <value>
+ /// The parent instance.
+ /// </value>
+ /// <remarks>
+ /// <para>
+ /// Setting <see cref="Parent" /> to a non-null instance enables this instance to
+ /// inherit the position, rotation, and scale of the parent instance. Setting <see cref="Parent" /> to
+ /// <code>null</code> disables the inheritance altogether for this instance.
+ /// </para>
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public BaseTransform<TMatrix> Parent
+ {
+ get { return _parent; }
+ set
+ {
+ if (_parent == value)
+ return;
+
+ var oldParentTransform = Parent;
+ _parent = value;
+ OnParentChanged(oldParentTransform, value);
+ }
+ }
+
+ public event Action TransformBecameDirty; // observer pattern for when the world (or local) matrix became dirty
+ public event Action TranformUpdated; // observer pattern for after the world (or local) matrix was re-calculated
+
+ /// <summary>
+ /// Gets the model-to-local space <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="matrix">The model-to-local space <see cref="Matrix2" />.</param>
+ public void GetLocalMatrix(out TMatrix matrix)
+ {
+ RecalculateLocalMatrixIfNecessary();
+ matrix = _localMatrix;
+ }
+
+ /// <summary>
+ /// Gets the local-to-world space <see cref="Matrix2" />.
+ /// </summary>
+ /// <param name="matrix">The local-to-world space <see cref="Matrix2" />.</param>
+ public void GetWorldMatrix(out TMatrix matrix)
+ {
+ RecalculateWorldMatrixIfNecessary();
+ matrix = _worldMatrix;
+ }
+
+ protected internal void LocalMatrixBecameDirty()
+ {
+ _flags |= TransformFlags.LocalMatrixIsDirty;
+ }
+
+ protected internal void WorldMatrixBecameDirty()
+ {
+ _flags |= TransformFlags.WorldMatrixIsDirty;
+ TransformBecameDirty?.Invoke();
+ }
+
+ private void OnParentChanged(BaseTransform<TMatrix> oldParent, BaseTransform<TMatrix> newParent)
+ {
+ var parent = oldParent;
+ while (parent != null)
+ {
+ parent.TransformBecameDirty -= ParentOnTransformBecameDirty;
+ parent = parent.Parent;
+ }
+
+ parent = newParent;
+ while (parent != null)
+ {
+ parent.TransformBecameDirty += ParentOnTransformBecameDirty;
+ parent = parent.Parent;
+ }
+ }
+
+ private void ParentOnTransformBecameDirty()
+ {
+ _flags |= TransformFlags.All;
+ }
+
+ private void RecalculateWorldMatrixIfNecessary()
+ {
+ if ((_flags & TransformFlags.WorldMatrixIsDirty) == 0)
+ return;
+
+ RecalculateLocalMatrixIfNecessary();
+ RecalculateWorldMatrix(ref _localMatrix, out _worldMatrix);
+
+ _flags &= ~TransformFlags.WorldMatrixIsDirty;
+ TranformUpdated?.Invoke();
+ }
+
+ protected internal abstract void RecalculateWorldMatrix(ref TMatrix localMatrix, out TMatrix matrix);
+
+ private void RecalculateLocalMatrixIfNecessary()
+ {
+ if ((_flags & TransformFlags.LocalMatrixIsDirty) == 0)
+ return;
+
+ RecalculateLocalMatrix(out _localMatrix);
+
+ _flags &= ~TransformFlags.LocalMatrixIsDirty;
+ WorldMatrixBecameDirty();
+ }
+
+ protected internal abstract void RecalculateLocalMatrix(out TMatrix matrix);
+ }
+
+ /// <summary>
+ /// Represents the position, rotation, and scale of a two-dimensional game object.
+ /// </summary>
+ /// <seealso cref="BaseTransform{Matrix2D}" />
+ /// <seealso cref="IMovable" />
+ /// <seealso cref="IRotatable" />
+ /// <seealso cref="IScalable" />
+ /// <remarks>
+ /// <para>
+ /// Every game object has a transform which is used to store and manipulate the position, rotation and scale
+ /// of the object. Every transform can have a parent, which allows to apply position, rotation and scale to game
+ /// objects hierarchically.
+ /// </para>
+ /// </remarks>
+ public class Transform2 : BaseTransform<Matrix2>, IMovable, IRotatable, IScalable
+ {
+ private Vector2 _position;
+ private float _rotation;
+ private Vector2 _scale = Vector2.One;
+
+ public Transform2()
+ : this(Vector2.Zero, 0, Vector2.One)
+ {
+ }
+
+ public Transform2(float x, float y, float rotation = 0, float scaleX = 1, float scaleY = 1)
+ : this(new Vector2(x, y), rotation, new Vector2(scaleX, scaleY))
+ {
+ }
+
+ public Transform2(Vector2? position = null, float rotation = 0, Vector2? scale = null)
+ {
+ Position = position ?? Vector2.Zero;
+ Rotation = rotation;
+ Scale = scale ?? Vector2.One;
+ }
+
+ /// <summary>
+ /// Gets the world position.
+ /// </summary>
+ /// <value>
+ /// The world position.
+ /// </value>
+ public Vector2 WorldPosition => WorldMatrix.Translation;
+
+ /// <summary>
+ /// Gets the world scale.
+ /// </summary>
+ /// <value>
+ /// The world scale.
+ /// </value>
+ public Vector2 WorldScale => WorldMatrix.Scale;
+
+ /// <summary>
+ /// Gets the world rotation angle in radians.
+ /// </summary>
+ /// <value>
+ /// The world rotation angle in radians.
+ /// </value>
+ public float WorldRotation => WorldMatrix.Rotation;
+
+ /// <summary>
+ /// Gets or sets the local position.
+ /// </summary>
+ /// <value>
+ /// The local position.
+ /// </value>
+ public Vector2 Position
+ {
+ get { return _position; }
+ set
+ {
+ _position = value;
+ LocalMatrixBecameDirty();
+ WorldMatrixBecameDirty();
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the local rotation angle in radians.
+ /// </summary>
+ /// <value>
+ /// The local rotation angle in radians.
+ /// </value>
+ public float Rotation
+ {
+ get { return _rotation; }
+ set
+ {
+ _rotation = value;
+ LocalMatrixBecameDirty();
+ WorldMatrixBecameDirty();
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the local scale.
+ /// </summary>
+ /// <value>
+ /// The local scale.
+ /// </value>
+ public Vector2 Scale
+ {
+ get { return _scale; }
+ set
+ {
+ _scale = value;
+ LocalMatrixBecameDirty();
+ WorldMatrixBecameDirty();
+ }
+ }
+
+ protected internal override void RecalculateWorldMatrix(ref Matrix2 localMatrix, out Matrix2 matrix)
+ {
+ if (Parent != null)
+ {
+ Parent.GetWorldMatrix(out matrix);
+ Matrix2.Multiply(ref localMatrix, ref matrix, out matrix);
+ }
+ else
+ {
+ matrix = localMatrix;
+ }
+ }
+
+ protected internal override void RecalculateLocalMatrix(out Matrix2 matrix)
+ {
+ matrix = Matrix2.CreateScale(_scale) *
+ Matrix2.CreateRotationZ(_rotation) *
+ Matrix2.CreateTranslation(_position);
+ }
+
+ public override string ToString()
+ {
+ return $"Position: {Position}, Rotation: {Rotation}, Scale: {Scale}";
+ }
+ }
+
+
+ /// <summary>
+ /// Represents the position, rotation, and scale of a three-dimensional game object.
+ /// </summary>
+ /// <seealso cref="BaseTransform{Matrix}" />
+ /// <remarks>
+ /// <para>
+ /// Every game object has a transform which is used to store and manipulate the position, rotation and scale
+ /// of the object. Every transform can have a parent, which allows to apply position, rotation and scale to game
+ /// objects hierarchically.
+ /// </para>
+ /// </remarks>
+ public class Transform3 : BaseTransform<Matrix> {
+ private Vector3 _position;
+ private Quaternion _rotation;
+ private Vector3 _scale = Vector3.One;
+
+ public Transform3(Vector3? position = null, Quaternion? rotation = null, Vector3? scale = null) {
+ Position = position ?? Vector3.Zero;
+ Rotation = rotation ?? Quaternion.Identity;
+ Scale = scale ?? Vector3.One;
+ }
+
+ /// <summary>
+ /// Gets the world position.
+ /// </summary>
+ /// <value>
+ /// The world position.
+ /// </value>
+ public Vector3 WorldPosition => WorldMatrix.Translation;
+
+ /// <summary>
+ /// Gets the world scale.
+ /// </summary>
+ /// <value>
+ /// The world scale.
+ /// </value>
+ public Vector3 WorldScale {
+ get {
+ Vector3 scale = Vector3.Zero;
+ Quaternion rotation = Quaternion.Identity;
+ Vector3 translation = Vector3.Zero;
+ WorldMatrix.Decompose(out scale, out rotation, out translation);
+ return scale;
+ }
+ }
+
+
+ /// <summary>
+ /// Gets the world rotation quaternion in radians.
+ /// </summary>
+ /// <value>
+ /// The world rotation quaternion in radians.
+ /// </value>
+ public Quaternion WorldRotation {
+ get {
+ Vector3 scale = Vector3.Zero;
+ Quaternion rotation = Quaternion.Identity;
+ Vector3 translation = Vector3.Zero;
+ WorldMatrix.Decompose(out scale, out rotation, out translation);
+ return rotation;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the local position.
+ /// </summary>
+ /// <value>
+ /// The local position.
+ /// </value>
+ public Vector3 Position {
+ get { return _position; }
+ set {
+ _position = value;
+ LocalMatrixBecameDirty();
+ WorldMatrixBecameDirty();
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the local rotation quaternion in radians.
+ /// </summary>
+ /// <value>
+ /// The local rotation quaternion in radians.
+ /// </value>
+ public Quaternion Rotation {
+ get { return _rotation; }
+ set {
+ _rotation = value;
+ LocalMatrixBecameDirty();
+ WorldMatrixBecameDirty();
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the local scale.
+ /// </summary>
+ /// <value>
+ /// The local scale.
+ /// </value>
+ public Vector3 Scale {
+ get { return _scale; }
+ set {
+ _scale = value;
+ LocalMatrixBecameDirty();
+ WorldMatrixBecameDirty();
+ }
+ }
+
+ protected internal override void RecalculateWorldMatrix(ref Matrix localMatrix, out Matrix matrix) {
+ if (Parent != null) {
+ Parent.GetWorldMatrix(out matrix);
+ Matrix.Multiply(ref localMatrix, ref matrix, out matrix);
+ }
+ else {
+ matrix = localMatrix;
+ }
+ }
+
+ protected internal override void RecalculateLocalMatrix(out Matrix matrix) {
+ matrix = Matrix.CreateScale(_scale) *
+ Matrix.CreateFromQuaternion(_rotation) *
+ Matrix.CreateTranslation(_position);
+ }
+
+ public override string ToString() {
+ return $"Position: {Position}, Rotation: {Rotation}, Scale: {Scale}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveBatch.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveBatch.cs
new file mode 100644
index 0000000..621a627
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveBatch.cs
@@ -0,0 +1,183 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MonoGame.Extended.VectorDraw
+{
+ //Taken from Velcro Physics
+ //Used with permission: https://github.com/craftworkgames/MonoGame.Extended/issues/574
+
+ public class PrimitiveBatch : IDisposable
+ {
+ private const int DefaultBufferSize = 500;
+
+ // a basic effect, which contains the shaders that we will use to draw our
+ // primitives.
+ private readonly BasicEffect _basicEffect;
+
+ // the device that we will issue draw calls to.
+ private readonly GraphicsDevice _device;
+
+ private readonly VertexPositionColor[] _lineVertices;
+ private readonly VertexPositionColor[] _triangleVertices;
+
+ // hasBegun is flipped to true once Begin is called, and is used to make
+ // sure users don't call End before Begin is called.
+ private bool _hasBegun;
+
+ private bool _isDisposed;
+ private int _lineVertsCount;
+ private int _triangleVertsCount;
+
+ public PrimitiveBatch(GraphicsDevice graphicsDevice, int bufferSize = DefaultBufferSize)
+ {
+ if (graphicsDevice == null)
+ throw new ArgumentNullException(nameof(graphicsDevice));
+
+ _device = graphicsDevice;
+
+ _triangleVertices = new VertexPositionColor[bufferSize - bufferSize % 3];
+ _lineVertices = new VertexPositionColor[bufferSize - bufferSize % 2];
+
+ // set up a new basic effect, and enable vertex colors.
+ _basicEffect = new BasicEffect(graphicsDevice);
+ _basicEffect.VertexColorEnabled = true;
+ }
+
+ #region IDisposable Members
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ #endregion
+
+ public void SetProjection(ref Matrix projection)
+ {
+ _basicEffect.Projection = projection;
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing && !_isDisposed)
+ {
+ if (_basicEffect != null)
+ _basicEffect.Dispose();
+
+ _isDisposed = true;
+ }
+ }
+
+ /// <summary>
+ /// Begin is called to tell the PrimitiveBatch what kind of primitives will be
+ /// drawn, and to prepare the graphics card to render those primitives.
+ /// </summary>
+ /// <param name="projection">The projection.</param>
+ /// <param name="view">The view.</param>
+ public void Begin(ref Matrix projection, ref Matrix view)
+ {
+ if (_hasBegun)
+ throw new InvalidOperationException("End must be called before Begin can be called again.");
+
+ //tell our basic effect to begin.
+ _basicEffect.Projection = projection;
+ _basicEffect.View = view;
+ _basicEffect.CurrentTechnique.Passes[0].Apply();
+
+ // flip the error checking boolean. It's now ok to call AddVertex, Flush,
+ // and End.
+ _hasBegun = true;
+ }
+
+ public bool IsReady()
+ {
+ return _hasBegun;
+ }
+
+ public void AddVertex(Vector2 vertex, Color color, PrimitiveType primitiveType)
+ {
+ if (!_hasBegun)
+ throw new InvalidOperationException("Begin must be called before AddVertex can be called.");
+
+ if (primitiveType == PrimitiveType.LineStrip || primitiveType == PrimitiveType.TriangleStrip)
+ throw new NotSupportedException("The specified primitiveType is not supported by PrimitiveBatch.");
+
+ if (primitiveType == PrimitiveType.TriangleList)
+ {
+ if (_triangleVertsCount >= _triangleVertices.Length)
+ FlushTriangles();
+
+ _triangleVertices[_triangleVertsCount].Position = new Vector3(vertex, -0.1f);
+ _triangleVertices[_triangleVertsCount].Color = color;
+ _triangleVertsCount++;
+ }
+
+ if (primitiveType == PrimitiveType.LineList)
+ {
+ if (_lineVertsCount >= _lineVertices.Length)
+ FlushLines();
+
+ _lineVertices[_lineVertsCount].Position = new Vector3(vertex, 0f);
+ _lineVertices[_lineVertsCount].Color = color;
+ _lineVertsCount++;
+ }
+ }
+
+ /// <summary>
+ /// End is called once all the primitives have been drawn using AddVertex.
+ /// it will call Flush to actually submit the draw call to the graphics card, and
+ /// then tell the basic effect to end.
+ /// </summary>
+ public void End()
+ {
+ if (!_hasBegun)
+ {
+ throw new InvalidOperationException("Begin must be called before End can be called.");
+ }
+
+ // Draw whatever the user wanted us to draw
+ FlushTriangles();
+ FlushLines();
+
+ _hasBegun = false;
+ }
+
+ private void FlushTriangles()
+ {
+ if (!_hasBegun)
+ {
+ throw new InvalidOperationException("Begin must be called before Flush can be called.");
+ }
+ if (_triangleVertsCount >= 3)
+ {
+ int primitiveCount = _triangleVertsCount / 3;
+
+ // submit the draw call to the graphics card
+ _device.SamplerStates[0] = SamplerState.AnisotropicClamp;
+ _device.DrawUserPrimitives(PrimitiveType.TriangleList, _triangleVertices, 0, primitiveCount);
+ _triangleVertsCount -= primitiveCount * 3;
+ }
+ }
+
+ private void FlushLines()
+ {
+ if (!_hasBegun)
+ {
+ throw new InvalidOperationException("Begin must be called before Flush can be called.");
+ }
+ if (_lineVertsCount >= 2)
+ {
+ int primitiveCount = _lineVertsCount / 2;
+
+ // submit the draw call to the graphics card
+ _device.SamplerStates[0] = SamplerState.AnisotropicClamp;
+ _device.DrawUserPrimitives(PrimitiveType.LineList, _lineVertices, 0, primitiveCount);
+ _lineVertsCount -= primitiveCount * 2;
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveDrawing.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveDrawing.cs
new file mode 100644
index 0000000..fcf5791
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveDrawing.cs
@@ -0,0 +1,255 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.Triangulation;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MonoGame.Extended.VectorDraw
+{
+ public class PrimitiveDrawing
+ {
+ //Drawing
+ private PrimitiveBatch _primitiveBatch;
+
+ private SpriteBatch _batch;
+ private SpriteFont _font;
+ private GraphicsDevice _device;
+ private readonly Vector2[] _tempVertices = new Vector2[1000]; //TODO: something else...
+
+ //private Matrix _localProjection;
+ //private Matrix _localView;
+
+ //TODO: do we need to split this based on platform?
+#if XBOX || WINDOWS_PHONE
+ public const int CircleSegments = 16;
+#else
+ public const int CircleSegments = 32;
+#endif
+
+ public PrimitiveDrawing(PrimitiveBatch primitiveBatch)
+ {
+ _primitiveBatch = primitiveBatch;
+ }
+
+ public void DrawPoint(Vector2 center, Color color)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ //Add two points or the PrimitiveBatch acts up
+ _primitiveBatch.AddVertex(center, color, PrimitiveType.LineList);
+ _primitiveBatch.AddVertex(center, color, PrimitiveType.LineList);
+ }
+
+ public void DrawRectangle(Vector2 location, float width, float height, Color color)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ Vector2[] rectVerts = new Vector2[4]
+ {
+ new Vector2(0, 0),
+ new Vector2(width, 0),
+ new Vector2(width, height),
+ new Vector2(0, height)
+ };
+
+ //Location is offset here
+ DrawPolygon(location, rectVerts, color);
+ }
+
+ public void DrawSolidRectangle(Vector2 location, float width, float height, Color color)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ Vector2[] rectVerts = new Vector2[4]
+ {
+ new Vector2(0, 0),
+ new Vector2(width, 0),
+ new Vector2(width, height),
+ new Vector2(0, height)
+ };
+
+ DrawSolidPolygon(location, rectVerts, color);
+ }
+
+ public void DrawCircle(Vector2 center, float radius, Color color)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ const double increment = Math.PI * 2.0 / CircleSegments;
+ double theta = 0.0;
+
+ for (int i = 0; i < CircleSegments; i++)
+ {
+ Vector2 v1 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta));
+ Vector2 v2 = center + radius * new Vector2((float)Math.Cos(theta + increment), (float)Math.Sin(theta + increment));
+
+ _primitiveBatch.AddVertex(v1, color, PrimitiveType.LineList);
+ _primitiveBatch.AddVertex(v2, color, PrimitiveType.LineList);
+
+ theta += increment;
+ }
+ }
+
+ public void DrawSolidCircle(Vector2 center, float radius, Color color)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ const double increment = Math.PI * 2.0 / CircleSegments;
+ double theta = 0.0;
+
+ Color colorFill = color * 0.5f;
+
+ Vector2 v0 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta));
+ theta += increment;
+
+ for (int i = 1; i < CircleSegments - 1; i++)
+ {
+ Vector2 v1 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta));
+ Vector2 v2 = center + radius * new Vector2((float)Math.Cos(theta + increment), (float)Math.Sin(theta + increment));
+
+ _primitiveBatch.AddVertex(v0, colorFill, PrimitiveType.TriangleList);
+ _primitiveBatch.AddVertex(v1, colorFill, PrimitiveType.TriangleList);
+ _primitiveBatch.AddVertex(v2, colorFill, PrimitiveType.TriangleList);
+
+ theta += increment;
+ }
+
+ DrawCircle(center, radius, color);
+ }
+ public void DrawSolidCircle(Vector2 center, float radius, Color color, Color fillcolor)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ const double increment = Math.PI * 2.0 / CircleSegments;
+ double theta = 0.0;
+
+ Vector2 v0 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta));
+ theta += increment;
+
+ for (int i = 1; i < CircleSegments - 1; i++)
+ {
+ Vector2 v1 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta));
+ Vector2 v2 = center + radius * new Vector2((float)Math.Cos(theta + increment), (float)Math.Sin(theta + increment));
+
+ _primitiveBatch.AddVertex(v0, fillcolor, PrimitiveType.TriangleList);
+ _primitiveBatch.AddVertex(v1, fillcolor, PrimitiveType.TriangleList);
+ _primitiveBatch.AddVertex(v2, fillcolor, PrimitiveType.TriangleList);
+
+ theta += increment;
+ }
+
+ DrawCircle(center, radius, color);
+ }
+
+ public void DrawSegment(Vector2 start, Vector2 end, Color color)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ _primitiveBatch.AddVertex(start, color, PrimitiveType.LineList);
+ _primitiveBatch.AddVertex(end, color, PrimitiveType.LineList);
+ }
+
+ public void DrawPolygon(Vector2 position, Vector2[] vertices, Color color, bool closed = true)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ int count = vertices.Length;
+
+ for (int i = 0; i < count - 1; i++)
+ {
+ //translate the vertices according to the position passed
+ _primitiveBatch.AddVertex(new Vector2(vertices[i].X + position.X, vertices[i].Y + position.Y), color, PrimitiveType.LineList);
+ _primitiveBatch.AddVertex(new Vector2(vertices[i + 1].X + position.X, vertices[i + 1].Y + position.Y), color, PrimitiveType.LineList);
+ }
+ if (closed)
+ {
+ //TODO: verify closed is working as expected
+ _primitiveBatch.AddVertex(new Vector2(vertices[count - 1].X + position.X, vertices[count - 1].Y + position.Y), color, PrimitiveType.LineList);
+ _primitiveBatch.AddVertex(new Vector2(vertices[0].X + position.X, vertices[0].Y + position.Y), color, PrimitiveType.LineList);
+ }
+ }
+
+ public void DrawSolidPolygon(Vector2 position, Vector2[] vertices, Color color, bool outline = true)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ int count = vertices.Length;
+
+ if (count == 2)
+ {
+ DrawPolygon(position, vertices, color);
+ return;
+ }
+
+ Color colorFill = color * (outline ? 0.5f : 1.0f);
+
+ Vector2[] outVertices;
+ int[] outIndices;
+ Triangulator.Triangulate(vertices, WindingOrder.CounterClockwise, out outVertices, out outIndices);
+
+ for(int i = 0; i < outIndices.Length - 2; i += 3)
+ {
+ _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i]].X + position.X, outVertices[outIndices[i]].Y + position.Y), colorFill, PrimitiveType.TriangleList);
+ _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i + 1]].X + position.X, outVertices[outIndices[i + 1]].Y + position.Y), colorFill, PrimitiveType.TriangleList);
+ _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i + 2]].X + position.X, outVertices[outIndices[i + 2]].Y + position.Y), colorFill, PrimitiveType.TriangleList);
+ }
+
+ if (outline)
+ DrawPolygon(position, vertices, color);
+ }
+
+ public void DrawEllipse(Vector2 center, Vector2 radius, int sides, Color color)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ DrawPolygon(center, CreateEllipse(radius.X, radius.Y, sides), color);
+ }
+
+ public void DrawSolidEllipse(Vector2 center, Vector2 radius, int sides, Color color, bool outline = true)
+ {
+ if (!_primitiveBatch.IsReady())
+ throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything.");
+
+ Color colorFill = color * (outline ? 0.5f : 1.0f);
+
+ Vector2[] vertices = CreateEllipse(radius.X, radius.Y, sides);
+
+ Vector2[] outVertices;
+ int[] outIndices;
+ Triangulator.Triangulate(vertices, WindingOrder.CounterClockwise, out outVertices, out outIndices);
+
+ for (int i = 0; i < outIndices.Length - 2; i += 3)
+ {
+ _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i]].X + center.X, outVertices[outIndices[i]].Y + center.Y), colorFill, PrimitiveType.TriangleList);
+ _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i + 1]].X + center.X, outVertices[outIndices[i + 1]].Y + center.Y), colorFill, PrimitiveType.TriangleList);
+ _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i + 2]].X + center.X, outVertices[outIndices[i + 2]].Y + center.Y), colorFill, PrimitiveType.TriangleList);
+ }
+ }
+
+ private static Vector2[] CreateEllipse(float rx, float ry, int sides)
+ {
+ var vertices = new Vector2[sides];
+
+ var t = 0.0;
+ var dt = 2.0 * Math.PI / sides;
+ for (var i = 0; i < sides; i++, t += dt)
+ {
+ var x = (float)(rx * Math.Cos(t));
+ var y = (float)(ry * Math.Sin(t));
+ vertices[i] = new Vector2(x, y);
+ }
+ return vertices;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/BoxingViewportAdapter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/BoxingViewportAdapter.cs
new file mode 100644
index 0000000..ea80a5d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/BoxingViewportAdapter.cs
@@ -0,0 +1,96 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+// ReSharper disable once CheckNamespace
+
+namespace MonoGame.Extended.ViewportAdapters
+{
+ public enum BoxingMode
+ {
+ None,
+ Letterbox,
+ Pillarbox
+ }
+
+ public class BoxingViewportAdapter : ScalingViewportAdapter
+ {
+ private readonly GameWindow _window;
+ private readonly GraphicsDevice _graphicsDevice;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BoxingViewportAdapter" />.
+ /// </summary>
+ public BoxingViewportAdapter(GameWindow window, GraphicsDevice graphicsDevice, int virtualWidth, int virtualHeight, int horizontalBleed = 0, int verticalBleed = 0)
+ : base(graphicsDevice, virtualWidth, virtualHeight)
+ {
+ _window = window;
+ _graphicsDevice = graphicsDevice;
+ window.ClientSizeChanged += OnClientSizeChanged;
+ HorizontalBleed = horizontalBleed;
+ VerticalBleed = verticalBleed;
+ }
+
+ public override void Dispose()
+ {
+ _window.ClientSizeChanged -= OnClientSizeChanged;
+ base.Dispose();
+ }
+
+ /// <summary>
+ /// Size of horizontal bleed areas (from left and right edges) which can be safely cut off
+ /// </summary>
+ public int HorizontalBleed { get; }
+
+ /// <summary>
+ /// Size of vertical bleed areas (from top and bottom edges) which can be safely cut off
+ /// </summary>
+ public int VerticalBleed { get; }
+
+ public BoxingMode BoxingMode { get; private set; }
+
+ private void OnClientSizeChanged(object sender, EventArgs eventArgs)
+ {
+ var clientBounds = _window.ClientBounds;
+
+ var worldScaleX = (float)clientBounds.Width / VirtualWidth;
+ var worldScaleY = (float)clientBounds.Height / VirtualHeight;
+
+ var safeScaleX = (float)clientBounds.Width / (VirtualWidth - HorizontalBleed);
+ var safeScaleY = (float)clientBounds.Height / (VirtualHeight - VerticalBleed);
+
+ var worldScale = MathHelper.Max(worldScaleX, worldScaleY);
+ var safeScale = MathHelper.Min(safeScaleX, safeScaleY);
+ var scale = MathHelper.Min(worldScale, safeScale);
+
+ var width = (int)(scale * VirtualWidth + 0.5f);
+ var height = (int)(scale * VirtualHeight + 0.5f);
+
+ if (height >= clientBounds.Height && width < clientBounds.Width)
+ BoxingMode = BoxingMode.Pillarbox;
+ else
+ {
+ if (width >= clientBounds.Height && height <= clientBounds.Height)
+ BoxingMode = BoxingMode.Letterbox;
+ else
+ BoxingMode = BoxingMode.None;
+ }
+
+ var x = clientBounds.Width / 2 - width / 2;
+ var y = clientBounds.Height / 2 - height / 2;
+ GraphicsDevice.Viewport = new Viewport(x, y, width, height);
+ }
+
+ public override void Reset()
+ {
+ base.Reset();
+ OnClientSizeChanged(this, EventArgs.Empty);
+ }
+
+ public override Point PointToScreen(int x, int y)
+ {
+ var viewport = GraphicsDevice.Viewport;
+ return base.PointToScreen(x - viewport.X, y - viewport.Y);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/DefaultViewportAdapter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/DefaultViewportAdapter.cs
new file mode 100644
index 0000000..9026ce4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/DefaultViewportAdapter.cs
@@ -0,0 +1,28 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+// ReSharper disable once CheckNamespace
+
+namespace MonoGame.Extended.ViewportAdapters
+{
+ public class DefaultViewportAdapter : ViewportAdapter
+ {
+ private readonly GraphicsDevice _graphicsDevice;
+
+ public DefaultViewportAdapter(GraphicsDevice graphicsDevice)
+ : base(graphicsDevice)
+ {
+ _graphicsDevice = graphicsDevice;
+ }
+
+ public override int VirtualWidth => _graphicsDevice.Viewport.Width;
+ public override int VirtualHeight => _graphicsDevice.Viewport.Height;
+ public override int ViewportWidth => _graphicsDevice.Viewport.Width;
+ public override int ViewportHeight => _graphicsDevice.Viewport.Height;
+
+ public override Matrix GetScaleMatrix()
+ {
+ return Matrix.Identity;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ScalingViewportAdapter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ScalingViewportAdapter.cs
new file mode 100644
index 0000000..674a62e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ScalingViewportAdapter.cs
@@ -0,0 +1,27 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.ViewportAdapters
+{
+ public class ScalingViewportAdapter : ViewportAdapter
+ {
+ public ScalingViewportAdapter(GraphicsDevice graphicsDevice, int virtualWidth, int virtualHeight)
+ : base(graphicsDevice)
+ {
+ VirtualWidth = virtualWidth;
+ VirtualHeight = virtualHeight;
+ }
+
+ public override int VirtualWidth { get; }
+ public override int VirtualHeight { get; }
+ public override int ViewportWidth => GraphicsDevice.Viewport.Width;
+ public override int ViewportHeight => GraphicsDevice.Viewport.Height;
+
+ public override Matrix GetScaleMatrix()
+ {
+ var scaleX = (float) ViewportWidth/VirtualWidth;
+ var scaleY = (float) ViewportHeight/VirtualHeight;
+ return Matrix.CreateScale(scaleX, scaleY, 1.0f);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ViewportAdapter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ViewportAdapter.cs
new file mode 100644
index 0000000..eb0491e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ViewportAdapter.cs
@@ -0,0 +1,46 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.ViewportAdapters
+{
+ public abstract class ViewportAdapter : IDisposable
+ {
+ protected ViewportAdapter(GraphicsDevice graphicsDevice)
+ {
+ GraphicsDevice = graphicsDevice;
+ }
+
+ public virtual void Dispose()
+ {
+ }
+
+ public GraphicsDevice GraphicsDevice { get; }
+ public Viewport Viewport => GraphicsDevice.Viewport;
+
+ public abstract int VirtualWidth { get; }
+ public abstract int VirtualHeight { get; }
+ public abstract int ViewportWidth { get; }
+ public abstract int ViewportHeight { get; }
+
+ public Rectangle BoundingRectangle => new Rectangle(0, 0, VirtualWidth, VirtualHeight);
+ public Point Center => BoundingRectangle.Center;
+ public abstract Matrix GetScaleMatrix();
+
+ public Point PointToScreen(Point point)
+ {
+ return PointToScreen(point.X, point.Y);
+ }
+
+ public virtual Point PointToScreen(int x, int y)
+ {
+ var scaleMatrix = GetScaleMatrix();
+ var invertedMatrix = Matrix.Invert(scaleMatrix);
+ return Vector2.Transform(new Vector2(x, y), invertedMatrix).ToPoint();
+ }
+
+ public virtual void Reset()
+ {
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/WindowViewportAdapter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/WindowViewportAdapter.cs
new file mode 100644
index 0000000..b995887
--- /dev/null
+++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/WindowViewportAdapter.cs
@@ -0,0 +1,36 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.ViewportAdapters
+{
+ public class WindowViewportAdapter : ViewportAdapter
+ {
+ protected readonly GameWindow Window;
+
+ public WindowViewportAdapter(GameWindow window, GraphicsDevice graphicsDevice)
+ : base(graphicsDevice)
+ {
+ Window = window;
+ window.ClientSizeChanged += OnClientSizeChanged;
+ }
+
+ public override int ViewportWidth => Window.ClientBounds.Width;
+ public override int ViewportHeight => Window.ClientBounds.Height;
+ public override int VirtualWidth => Window.ClientBounds.Width;
+ public override int VirtualHeight => Window.ClientBounds.Height;
+
+ public override Matrix GetScaleMatrix()
+ {
+ return Matrix.Identity;
+ }
+
+ private void OnClientSizeChanged(object sender, EventArgs eventArgs)
+ {
+ var x = Window.ClientBounds.Width;
+ var y = Window.ClientBounds.Height;
+
+ GraphicsDevice.Viewport = new Viewport(0, 0, x, y);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/Directory.Build.props b/Plugins/MonoGame.Extended/tests/Directory.Build.props
new file mode 100644
index 0000000..06b0a41
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/Directory.Build.props
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project>
+
+ <Import Project="$(MSBuildThisFileDirectory)..\Directory.Build.props" />
+
+ <PropertyGroup>
+ <ArtifactsPath>$(SolutionDirectory).artifacts/tests</ArtifactsPath>
+ <ProjectCategory>tests</ProjectCategory>
+ <GenerateDocumentationFile>false</GenerateDocumentationFile>
+ <IsPackable>false</IsPackable>
+ <NoWarn>CA1707</NoWarn>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="MonoGame.Framework.DesktopGL"
+ Version="3.8.1.303" />
+
+ <PackageReference Include="NSubstitute"
+ Version="4.2.2" />
+
+ <PackageReference Include="xunit"
+ Version="2.6.2"
+ IsImplicitlyDefined="true" />
+
+ <PackageReference Include="Microsoft.NET.Test.Sdk"
+ Version="17.6.0"
+ IsImplicitlyDefined="true" />
+
+ <PackageReference Include="xunit.runner.visualstudio"
+ Version="2.5.6"
+ IsImplicitlyDefined="true"
+ IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive"
+ PrivateAssets="all" />
+
+ <PackageReference Include="coverlet.collector"
+ Version="6.0.0"
+ IsImplicitlyDefined="true"
+ IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive"
+ PrivateAssets="all" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Using Include="Xunit" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/CollisionComponentTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/CollisionComponentTests.cs
new file mode 100644
index 0000000..39ac707
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/CollisionComponentTests.cs
@@ -0,0 +1,416 @@
+using System;
+using Microsoft.Xna.Framework;
+using Xunit;
+
+namespace MonoGame.Extended.Collisions.Tests
+{
+ using MonoGame.Extended.Collisions.Layers;
+
+ /// <summary>
+ /// Test collision of actors with various shapes.
+ /// </summary>
+ /// <remarks>
+ /// Uses the fact that <see cref="BasicActor"/> moves itself away from
+ /// <see cref="BasicWall"/> on collision.
+ /// </remarks>
+ public class CollisionComponentTests
+ {
+ private readonly CollisionComponent _collisionComponent;
+ private readonly GameTime _gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromMilliseconds(16));
+
+ public CollisionComponentTests()
+ {
+ _collisionComponent = new CollisionComponent(new RectangleF(Point2.Zero, new Point2(10, 10)));
+ }
+
+ #region Circle Circle
+
+ [Fact]
+ public void PenetrationVectorSameCircleTest()
+ {
+ Point2 pos1 = Point2.Zero;
+ Point2 pos2 = Point2.Zero;
+
+ IShapeF shape1 = new CircleF(pos1, 2.0f);
+ IShapeF shape2 = new CircleF(pos2, 2.0f);
+
+ var actor1 = new BasicActor()
+ {
+ Position = pos1,
+ Bounds = shape1
+ };
+ var actor2 = new BasicWall()
+ {
+ Position = pos2,
+ Bounds = shape2
+ };
+
+ Assert.True(shape1.Intersects(shape2));
+ _collisionComponent.Insert(actor1);
+ _collisionComponent.Insert(actor2);
+ _collisionComponent.Update(_gameTime);
+ Assert.True(Math.Abs(actor1.Position.Y - -4f) < float.Epsilon);
+ }
+
+ [Fact]
+ public void PenetrationVectorSlightlyOverlappingCircleTest()
+ {
+ Point2 pos1 = new Point2(0, 1.5f);
+ Point2 pos2 = Point2.Zero;
+
+ IShapeF shape1 = new CircleF(pos1, 2.0f);
+ IShapeF shape2 = new CircleF(pos2, 2.0f);
+
+ var actor1 = new BasicActor()
+ {
+ Position = pos1,
+ Bounds = shape1
+ };
+ var actor2 = new BasicWall()
+ {
+ Position = pos2,
+ Bounds = shape2
+ };
+
+ Assert.True(shape1.Intersects(shape2));
+ _collisionComponent.Insert(actor1);
+ _collisionComponent.Insert(actor2);
+ _collisionComponent.Update(_gameTime);
+ // Actor should have moved up because the distance is shorter.
+ Assert.True(actor1.Position.Y > actor2.Position.Y);
+ // The circle centers should be about 4 units away after moving
+ Assert.True(Math.Abs(actor1.Position.Y - 4.0f) < float.Epsilon);
+ }
+
+ [Fact]
+ public void PenetrationVectorSlightlyOverlappingOffAxisTest()
+ {
+ Point2 pos1 = new Point2(2, 2.5f);
+ Point2 pos2 = new Point2(2, 1);
+
+ IShapeF shape1 = new CircleF(pos1, 2.0f);
+ IShapeF shape2 = new CircleF(pos2, 2.0f);
+
+ var actor1 = new BasicActor()
+ {
+ Position = pos1,
+ Bounds = shape1
+ };
+ var actor2 = new BasicWall()
+ {
+ Position = pos2,
+ Bounds = shape2
+ };
+
+ Assert.True(shape1.Intersects(shape2));
+ _collisionComponent.Insert(actor1);
+ _collisionComponent.Insert(actor2);
+ _collisionComponent.Update(_gameTime);
+ // Actor should have moved up because the distance is shorter.
+ Assert.True(actor1.Position.Y > actor2.Position.Y);
+ // The circle centers should be about 4 units away after moving
+ Assert.True(Math.Abs(actor1.Position.Y - 5.0f) < float.Epsilon);
+ Assert.True(Math.Abs(actor1.Position.X - 2.0f) < float.Epsilon);
+ }
+
+ [Fact]
+ public void PenetrationZeroRadiusCircleCircleTest()
+ {
+ Point2 pos1 = new Point2(0, 1.5f);
+ Point2 pos2 = Point2.Zero;
+
+ IShapeF shape1 = new CircleF(pos1, 0);
+ IShapeF shape2 = new CircleF(pos2, 2.0f);
+
+ var actor1 = new BasicActor()
+ {
+ Position = pos1,
+ Bounds = shape1
+ };
+ var actor2 = new BasicWall()
+ {
+ Position = pos2,
+ Bounds = shape2
+ };
+
+ Assert.True(shape1.Intersects(shape2));
+ _collisionComponent.Insert(actor1);
+ _collisionComponent.Insert(actor2);
+ _collisionComponent.Update(_gameTime);
+ // Actor should have moved up because the distance is shorter.
+ Assert.True(actor1.Position.Y > actor2.Position.Y);
+ // The circle centers should be about 4 units away after moving
+ // Assert.True(Math.Abs(actor1.Position.Y - 2.0f) < float.Epsilon);
+ }
+
+ #endregion
+
+ #region Circle Rectangle
+
+ [Fact]
+ public void PenetrationVectorCircleRectangleTest()
+ {
+ Point2 pos1 = new Point2(0, 1);
+ Point2 pos2 = new Point2(-2, -1);
+
+ IShapeF shape1 = new CircleF(pos1, 2.0f);
+ IShapeF shape2 = new RectangleF(pos2, new Size2(4, 2));
+
+
+ var actor1 = new BasicActor()
+ {
+ Position = pos1,
+ Bounds = shape1
+ };
+ var actor2 = new BasicWall()
+ {
+ Position = pos2,
+ Bounds = shape2
+ };
+
+ Assert.True(shape1.Intersects(shape2));
+ _collisionComponent.Insert(actor1);
+ _collisionComponent.Insert(actor2);
+ _collisionComponent.Update(_gameTime);
+ Assert.True(Math.Abs(actor1.Position.X - 0.0f) < float.Epsilon);
+ Assert.True(Math.Abs(actor1.Position.Y - 3.0f) < float.Epsilon);
+ }
+
+ [Fact]
+ public void PenetrationVectorCircleContainedInRectangleTest()
+ {
+ Point2 pos1 = new Point2(0, 0);
+ Point2 pos2 = new Point2(-2, -1);
+
+ IShapeF shape1 = new CircleF(pos1, 1.0f);
+ IShapeF shape2 = new RectangleF(pos2, new Size2(4, 2));
+
+
+ var actor1 = new BasicActor()
+ {
+ Position = pos1,
+ Bounds = shape1
+ };
+ var actor2 = new BasicWall()
+ {
+ Position = pos2,
+ Bounds = shape2
+ };
+
+ Assert.True(shape1.Intersects(shape2));
+ _collisionComponent.Insert(actor1);
+ _collisionComponent.Insert(actor2);
+ _collisionComponent.Update(_gameTime);
+ Assert.True(Math.Abs(actor1.Position.X - 0.0f) < float.Epsilon);
+ Assert.True(Math.Abs(actor1.Position.Y - -2.0f) < float.Epsilon);
+ }
+
+ [Fact]
+ public void PenetrationVectorCircleOffAxisRectangleTest()
+ {
+ Point2 pos1 = new Point2(2, 1);
+ Point2 pos2 = new Point2(-2, -1);
+
+ IShapeF shape1 = new CircleF(pos1, 2.0f);
+ IShapeF shape2 = new RectangleF(pos2, new Size2(4, 2));
+
+
+ var actor1 = new BasicActor()
+ {
+ Position = pos1,
+ Bounds = shape1
+ };
+ var actor2 = new BasicWall()
+ {
+ Position = pos2,
+ Bounds = shape2
+ };
+
+ Assert.True(shape1.Intersects(shape2));
+ _collisionComponent.Insert(actor1);
+ _collisionComponent.Insert(actor2);
+ _collisionComponent.Update(_gameTime);
+ Assert.True(Math.Abs(actor1.Position.X - 2.0f) < float.Epsilon);
+ Assert.True(Math.Abs(actor1.Position.Y - 3.0f) < float.Epsilon);
+ }
+
+ #endregion
+
+ #region Rectangle Rectangle
+
+ [Fact]
+ public void PenetrationVectorRectangleRectangleTest()
+ {
+ Point2 pos1 = new Point2(0, 0);
+ Point2 pos2 = new Point2(-2, -1);
+
+ IShapeF shape1 = new RectangleF(pos1, new Size2(4, 2));
+ IShapeF shape2 = new RectangleF(pos2, new Size2(4, 2));
+
+
+ var actor1 = new BasicActor()
+ {
+ Position = pos1,
+ Bounds = shape1
+ };
+ var actor2 = new BasicWall()
+ {
+ Position = pos2,
+ Bounds = shape2
+ };
+
+ Assert.True(shape1.Intersects(shape2));
+ _collisionComponent.Insert(actor1);
+ _collisionComponent.Insert(actor2);
+ _collisionComponent.Update(_gameTime);
+ Assert.True(Math.Abs(actor1.Position.X - 0.0f) < float.Epsilon);
+ Assert.True(Math.Abs(actor1.Position.Y - 1.0f) < float.Epsilon);
+ }
+
+ [Fact]
+ public void PenetrationVectorRectangleRectangleOffAxisTest()
+ {
+ Point2 pos1 = new Point2(4, 2);
+ Point2 pos2 = new Point2(3, 1);
+
+ IShapeF shape1 = new RectangleF(pos1, new Size2(4, 2));
+ IShapeF shape2 = new RectangleF(pos2, new Size2(4, 2));
+
+
+ var actor1 = new BasicActor()
+ {
+ Position = pos1,
+ Bounds = shape1
+ };
+ var actor2 = new BasicWall()
+ {
+ Position = pos2,
+ Bounds = shape2
+ };
+
+ Assert.True(shape1.Intersects(shape2));
+ _collisionComponent.Insert(actor1);
+ _collisionComponent.Insert(actor2);
+ _collisionComponent.Update(_gameTime);
+ Assert.True(Math.Abs(actor1.Position.X - 4.0f) < float.Epsilon);
+ Assert.True(Math.Abs(actor1.Position.Y - 3.0f) < float.Epsilon);
+ }
+
+ #endregion
+
+ public class Behaviours : CollisionComponentTests
+ {
+ [Fact]
+ public void Actors_is_colliding()
+ {
+ var staticBounds = new RectangleF(new Point2(0, 0), new Size2(1, 1));
+ var anotherStaticBounds = new RectangleF(new Point2(0, 0), new Size2(1, 1));
+ var staticActor = new CollisionIndicatingActor(staticBounds);
+ var anotherStaticActor = new CollisionIndicatingActor(anotherStaticBounds);
+ _collisionComponent.Insert(staticActor);
+ _collisionComponent.Insert(anotherStaticActor);
+
+ _collisionComponent.Update(_gameTime);
+
+ Assert.True(staticActor.IsColliding);
+ Assert.True(anotherStaticActor.IsColliding);
+ }
+
+ [Fact]
+ public void Actors_is_not_colliding_when_dynamic_actor_is_moved_out_of_collision_bounds()
+ {
+ var staticBounds = new RectangleF(new Point2(0, 0), new Size2(1, 1));
+ var dynamicBounds = new RectangleF(new Point2(0, 0), new Size2(1, 1));
+ var staticActor = new CollisionIndicatingActor(staticBounds);
+ var dynamicActor = new CollisionIndicatingActor(dynamicBounds);
+ _collisionComponent.Insert(staticActor);
+ _collisionComponent.Insert(dynamicActor);
+ dynamicActor.MoveTo(new Point2(2, 2));
+
+ _collisionComponent.Update(_gameTime);
+
+ Assert.False(staticActor.IsColliding);
+ Assert.False(dynamicActor.IsColliding);
+ }
+
+ [Fact]
+ public void Actors_is_colliding_when_dynamic_actor_is_moved_after_update()
+ {
+ var staticBounds = new RectangleF(new Point2(0, 0), new Size2(1, 1));
+ var staticActor = new CollisionIndicatingActor(staticBounds);
+ _collisionComponent.Insert(staticActor);
+ for (int i = 0; i < QuadTree.QuadTree.DefaultMaxObjectsPerNode; i++)
+ {
+ var fillerBounds = new RectangleF(new Point2(0, 2), new Size2(.1f, .1f));
+ var fillerActor = new CollisionIndicatingActor(fillerBounds);
+ _collisionComponent.Insert(fillerActor);
+ }
+
+ var dynamicBounds = new RectangleF(new Point2(2, 2), new Size2(1, 1));
+ var dynamicActor = new CollisionIndicatingActor(dynamicBounds);
+ _collisionComponent.Insert(dynamicActor);
+
+ _collisionComponent.Update(_gameTime);
+ Assert.False(staticActor.IsColliding);
+ Assert.False(dynamicActor.IsColliding);
+
+ dynamicActor.MoveTo(new Point2(0, 0));
+
+ _collisionComponent.Update(_gameTime);
+ Assert.True(dynamicActor.IsColliding);
+ Assert.True(staticActor.IsColliding);
+ }
+
+ [Fact]
+ public void Actors_is_colliding_when_dynamic_actor_is_moved_into_collision_bounds()
+ {
+ var staticBounds = new RectangleF(new Point2(0, 0), new Size2(1, 1));
+ var dynamicBounds = new RectangleF(new Point2(2, 2), new Size2(1, 1));
+ var staticActor = new CollisionIndicatingActor(staticBounds);
+ var dynamicActor = new CollisionIndicatingActor(dynamicBounds);
+ _collisionComponent.Insert(staticActor);
+ _collisionComponent.Insert(dynamicActor);
+ dynamicActor.MoveTo(new Point2(0, 0));
+
+ _collisionComponent.Update(_gameTime);
+
+ Assert.True(staticActor.IsColliding);
+ Assert.True(dynamicActor.IsColliding);
+ }
+
+ [Fact]
+ public void InsertActor_ThrowsUndefinedLayerException_IfThereIsNoLayerDefined()
+ {
+ var sut = new CollisionComponent();
+
+ var act = () => sut.Insert(new CollisionIndicatingActor(RectangleF.Empty));
+
+ Assert.Throws<UndefinedLayerException>(act);
+ }
+
+ private class CollisionIndicatingActor : ICollisionActor
+ {
+ private RectangleF _bounds;
+
+ public CollisionIndicatingActor(RectangleF bounds)
+ {
+ _bounds = bounds;
+ }
+
+ public IShapeF Bounds => _bounds;
+
+ public void OnCollision(CollisionEventArgs collisionInfo)
+ {
+ IsColliding = true;
+ }
+
+ public bool IsColliding { get; private set; }
+
+ public void MoveTo(Point2 position)
+ {
+ _bounds = new RectangleF(position, _bounds.Size);
+ }
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/Implementation/BasicActor.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/Implementation/BasicActor.cs
new file mode 100644
index 0000000..fe56fdc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/Implementation/BasicActor.cs
@@ -0,0 +1,33 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Collisions.Tests
+{
+ public class BasicActor : ICollisionActor
+ {
+ public Vector2 Position { get; set; }
+ public IShapeF Bounds { get; set; }
+ public Vector2 Velocity { get; set; }
+
+ public BasicActor()
+ {
+ Bounds = new RectangleF(0f, 0f, 1f, 1f);
+ }
+ public void OnCollision(CollisionEventArgs collisionInfo)
+ {
+ Bounds.Position -= collisionInfo.PenetrationVector;
+ Position -= collisionInfo.PenetrationVector;
+
+ if (collisionInfo.Other is BasicActor)
+ {
+ CollisionCount++;
+ }
+ else
+ {
+ Console.WriteLine(collisionInfo.Other.GetType().Name);
+ }
+ }
+
+ public int CollisionCount { get; set; }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/Implementation/BasicWall.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/Implementation/BasicWall.cs
new file mode 100644
index 0000000..3492735
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/Implementation/BasicWall.cs
@@ -0,0 +1,20 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Collisions.Tests
+{
+ public class BasicWall : ICollisionActor
+ {
+ public Vector2 Position { get; set; }
+ public IShapeF Bounds { get; set; }
+ public Vector2 Velocity { get; set; }
+
+ public BasicWall()
+ {
+ Bounds = new RectangleF(0f, 0f, 1f, 1f);
+ }
+ public void OnCollision(CollisionEventArgs collisionInfo)
+ {
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/MonoGame.Extended.Collisions.Tests.csproj b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/MonoGame.Extended.Collisions.Tests.csproj
new file mode 100644
index 0000000..c62c1cd
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/MonoGame.Extended.Collisions.Tests.csproj
@@ -0,0 +1,7 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\source\MonoGame.Extended.Collisions\MonoGame.Extended.Collisions.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/QuadTreeTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/QuadTreeTests.cs
new file mode 100644
index 0000000..1013288
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/QuadTreeTests.cs
@@ -0,0 +1,446 @@
+using System.Collections.Generic;
+using MonoGame.Extended.Collisions.QuadTree;
+using Xunit;
+
+namespace MonoGame.Extended.Collisions.Tests
+{
+ public class QuadTreeTests
+ {
+ private QuadTree.QuadTree MakeTree()
+ {
+ // Bounds set to ensure actors will fit inside the tree with default bounds.
+ var bounds = _quadTreeArea;
+ var tree = new QuadTree.QuadTree(bounds);
+
+ return tree;
+ }
+
+ private RectangleF _quadTreeArea = new RectangleF(-10f, -15, 20.0f, 30.0f);
+
+ [Fact]
+ public void ConstructorTest()
+ {
+ var bounds = new RectangleF(-10f, -15, 20.0f, 30.0f);
+ var tree = new QuadTree.QuadTree(bounds);
+
+ Assert.Equal(bounds, tree.NodeBounds);
+ Assert.True(tree.IsLeaf);
+ }
+
+ [Fact]
+ public void NumTargetsEmptyTest()
+ {
+ var tree = MakeTree();
+
+ Assert.Equal(0, tree.NumTargets());
+ }
+
+ [Fact]
+ public void NumTargetsOneTest()
+ {
+ var tree = MakeTree();
+ var actor = new BasicActor();
+
+ tree.Insert(new QuadtreeData(actor));
+
+ Assert.Equal(1, tree.NumTargets());
+ }
+
+
+ [Fact]
+ public void NumTargetsMultipleTest()
+ {
+ var tree = MakeTree();
+ for (int i = 0; i < 5; i++)
+ {
+ tree.Insert(new QuadtreeData(new BasicActor()));
+ }
+
+ Assert.Equal(5, tree.NumTargets());
+ }
+
+ [Fact]
+ public void NumTargetsManyTest()
+ {
+ var tree = MakeTree();
+ for (int i = 0; i < 1000; i++)
+ {
+ tree.Insert(new QuadtreeData(new BasicActor()));
+ Assert.Equal(i + 1, tree.NumTargets());
+ }
+
+ Assert.Equal(1000, tree.NumTargets());
+ }
+
+ [Fact]
+ public void InsertOneTest()
+ {
+ var tree = MakeTree();
+ var actor = new BasicActor();
+
+ tree.Insert(new QuadtreeData(actor));
+
+ Assert.Equal(1, tree.NumTargets());
+ }
+
+ [Fact]
+ public void InsertOneOverlappingQuadrantsTest()
+ {
+ var tree = MakeTree();
+ var actor = new BasicActor
+ {
+ Bounds = new RectangleF(-2.5f, -2.5f, 5f, 5f)
+ };
+
+ tree.Insert(new QuadtreeData(actor));
+
+ Assert.Equal(1, tree.NumTargets());
+ }
+
+ [Fact]
+ public void InsertMultipleTest()
+ {
+ var tree = MakeTree();
+
+ for (int i = 0; i < 10; i++)
+ {
+ tree.Insert(new QuadtreeData(new BasicActor()
+ {
+ Bounds = new RectangleF(0, 0, 1, 1)
+ }));
+ }
+
+ Assert.Equal(10, tree.NumTargets());
+ }
+
+ [Fact]
+ public void InsertManyTest()
+ {
+ var tree = MakeTree();
+
+ for (int i = 0; i < 1000; i++)
+ {
+ tree.Insert(new QuadtreeData(new BasicActor()
+ {
+ Bounds = new RectangleF(0, 0, 1, 1)
+ }));
+ }
+
+ Assert.Equal(1000, tree.NumTargets());
+ }
+
+ [Fact]
+ public void InsertMultipleOverlappingQuadrantsTest()
+ {
+ var tree = MakeTree();
+
+ for (int i = 0; i < 10; i++)
+ {
+ var actor = new BasicActor()
+ {
+ Bounds = new RectangleF(-10f, -15, 20.0f, 30.0f)
+ };
+ tree.Insert(new QuadtreeData(actor));
+ }
+
+ Assert.Equal(10, tree.NumTargets());
+ }
+
+ [Fact]
+ public void RemoveToEmptyTest()
+ {
+ var actor = new BasicActor()
+ {
+ Bounds = new RectangleF(-5f, -7f, 10.0f, 15.0f)
+ };
+ var data = new QuadtreeData(actor);
+
+ var tree = MakeTree();
+ tree.Insert(data);
+
+ tree.Remove(data);
+
+ Assert.Equal(0, tree.NumTargets());
+ }
+
+ [Fact]
+ public void RemoveTwoTest()
+ {
+ var tree = MakeTree();
+ var inserted = new List<QuadtreeData>();
+ var numTargets = 2;
+
+ for (int i = 0; i < numTargets; i++)
+ {
+ var data = new QuadtreeData(new BasicActor()
+ {
+ Bounds = new RectangleF(0, 0, 1, 1)
+ });
+ tree.Insert(data);
+ inserted.Add(data);
+ }
+
+
+ var inTree = numTargets;
+ Assert.Equal(inTree, tree.NumTargets());
+
+ foreach (var data in inserted)
+ {
+ tree.Remove(data);
+ Assert.Equal(--inTree, tree.NumTargets());
+ }
+ }
+
+ [Fact]
+ public void RemoveThreeTest()
+ {
+ var tree = MakeTree();
+ var inserted = new List<QuadtreeData>();
+ var numTargets = 3;
+
+ for (int i = 0; i < numTargets; i++)
+ {
+ var data = new QuadtreeData(new BasicActor()
+ {
+ Bounds = new RectangleF(0, 0, 1, 1)
+ });
+ tree.Insert(data);
+ inserted.Add(data);
+ }
+
+
+ var inTree = numTargets;
+ Assert.Equal(inTree, tree.NumTargets());
+
+ foreach (var data in inserted)
+ {
+ tree.Remove(data);
+ Assert.Equal(--inTree, tree.NumTargets());
+ }
+ }
+
+ [Fact]
+ public void RemoveManyTest()
+ {
+ var tree = MakeTree();
+ var inserted = new List<QuadtreeData>();
+ var numTargets = 1000;
+
+ for (int i = 0; i < numTargets; i++)
+ {
+ var data = new QuadtreeData(new BasicActor()
+ {
+ Bounds = new RectangleF(0, 0, 1, 1)
+ });
+ tree.Insert(data);
+ inserted.Add(data);
+ }
+
+
+ var inTree = numTargets;
+ Assert.Equal(inTree, tree.NumTargets());
+
+ foreach (var data in inserted)
+ {
+ data.RemoveFromAllParents();
+ Assert.Equal(--inTree, tree.NumTargets());
+ }
+ }
+
+
+ [Fact]
+ public void ShakeWhenEmptyTest()
+ {
+ var tree = MakeTree();
+ tree.Shake();
+
+ Assert.Equal(0, tree.NumTargets());
+ }
+
+ [Fact]
+ public void ShakeAfterSplittingWhenEmptyTest()
+ {
+ var tree = MakeTree();
+
+ tree.Split();
+ tree.Shake();
+ Assert.Equal(0, tree.NumTargets());
+ }
+
+ [Fact]
+ public void ShakeAfterSplittingNotEmptyTest()
+ {
+ var tree = MakeTree();
+
+ tree.Split();
+ var data = new QuadtreeData(new BasicActor());
+ tree.Insert(data);
+ tree.Shake();
+ Assert.Equal(1, tree.NumTargets());
+ }
+
+ [Fact]
+ public void ShakeWhenContainingOneTest()
+ {
+ var tree = MakeTree();
+ var numTargets = 1;
+
+ for (int i = 0; i < numTargets; i++)
+ {
+ var data = new QuadtreeData(new BasicActor());
+ tree.Insert(data);
+ }
+
+ tree.Shake();
+ Assert.Equal(numTargets, tree.NumTargets());
+ }
+
+ [Fact]
+ public void ShakeWhenContainingTwoTest()
+ {
+ var tree = MakeTree();
+ var numTargets = 2;
+
+ for (int i = 0; i < numTargets; i++)
+ {
+ var data = new QuadtreeData(new BasicActor());
+ tree.Insert(data);
+ }
+
+ tree.Shake();
+ Assert.Equal(numTargets, tree.NumTargets());
+ }
+
+ [Fact]
+ public void ShakeWhenContainingThreeTest()
+ {
+ var tree = MakeTree();
+ var numTargets = 3;
+
+ for (int i = 0; i < numTargets; i++)
+ {
+ var data = new QuadtreeData(new BasicActor());
+ tree.Insert(data);
+ }
+
+ tree.Shake();
+ Assert.Equal(numTargets, tree.NumTargets());
+ }
+
+ [Fact]
+ public void ShakeWhenContainingManyTest()
+ {
+ var tree = MakeTree();
+ var numTargets = QuadTree.QuadTree.DefaultMaxObjectsPerNode + 1;
+
+ for (int i = 0; i < numTargets; i++)
+ {
+ var data = new QuadtreeData(new BasicActor());
+ tree.Insert(data);
+ }
+
+ tree.Shake();
+ Assert.Equal(numTargets, tree.NumTargets());
+ }
+
+ [Fact]
+ public void QueryWhenEmptyTest()
+ {
+ var tree = MakeTree();
+
+ var query = tree.Query(ref _quadTreeArea);
+
+ Assert.Empty(query);
+ Assert.Equal(0, tree.NumTargets());
+ }
+
+ [Fact]
+ public void QueryNotOverlappingTest()
+ {
+ var tree = MakeTree();
+
+ var area = new RectangleF(100f, 100f, 1f, 1f);
+ var query = tree.Query(ref area);
+
+ Assert.Empty(query);
+ Assert.Equal(0, tree.NumTargets());
+ }
+
+ [Fact]
+ public void QueryLeafNodeNotEmptyTest()
+ {
+ var tree = MakeTree();
+ var actor = new BasicActor();
+ tree.Insert(new QuadtreeData(actor));
+
+ var query = tree.Query(ref _quadTreeArea);
+ Assert.Single(query);
+ Assert.Equal(tree.NumTargets(), query.Count);
+ }
+
+ [Fact]
+ public void QueryLeafNodeNoOverlapTest()
+ {
+ var tree = MakeTree();
+ var actor = new BasicActor();
+ tree.Insert(new QuadtreeData(actor));
+
+ var area = new RectangleF(100f, 100f, 1f, 1f);
+ var query = tree.Query(ref area);
+ Assert.Empty(query);
+ }
+
+ [Fact]
+ public void QueryLeafNodeMultipleTest()
+ {
+ var tree = MakeTree();
+ var numTargets = QuadTree.QuadTree.DefaultMaxObjectsPerNode;
+ for (int i = 0; i < numTargets; i++)
+ {
+ var data = new QuadtreeData(new BasicActor());
+ tree.Insert(data);
+ }
+
+
+ var query = tree.Query(ref _quadTreeArea);
+ Assert.Equal(numTargets, query.Count);
+ Assert.Equal(tree.NumTargets(), query.Count);
+ }
+
+ [Fact]
+ public void QueryNonLeafManyTest()
+ {
+ var tree = MakeTree();
+ var numTargets = 2*QuadTree.QuadTree.DefaultMaxObjectsPerNode;
+ for (int i = 0; i < numTargets; i++)
+ {
+ var data = new QuadtreeData(new BasicActor());
+ tree.Insert(data);
+ }
+
+
+ var query = tree.Query(ref _quadTreeArea);
+ Assert.Equal(numTargets, query.Count);
+ Assert.Equal(tree.NumTargets(), query.Count);
+ }
+
+ [Fact]
+ public void QueryTwiceConsecutiveTest()
+ {
+ var tree = MakeTree();
+ var numTargets = 2 * QuadTree.QuadTree.DefaultMaxObjectsPerNode;
+ for (int i = 0; i < numTargets; i++)
+ {
+ var data = new QuadtreeData(new BasicActor());
+ tree.Insert(data);
+ }
+
+
+ var query1 = tree.Query(ref _quadTreeArea);
+ var query2 = tree.Query(ref _quadTreeArea);
+ Assert.Equal(numTargets, query1.Count);
+ Assert.Equal(tree.NumTargets(), query1.Count);
+ Assert.Equal(query1.Count, query2.Count);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/SpatialHashTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/SpatialHashTests.cs
new file mode 100644
index 0000000..7c09a9e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/SpatialHashTests.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace MonoGame.Extended.Collisions.Tests;
+
+public class SpatialHashTests
+{
+ private SpatialHash generateSpatialHash() => new SpatialHash(new Size2(64, 64));
+ private readonly RectangleF RECT = new RectangleF(10, 10, 20, 20);
+
+ [Fact]
+ public void CollisionOneTrueTest()
+ {
+ var hash = generateSpatialHash();
+ hash.Insert(new BasicActor()
+ {
+ Bounds = RECT,
+ });
+ var collisions = hash.Query(RECT);
+ Assert.Equal(1, collisions.Count());
+ }
+
+ [Fact]
+ public void CollisionTwoTest()
+ {
+ var hash = generateSpatialHash();
+ hash.Insert(new BasicActor
+ {
+ Bounds = RECT,
+ });
+ hash.Insert(new BasicActor
+ {
+ Bounds = RECT,
+ });
+ var collisions = hash.Query(RECT);
+ Assert.Equal(2, collisions.Count());
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/packages.config b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/packages.config
new file mode 100644
index 0000000..fa95cfc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Collisions.Tests/packages.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="MonoGame.Framework.WindowsDX" version="3.6.0.1625" targetFramework="net452" />
+ <package id="NSubstitute" version="1.10.0.0" targetFramework="net452" />
+ <package id="NUnit" version="2.6.4" targetFramework="net452" />
+</packages> \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/MonoGame.Extended.Content.Pipeline.Tests.Tiled.csproj b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/MonoGame.Extended.Content.Pipeline.Tests.Tiled.csproj
new file mode 100644
index 0000000..9c8f57c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/MonoGame.Extended.Content.Pipeline.Tests.Tiled.csproj
@@ -0,0 +1,24 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <ItemGroup>
+ <PackageReference Include="MonoGame.Framework.Content.Pipeline" Version="3.8.1.303" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\source\MonoGame.Extended.Content.Pipeline\MonoGame.Extended.Content.Pipeline.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Content Include="TestData\isometric.tmx" CopyToOutputDirectory="PreserveNewest" />
+ <Content Include="TestData\isometric_tileset.png" CopyToOutputDirectory="PreserveNewest" />
+ <Content Include="TestData\level01.tmx" CopyToOutputDirectory="PreserveNewest" />
+ <Content Include="TestData\template.tx" CopyToOutputDirectory="PreserveNewest" />
+ <Content Include="TestData\test-object-layer.tmx" CopyToOutputDirectory="PreserveNewest" />
+ <Content Include="TestData\test-tileset-base64.tmx" CopyToOutputDirectory="PreserveNewest" />
+ <Content Include="TestData\test-tileset-csv.tmx" CopyToOutputDirectory="PreserveNewest" />
+ <Content Include="TestData\test-tileset-gzip.tmx" CopyToOutputDirectory="PreserveNewest" />
+ <Content Include="TestData\test-tileset-xml.tmx" CopyToOutputDirectory="PreserveNewest" />
+ <Content Include="TestData\test-tileset-zlib.tmx" CopyToOutputDirectory="PreserveNewest" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/isometric.tmx b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/isometric.tmx
new file mode 100644
index 0000000..3f3168c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/isometric.tmx
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE map SYSTEM "http://mapeditor.org/dtd/1.0/map.dtd">
+<map version="1.0" orientation="isometric" width="40" height="40" tilewidth="64" tileheight="32">
+ <tileset firstgid="1" name="Isometric Tileset" tilewidth="64" tileheight="128">
+ <image source="isometric_tileset.png" width="128" height="128"/>
+ <tile id="0">
+ <properties>
+ <property name="obstacle" value="1"/>
+ </properties>
+ </tile>
+ </tileset>
+ <layer name="Ground" width="40" height="40">
+ <data encoding="csv">
+2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,1,2,2,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,2,2,1,2,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,2,2,1,2,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,2,2,1,2,1,1,2,2,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,2,2,1,2,1,1,2,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,2,1,1,1,2,2,2,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,1,1,1,2,1,1,2,1,0,0,0,0,0,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+1,1,2,2,2,2,1,1,0,0,2,0,0,0,0,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,2,2,2,2,2,1,0,0,0,0,0,0,0,0,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,2,2,2,2,2,2,0,0,0,0,0,0,0,2,0,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+1,1,2,2,2,2,2,2,1,0,0,0,0,0,2,0,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+1,1,1,2,2,2,2,2,1,0,0,0,0,0,0,0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+1,1,1,1,2,2,2,2,2,2,0,0,0,0,0,0,0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,1,0,0,1,1,1,2,2,2,0,0,0,0,0,2,0,0,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,1,0,0,2,1,2,2,2,2,2,0,0,0,0,0,0,0,2,2,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,1,0,2,2,1,1,0,2,2,0,0,0,0,0,0,2,2,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,1,1,2,2,2,1,1,0,2,0,0,0,0,0,0,0,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,2,1,2,2,2,2,2,1,2,0,0,0,0,0,0,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+1,2,2,2,1,2,1,2,2,2,1,1,2,0,0,0,0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+1,1,1,2,2,1,1,1,2,2,2,1,1,2,2,0,0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,2,1,1,0,1,0,1,2,2,2,1,2,2,2,2,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,2,2,1,1,1,1,0,1,1,2,2,1,1,2,2,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,2,2,2,2,1,1,1,2,1,2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+0,0,0,2,2,2,2,2,2,2,2,2,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,0,0,1,2,2,2,2,2,2,2,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,2,0,1,1,2,2,2,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,2,2,2,1,1,1,1,2,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+2,2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2
+</data>
+ </layer>
+ <objectgroup color="#ffaa00" name="Objects" width="40" height="40">
+ <object x="1261" y="428" width="141" height="74"/>
+ <object type="npc" x="498" y="35" width="156" height="42"/>
+ <object x="576" y="224" width="448" height="128"/>
+ <object x="2183" y="747" width="228" height="96"/>
+ <object name="fddsadsa" type="warp" x="960" y="416" width="192" height="96"/>
+ <object x="1100" y="556" width="138" height="23"/>
+ <object x="218" y="217">
+ <polyline points="0,0 14,-90 36,3"/>
+ </object>
+ <object x="243" y="174">
+ <polyline points="0,0 -17,0"/>
+ </object>
+ <object x="302" y="219">
+ <polyline points="0,0 -2,-91 -36,-94 28,-91"/>
+ </object>
+ <object x="357" y="125">
+ <polyline points="0,0 7,96 41,98"/>
+ </object>
+ </objectgroup>
+</map>
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/isometric_tileset.png b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/isometric_tileset.png
new file mode 100644
index 0000000..4c6f1b4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/isometric_tileset.png
Binary files differ
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/level01.tmx b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/level01.tmx
new file mode 100644
index 0000000..39eb900
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/level01.tmx
@@ -0,0 +1,440 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.0" orientation="orthogonal" renderorder="right-down" width="20" height="10" tilewidth="128" tileheight="128" backgroundcolor="#7d7d7d" nextobjectid="1">
+ <properties>
+ <property name="awesome" value="42"/>
+ </properties>
+ <tileset firstgid="1" name="free-tileset" tilewidth="128" tileheight="128" tilecount="30" spacing="2" margin="2">
+ <image source="free-tileset.png" width="652" height="783"/>
+ <tile id="7">
+ <properties>
+ <property name="frog" value="dog"/>
+ </properties>
+ </tile>
+ <tile id="23">
+ <properties>
+ <property name="element" value="box"/>
+ </properties>
+ </tile>
+ <tile id="24">
+ <properties>
+ <property name="hp" value="55"/>
+ <property name="mp" value="16"/>
+ </properties>
+ </tile>
+ </tileset>
+ <layer name="Tile Layer 2" width="20" height="10">
+ <data>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="26"/>
+ <tile gid="21"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="25"/>
+ <tile gid="29"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="21"/>
+ <tile gid="20"/>
+ <tile gid="27"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="20"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="21"/>
+ <tile gid="28"/>
+ <tile gid="25"/>
+ <tile gid="30"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="29"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="18"/>
+ <tile gid="18"/>
+ <tile gid="18"/>
+ <tile gid="18"/>
+ <tile gid="18"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="19"/>
+ <tile gid="19"/>
+ <tile gid="19"/>
+ <tile gid="19"/>
+ <tile gid="19"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="19"/>
+ <tile gid="19"/>
+ <tile gid="19"/>
+ <tile gid="19"/>
+ <tile gid="19"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ </data>
+ </layer>
+ <imagelayer name="Image Layer 1" x="100" y="100">
+ <image source="hills.png"/>
+ </imagelayer>
+ <layer name="Tile Layer 1" width="20" height="10">
+ <properties>
+ <property name="customlayerprop" value="1"/>
+ <property name="customlayerprop2" value="2"/>
+ </properties>
+ <data>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="14"/>
+ <tile gid="15"/>
+ <tile gid="15"/>
+ <tile gid="16"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="14"/>
+ <tile gid="15"/>
+ <tile gid="16"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="24"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="1"/>
+ <tile gid="2"/>
+ <tile gid="2"/>
+ <tile gid="3"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="1"/>
+ <tile gid="2"/>
+ <tile gid="2"/>
+ <tile gid="2"/>
+ <tile gid="2"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="4"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="7"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="4"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="2"/>
+ <tile gid="2"/>
+ <tile gid="2"/>
+ <tile gid="2"/>
+ <tile gid="8"/>
+ <tile gid="9"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="7"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="1"/>
+ <tile gid="2"/>
+ <tile gid="8"/>
+ <tile gid="9"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="7"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="4"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="7"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="0"/>
+ <tile gid="4"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ <tile gid="6"/>
+ </data>
+ </layer>
+</map>
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/template.tx b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/template.tx
new file mode 100644
index 0000000..1f00d19
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/template.tx
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<template>
+ <object width="30.9247" height="20.7597"/>
+</template>
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-object-layer.tmx b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-object-layer.tmx
new file mode 100644
index 0000000..dcdbee9
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-object-layer.tmx
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.0" orientation="orthogonal" renderorder="right-down" width="25" height="15" tilewidth="64" tileheight="64" nextobjectid="12">
+ <objectgroup name="Object Layer 1">
+ <object id="1" x="131.345" y="65.234" width="311.111" height="311.232">
+ <properties>
+ <property name="shape" value="circle"/>
+ </properties>
+ <ellipse/>
+ </object>
+ <object id="7" class="sprite" x="240" y="440" width="322" height="186" visible="0"/>
+ <object id="8" type="rectangle" x="506" y="142" width="136" height="234">
+ <properties>
+ <property name="area" value="player-spawn"/>
+ </properties>
+ </object>
+ <object id="9" name="polygon" x="621" y="450">
+ <polygon points="0,0 180,90 -8,275 -45,81 38,77"/>
+ </object>
+ <object id="11" x="43" y="350">
+ <polyline points="0,0 28,299 326,413 461,308"/>
+ </object>
+ <object id="12" gid="23" x="169.333" y="490.909" width="345.818" height="364"/>
+ </objectgroup>
+</map>
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-tileset-base64.tmx b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-tileset-base64.tmx
new file mode 100644
index 0000000..09177cb
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-tileset-base64.tmx
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.0" orientation="orthogonal" renderorder="left-down" width="3" height="3" tilewidth="32" tileheight="32" nextobjectid="1">
+ <tileset firstgid="1" name="test-tileset" tilewidth="32" tileheight="32" spacing="2" margin="2">
+ <image source="test-tileset.png" width="104" height="104"/>
+ </tileset>
+ <layer name="Tile Layer 1" width="3" height="3">
+ <data encoding="base64">
+ AQAAAAIAAAADAAAABAAAAAUAAAAGAAAABwAAAAgAAAAJAAAA
+ </data>
+ </layer>
+</map>
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-tileset-csv.tmx b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-tileset-csv.tmx
new file mode 100644
index 0000000..b709f2b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-tileset-csv.tmx
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.0" orientation="orthogonal" renderorder="left-down" width="3" height="3" tilewidth="32" tileheight="32" nextobjectid="1">
+ <tileset firstgid="1" name="test-tileset" tilewidth="32" tileheight="32" spacing="2" margin="2">
+ <image source="test-tileset.png" width="104" height="104"/>
+ </tileset>
+ <layer name="Tile Layer 1" width="3" height="3">
+ <data encoding="csv">
+1,2,3,
+4,5,6,
+7,8,9
+</data>
+ </layer>
+</map>
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-tileset-gzip.tmx b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-tileset-gzip.tmx
new file mode 100644
index 0000000..c48705f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-tileset-gzip.tmx
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.0" orientation="orthogonal" renderorder="left-down" width="3" height="3" tilewidth="32" tileheight="32" nextobjectid="1">
+ <tileset firstgid="1" name="test-tileset" tilewidth="32" tileheight="32" spacing="2" margin="2">
+ <image source="test-tileset.png" width="104" height="104"/>
+ </tileset>
+ <layer name="Tile Layer 1" width="3" height="3">
+ <data encoding="base64" compression="gzip">
+ H4sIAAAAAAAACw3Dhw0AAAjDsLLh/4eJJZskZzBZbA6Xxwdm9rUOJAAAAA==
+ </data>
+ </layer>
+</map>
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-tileset-xml.tmx b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-tileset-xml.tmx
new file mode 100644
index 0000000..0f1a741
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-tileset-xml.tmx
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.0" orientation="orthogonal" renderorder="left-down" width="3" height="3" tilewidth="32" tileheight="32" nextobjectid="1">
+ <tileset firstgid="1" name="test-tileset" tilewidth="32" tileheight="32" spacing="2" margin="2">
+ <image source="test-tileset.png" width="104" height="104"/>
+ </tileset>
+ <layer name="Tile Layer 1" width="3" height="3">
+ <data>
+ <tile gid="1"/>
+ <tile gid="2"/>
+ <tile gid="3"/>
+ <tile gid="4"/>
+ <tile gid="5"/>
+ <tile gid="6"/>
+ <tile gid="7"/>
+ <tile gid="8"/>
+ <tile gid="9"/>
+ </data>
+ </layer>
+</map>
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-tileset-zlib.tmx b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-tileset-zlib.tmx
new file mode 100644
index 0000000..7a5a548
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TestData/test-tileset-zlib.tmx
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.0" orientation="orthogonal" renderorder="left-down" width="3" height="3" tilewidth="32" tileheight="32" nextobjectid="1">
+ <tileset firstgid="1" name="test-tileset" tilewidth="32" tileheight="32" spacing="2" margin="2">
+ <image source="test-tileset.png" width="104" height="104"/>
+ </tileset>
+ <layer name="Tile Layer 1" width="3" height="3">
+ <data encoding="base64" compression="zlib">
+ eJwNw4cNAAAIw7Cy4f+HiSWbJGcwWWwOl8cHArgALg==
+ </data>
+ </layer>
+</map>
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TiledMapImporterProcessorTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TiledMapImporterProcessorTests.cs
new file mode 100644
index 0000000..352457a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests.Tiled/TiledMapImporterProcessorTests.cs
@@ -0,0 +1,205 @@
+using System.IO;
+using System.Linq;
+using Microsoft.Xna.Framework.Content.Pipeline;
+using MonoGame.Extended.Content.Pipeline.Tiled;
+using MonoGame.Extended.Tiled.Serialization;
+using NSubstitute;
+using Xunit;
+
+namespace MonoGame.Extended.Content.Pipeline.Tests.Tiled
+{
+
+ public class TiledMapImporterProcessorTests
+ {
+ [Fact]
+ public void TiledMapImporter_Import_Test()
+ {
+ var filePath = PathExtensions.GetApplicationFullPath("TestData", "level01.tmx");
+
+ var logger = Substitute.For<ContentBuildLogger>();
+ var importer = new TiledMapImporter();
+ var importerContext = Substitute.For<ContentImporterContext>();
+ importerContext.Logger.Returns(logger);
+
+ var contentItem = importer.Import(filePath, importerContext);
+ var map = contentItem.Data;
+
+ Assert.Equal("1.0", map.Version);
+ Assert.Equal(TiledMapOrientationContent.Orthogonal, map.Orientation);
+ Assert.Equal(TiledMapTileDrawOrderContent.RightDown, map.RenderOrder);
+ Assert.Equal(20, map.Width);
+ Assert.Equal(10, map.Height);
+ Assert.Equal(128, map.TileWidth);
+ Assert.Equal(128, map.TileHeight);
+ Assert.Equal("#7d7d7d", map.BackgroundColor);
+ Assert.Equal("awesome", map.Properties[0].Name);
+ Assert.Equal("42", map.Properties[0].Value);
+ Assert.Single(map.Tilesets);
+ Assert.Equal(3, map.Layers.Count);
+ Assert.Equal(TiledMapOrientationContent.Orthogonal, map.Orientation);
+
+ var tileset = map.Tilesets.First();
+ Assert.Equal(1, tileset.FirstGlobalIdentifier);
+ Assert.Equal("free-tileset.png", Path.GetFileName(tileset.Image.Source));
+ Assert.Equal(652, tileset.Image.Width);
+ Assert.Equal(783, tileset.Image.Height);
+ Assert.Equal(2, tileset.Margin);
+ Assert.Equal(30, tileset.TileCount);
+ Assert.Equal("free-tileset", tileset.Name);
+ Assert.Null(tileset.Source);
+ Assert.Equal(2, tileset.Spacing);
+ //Assert.Equal(0, tileset.TerrainTypes.Count);
+ Assert.Empty(tileset.Properties);
+ Assert.Equal(128, tileset.TileHeight);
+ Assert.Equal(128, tileset.TileWidth);
+ Assert.Equal(0, tileset.TileOffset.X);
+ Assert.Equal(0, tileset.TileOffset.Y);
+
+ var tileLayer2 = (TiledMapTileLayerContent)map.Layers[0];
+ Assert.Equal("Tile Layer 2", tileLayer2.Name);
+ Assert.Equal(1, tileLayer2.Opacity);
+ Assert.Empty(tileLayer2.Properties);
+ Assert.True(tileLayer2.Visible);
+ Assert.Equal(200, tileLayer2.Data.Tiles.Count);
+ Assert.Equal(0, tileLayer2.X);
+ Assert.Equal(0, tileLayer2.Y);
+
+ var imageLayer = (TiledMapImageLayerContent)map.Layers[1];
+ Assert.Equal("Image Layer 1", imageLayer.Name);
+ Assert.Equal(1, imageLayer.Opacity);
+ Assert.Empty(imageLayer.Properties);
+ Assert.True(imageLayer.Visible);
+ Assert.Equal("hills.png", Path.GetFileName(imageLayer.Image.Source));
+ Assert.Equal(100, imageLayer.X);
+ Assert.Equal(100, imageLayer.Y);
+
+ var tileLayer1 = (TiledMapTileLayerContent)map.Layers[2];
+ Assert.Equal("Tile Layer 1", tileLayer1.Name);
+ Assert.Equal(2, tileLayer1.Properties.Count);
+
+ Assert.Equal("customlayerprop", tileLayer1.Properties[0].Name);
+ Assert.Equal("1", tileLayer1.Properties[0].Value);
+
+ Assert.Equal("customlayerprop2", tileLayer1.Properties[1].Name);
+ Assert.Equal("2", tileLayer1.Properties[1].Value);
+ }
+
+ [Fact]
+ public void TiledMapImporter_Xml_Test()
+ {
+ var filePath = PathExtensions.GetApplicationFullPath("TestData", "test-tileset-xml.tmx");
+ var map = ImportAndProcessMap(filePath);
+ var layer = map.Layers.OfType<TiledMapTileLayerContent>().First();
+ var actualData = layer.Data.Tiles.Select(i => i.GlobalIdentifier).ToArray();
+
+ Assert.Null(layer.Data.Encoding);
+ Assert.Null(layer.Data.Compression);
+ Assert.True(new uint[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }.SequenceEqual(actualData));
+ }
+
+ [Fact]
+ public void TiledMapImporter_Csv_Test()
+ {
+ var filePath = PathExtensions.GetApplicationFullPath("TestData", "test-tileset-csv.tmx");
+ var map = ImportAndProcessMap(filePath);
+ var layer = map.Layers.OfType<TiledMapTileLayerContent>().First();
+ var data = layer.Data.Tiles.Select(i => i.GlobalIdentifier).ToArray();
+
+ Assert.Equal("csv", layer.Data.Encoding);
+ Assert.Null(layer.Data.Compression);
+ //Assert.True(new uint[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }.SequenceEqual(data));
+ }
+
+ [Fact]
+ public void TiledMapImporter_Base64_Test()
+ {
+ var filePath = PathExtensions.GetApplicationFullPath("TestData", "test-tileset-base64.tmx");
+ var map = ImportAndProcessMap(filePath);
+ var layer = map.Layers.OfType<TiledMapTileLayerContent>().First();
+ var data = layer.Data.Tiles.Select(i => i.GlobalIdentifier).ToArray();
+
+ Assert.Equal("base64", layer.Data.Encoding);
+ Assert.Null(layer.Data.Compression);
+ //Assert.True(new uint[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }.SequenceEqual(data));
+ }
+
+ [Fact]
+ public void TiledMapImporter_Gzip_Test()
+ {
+ var filePath = PathExtensions.GetApplicationFullPath("TestData", "test-tileset-gzip.tmx");
+ var map = ImportAndProcessMap(filePath);
+ var layer = map.Layers.OfType<TiledMapTileLayerContent>().First();
+ var data = layer.Data.Tiles.Select(i => i.GlobalIdentifier).ToArray();
+
+ Assert.Equal("base64", layer.Data.Encoding);
+ Assert.Equal("gzip", layer.Data.Compression);
+ //Assert.True(new uint[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }.SequenceEqual(data));
+ }
+
+
+ [Fact]
+ public void TiledMapImporter_Zlib_Test()
+ {
+ var filePath = PathExtensions.GetApplicationFullPath("TestData", "test-tileset-zlib.tmx");
+ var map = ImportAndProcessMap(filePath);
+ var layer = map.Layers.OfType<TiledMapTileLayerContent>().First();
+ var data = layer.Data.Tiles.Select(i => i.GlobalIdentifier).ToArray();
+
+ Assert.Equal("base64", layer.Data.Encoding);
+ Assert.Equal("zlib", layer.Data.Compression);
+ //Assert.True(new uint[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }.SequenceEqual(data));
+ }
+
+ [Fact]
+ public void TiledMapImporter_ObjectLayer_Test()
+ {
+ var filePath = PathExtensions.GetApplicationFullPath("TestData", "test-object-layer.tmx");
+ var map = ImportAndProcessMap(filePath);
+
+ Assert.Single(map.Layers);
+ Assert.IsType<TiledMapObjectLayerContent>(map.Layers[0]);
+ var tmxObjectGroup = map.Layers[0] as TiledMapObjectLayerContent;
+ var tmxObject = tmxObjectGroup.Objects[0];
+ var tmxPolygon = tmxObjectGroup.Objects[3].Polygon;
+ var tmxPolyline = tmxObjectGroup.Objects[4].Polyline;
+
+ Assert.Equal("Object Layer 1", tmxObjectGroup.Name);
+ Assert.Equal(1, tmxObject.Identifier);
+ Assert.Equal(131.345f, tmxObject.X);
+ Assert.Equal(65.234f, tmxObject.Y);
+ Assert.Equal(311.111f, tmxObject.Width);
+ Assert.Equal(311.232f, tmxObject.Height);
+ Assert.Single(tmxObject.Properties);
+ Assert.Equal("shape", tmxObject.Properties[0].Name);
+ Assert.Equal("circle", tmxObject.Properties[0].Value);
+ Assert.NotNull(tmxObject.Ellipse);
+ Assert.False(tmxObjectGroup.Objects[1].Visible);
+ Assert.Equal((uint)0, tmxObjectGroup.Objects[1].GlobalIdentifier);
+ Assert.Equal((uint)23, tmxObjectGroup.Objects[5].GlobalIdentifier);
+ Assert.Equal("rectangle", tmxObjectGroup.Objects[2].Type);
+ Assert.Equal("sprite", tmxObjectGroup.Objects[1].Class);
+ Assert.NotNull(tmxPolygon);
+ Assert.Equal("0,0 180,90 -8,275 -45,81 38,77", tmxPolygon.Points);
+ Assert.NotNull(tmxPolyline);
+ Assert.Equal("0,0 28,299 326,413 461,308", tmxPolyline.Points);
+ }
+
+
+ private static TiledMapContent ImportAndProcessMap(string filename)
+ {
+ var logger = Substitute.For<ContentBuildLogger>();
+ var importer = new TiledMapImporter();
+ var importerContext = Substitute.For<ContentImporterContext>();
+ importerContext.Logger.Returns(logger);
+
+ var processor = new TiledMapProcessor();
+ var processorContext = Substitute.For<ContentProcessorContext>();
+ processorContext.Logger.Returns(logger);
+
+ var import = importer.Import(filename, importerContext);
+ var result = processor.Process(import, processorContext);
+
+ return result.Data;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/AstridAnimatorImporterTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/AstridAnimatorImporterTests.cs
new file mode 100644
index 0000000..c3af651
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/AstridAnimatorImporterTests.cs
@@ -0,0 +1,34 @@
+using System;
+using System.IO;
+using MonoGame.Extended.Content.Pipeline.Animations;
+using Xunit;
+
+namespace MonoGame.Extended.Content.Pipeline.Tests
+{
+
+ public class AstridAnimatorImporterTests
+ {
+ [Fact]
+ public void AstridAnimatorImporter_Import_Test()
+ {
+ var filePath = PathExtensions.GetApplicationFullPath("TestData", "astrid-animator.aa");
+ var importer = new AstridAnimatorImporter();
+ var result = importer.Import(filePath, null);
+ var data = result.Data;
+
+ Assert.Equal("astrid-animator-atlas.json", data.TextureAtlas);
+ Assert.Equal(2, data.Animations.Count);
+
+ Assert.Equal("appear", data.Animations[0].Name);
+ Assert.Equal(8, data.Animations[0].FramesPerSecond);
+ Assert.Equal(2, data.Animations[0].Frames.Count);
+ Assert.Equal("appear_01", data.Animations[0].Frames[0]);
+ Assert.Equal("appear_02", data.Animations[0].Frames[1]);
+
+ Assert.Equal("die", data.Animations[1].Name);
+ Assert.Equal(16, data.Animations[1].FramesPerSecond);
+ Assert.Single(data.Animations[1].Frames);
+ Assert.Equal("die_01", data.Animations[1].Frames[0]);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/AstridAnimatorProcessorTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/AstridAnimatorProcessorTests.cs
new file mode 100644
index 0000000..0a27412
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/AstridAnimatorProcessorTests.cs
@@ -0,0 +1,30 @@
+using System;
+using System.IO;
+using MonoGame.Extended.Content.Pipeline.Animations;
+using Xunit;
+
+namespace MonoGame.Extended.Content.Pipeline.Tests
+{
+
+ public class AstridAnimatorProcessorTests
+ {
+ [Fact]
+ public void AstridAnimatorProcessor_Process_Test()
+ {
+ var filePath = PathExtensions.GetApplicationFullPath("TestData", "astrid-animator.aa");
+
+ var importer = new AstridAnimatorImporter();
+ var importerResult = importer.Import(filePath, null);
+
+ var processor = new AstridAnimatorProcessor();
+ var result = processor.Process(importerResult, null);
+
+ Assert.Equal("astrid-animator-atlas", result.TextureAtlasAssetName);
+ Assert.Equal("TestData", Path.GetFileName(result.Directory));
+ Assert.Equal(3, result.Frames.Count);
+ Assert.Equal("appear_01", result.Frames[0]);
+ Assert.Equal("appear_02", result.Frames[1]);
+ Assert.Equal("die_01", result.Frames[2]);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/MonoGame.Extended.Content.Pipeline.Tests.csproj b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/MonoGame.Extended.Content.Pipeline.Tests.csproj
new file mode 100644
index 0000000..cee444e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/MonoGame.Extended.Content.Pipeline.Tests.csproj
@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <ItemGroup>
+ <PackageReference Include="MonoGame.Framework.Content.Pipeline" Version="3.8.1.303" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\source\MonoGame.Extended.Content.Pipeline\MonoGame.Extended.Content.Pipeline.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <None Update="TestData\astrid-animator-atlas.json" CopyToOutputDirectory="PreserveNewest" />
+ <None Update="TestData\astrid-animator.aa" CopyToOutputDirectory="PreserveNewest" />
+ <None Update="TestData\test-tileset.json" CopyToOutputDirectory="PreserveNewest" />
+ </ItemGroup>
+</Project>
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/TestData/astrid-animator-atlas.json b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/TestData/astrid-animator-atlas.json
new file mode 100644
index 0000000..7a9f9f0
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/TestData/astrid-animator-atlas.json
@@ -0,0 +1,390 @@
+{
+ "frames": [
+ {
+ "filename": "appear_01.png",
+ "frame": {"x":494,"y":111,"w":27,"h":31},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":52,"y":113,"w":27,"h":31},
+ "sourceSize": {"w":110,"h":144},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "appear_02.png",
+ "frame": {"x":465,"y":111,"w":27,"h":42},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":52,"y":102,"w":27,"h":42},
+ "sourceSize": {"w":110,"h":144},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "appear_03.png",
+ "frame": {"x":705,"y":158,"w":31,"h":52},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":41,"y":92,"w":31,"h":52},
+ "sourceSize": {"w":110,"h":144},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "appear_04.png",
+ "frame": {"x":112,"y":298,"w":74,"h":17},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":16,"y":127,"w":74,"h":17},
+ "sourceSize": {"w":110,"h":144},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "appear_05.png",
+ "frame": {"x":564,"y":621,"w":94,"h":62},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":16,"y":82,"w":94,"h":62},
+ "sourceSize": {"w":110,"h":144},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "appear_06.png",
+ "frame": {"x":507,"y":544,"w":106,"h":75},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":1,"y":69,"w":106,"h":75},
+ "sourceSize": {"w":110,"h":144},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "appear_07.png",
+ "frame": {"x":119,"y":637,"w":98,"h":93},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":0,"y":51,"w":98,"h":93},
+ "sourceSize": {"w":110,"h":144},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "appear_08.png",
+ "frame": {"x":486,"y":621,"w":76,"h":118},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":12,"y":26,"w":76,"h":118},
+ "sourceSize": {"w":110,"h":144},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "appear_09.png",
+ "frame": {"x":321,"y":601,"w":73,"h":124},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":15,"y":20,"w":73,"h":124},
+ "sourceSize": {"w":110,"h":144},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "appear_10.png",
+ "frame": {"x":406,"y":600,"w":78,"h":140},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":10,"y":4,"w":78,"h":140},
+ "sourceSize": {"w":110,"h":144},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "appear_11.png",
+ "frame": {"x":615,"y":469,"w":82,"h":144},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":11,"y":0,"w":82,"h":144},
+ "sourceSize": {"w":110,"h":144},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "die_01.png",
+ "frame": {"x":523,"y":15,"w":112,"h":144},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":38,"y":0,"w":112,"h":144},
+ "sourceSize": {"w":222,"h":146},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "die_02.png",
+ "frame": {"x":113,"y":2,"w":134,"h":129},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":32,"y":15,"w":134,"h":129},
+ "sourceSize": {"w":222,"h":146},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "die_03.png",
+ "frame": {"x":249,"y":2,"w":139,"h":106},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":36,"y":38,"w":139,"h":106},
+ "sourceSize": {"w":222,"h":146},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "die_04.png",
+ "frame": {"x":465,"y":161,"w":133,"h":73},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":44,"y":71,"w":133,"h":73},
+ "sourceSize": {"w":222,"h":146},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "die_05.png",
+ "frame": {"x":113,"y":133,"w":147,"h":69},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":42,"y":76,"w":147,"h":69},
+ "sourceSize": {"w":222,"h":146},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "die_06.png",
+ "frame": {"x":564,"y":685,"w":154,"h":51},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":41,"y":94,"w":154,"h":51},
+ "sourceSize": {"w":222,"h":146},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "die_07.png",
+ "frame": {"x":242,"y":266,"w":188,"h":24},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":28,"y":121,"w":188,"h":24},
+ "sourceSize": {"w":222,"h":146},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "die_08.png",
+ "frame": {"x":390,"y":2,"w":222,"h":11},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":0,"y":134,"w":222,"h":11},
+ "sourceSize": {"w":222,"h":146},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "go_01.png",
+ "frame": {"x":637,"y":2,"w":100,"h":154},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":0,"y":1,"w":100,"h":154},
+ "sourceSize": {"w":100,"h":156},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "go_02.png",
+ "frame": {"x":262,"y":110,"w":100,"h":154},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":0,"y":1,"w":100,"h":154},
+ "sourceSize": {"w":100,"h":156},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "go_03.png",
+ "frame": {"x":364,"y":111,"w":99,"h":153},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":1,"y":2,"w":99,"h":153},
+ "sourceSize": {"w":100,"h":156},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "go_04.png",
+ "frame": {"x":203,"y":298,"w":99,"h":153},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":1,"y":2,"w":99,"h":153},
+ "sourceSize": {"w":100,"h":156},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "go_05.png",
+ "frame": {"x":305,"y":447,"w":99,"h":152},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":1,"y":3,"w":99,"h":152},
+ "sourceSize": {"w":100,"h":156},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "go_06.png",
+ "frame": {"x":405,"y":292,"w":98,"h":152},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":2,"y":3,"w":98,"h":152},
+ "sourceSize": {"w":100,"h":156},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "go_07.png",
+ "frame": {"x":304,"y":292,"w":99,"h":153},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":1,"y":2,"w":99,"h":153},
+ "sourceSize": {"w":100,"h":156},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "go_08.png",
+ "frame": {"x":103,"y":480,"w":99,"h":155},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":1,"y":1,"w":99,"h":155},
+ "sourceSize": {"w":100,"h":156},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "go_09.png",
+ "frame": {"x":2,"y":322,"w":99,"h":156},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":1,"y":0,"w":99,"h":156},
+ "sourceSize": {"w":100,"h":156},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "go_10.png",
+ "frame": {"x":2,"y":480,"w":99,"h":156},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":1,"y":0,"w":99,"h":156},
+ "sourceSize": {"w":100,"h":156},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "hit_01.png",
+ "frame": {"x":2,"y":2,"w":109,"h":159},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":77,"y":0,"w":109,"h":159},
+ "sourceSize": {"w":186,"h":162},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "hit_02.png",
+ "frame": {"x":103,"y":322,"w":98,"h":156},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":79,"y":3,"w":98,"h":156},
+ "sourceSize": {"w":186,"h":162},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "hit_03.png",
+ "frame": {"x":2,"y":163,"w":108,"h":157},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":25,"y":3,"w":108,"h":157},
+ "sourceSize": {"w":186,"h":162},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "hit_04.png",
+ "frame": {"x":390,"y":15,"w":131,"h":94},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":0,"y":66,"w":131,"h":94},
+ "sourceSize": {"w":186,"h":162},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "hit_05.png",
+ "frame": {"x":112,"y":204,"w":128,"h":92},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":3,"y":70,"w":128,"h":92},
+ "sourceSize": {"w":186,"h":162},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "hit_06.png",
+ "frame": {"x":2,"y":638,"w":115,"h":100},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":16,"y":60,"w":115,"h":100},
+ "sourceSize": {"w":186,"h":162},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "hit_07.png",
+ "frame": {"x":219,"y":608,"w":100,"h":132},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":32,"y":28,"w":100,"h":132},
+ "sourceSize": {"w":186,"h":162},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "idle_01.png",
+ "frame": {"x":204,"y":453,"w":99,"h":153},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":0,"y":0,"w":99,"h":153},
+ "sourceSize": {"w":100,"h":154},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "idle_02.png",
+ "frame": {"x":406,"y":446,"w":99,"h":152},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":0,"y":1,"w":99,"h":152},
+ "sourceSize": {"w":100,"h":154},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "idle_03.png",
+ "frame": {"x":505,"y":236,"w":98,"h":152},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":1,"y":1,"w":98,"h":152},
+ "sourceSize": {"w":100,"h":154},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "idle_04.png",
+ "frame": {"x":507,"y":390,"w":98,"h":152},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":1,"y":1,"w":98,"h":152},
+ "sourceSize": {"w":100,"h":154},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "idle_05.png",
+ "frame": {"x":605,"y":161,"w":98,"h":152},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":1,"y":1,"w":98,"h":152},
+ "sourceSize": {"w":100,"h":154},
+ "pivot": {"x":0.5,"y":1}
+ },
+ {
+ "filename": "idle_06.png",
+ "frame": {"x":607,"y":315,"w":99,"h":152},
+ "rotated": false,
+ "trimmed": true,
+ "spriteSourceSize": {"x":0,"y":1,"w":99,"h":152},
+ "sourceSize": {"w":100,"h":154},
+ "pivot": {"x":0.5,"y":1}
+ }],
+ "meta": {
+ "app": "http://www.codeandweb.com/texturepacker",
+ "version": "1.0",
+ "image": "zombie.png",
+ "format": "RGBA8888",
+ "size": {"w":742,"h":742},
+ "scale": "0.5",
+ "smartupdate": "$TexturePacker:SmartUpdate:28fca4a18eeef90b2646ccc59eb1f593:ccb2bd5648fe15c740c647d39945c765:3e6ed6fe54c801c395eefb25aa5e45e8$"
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/TestData/astrid-animator.aa b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/TestData/astrid-animator.aa
new file mode 100644
index 0000000..ba8d43f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/TestData/astrid-animator.aa
@@ -0,0 +1,15 @@
+{
+ "TextureAtlas": "astrid-animator-atlas.json",
+ "Animations": [
+ {
+ "Name": "appear",
+ "FramesPerSecond": 8,
+ "Frames": [ "appear_01", "appear_02" ]
+ },
+ {
+ "Name": "die",
+ "FramesPerSecond": 16,
+ "Frames": [ "die_01" ]
+ }
+ ]
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/TestData/test-tileset.json b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/TestData/test-tileset.json
new file mode 100644
index 0000000..57e46f5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/TestData/test-tileset.json
@@ -0,0 +1,93 @@
+{"frames": [
+
+{
+ "filename": "1.png",
+ "frame": {"x":2,"y":2,"w":32,"h":32},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":32,"h":32},
+ "sourceSize": {"w":32,"h":32},
+ "pivot": {"x":0.5,"y":0.5}
+},
+{
+ "filename": "2.png",
+ "frame": {"x":36,"y":2,"w":32,"h":32},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":32,"h":32},
+ "sourceSize": {"w":32,"h":32},
+ "pivot": {"x":0.5,"y":0.5}
+},
+{
+ "filename": "3.png",
+ "frame": {"x":70,"y":2,"w":32,"h":32},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":32,"h":32},
+ "sourceSize": {"w":32,"h":32},
+ "pivot": {"x":0.5,"y":0.5}
+},
+{
+ "filename": "4.png",
+ "frame": {"x":2,"y":36,"w":32,"h":32},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":32,"h":32},
+ "sourceSize": {"w":32,"h":32},
+ "pivot": {"x":0.5,"y":0.5}
+},
+{
+ "filename": "5.png",
+ "frame": {"x":36,"y":36,"w":32,"h":32},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":32,"h":32},
+ "sourceSize": {"w":32,"h":32},
+ "pivot": {"x":0.5,"y":0.5}
+},
+{
+ "filename": "6.png",
+ "frame": {"x":70,"y":36,"w":32,"h":32},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":32,"h":32},
+ "sourceSize": {"w":32,"h":32},
+ "pivot": {"x":0.5,"y":0.5}
+},
+{
+ "filename": "7.png",
+ "frame": {"x":2,"y":70,"w":32,"h":32},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":32,"h":32},
+ "sourceSize": {"w":32,"h":32},
+ "pivot": {"x":0.5,"y":0.5}
+},
+{
+ "filename": "8.png",
+ "frame": {"x":36,"y":70,"w":32,"h":32},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":32,"h":32},
+ "sourceSize": {"w":32,"h":32},
+ "pivot": {"x":0.5,"y":0.5}
+},
+{
+ "filename": "9.png",
+ "frame": {"x":70,"y":70,"w":32,"h":32},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":32,"h":32},
+ "sourceSize": {"w":32,"h":32},
+ "pivot": {"x":0.5,"y":0.5}
+}],
+"meta": {
+ "app": "http://www.codeandweb.com/texturepacker",
+ "version": "1.0",
+ "image": "test-tileset.png",
+ "format": "RGBA8888",
+ "size": {"w":104,"h":104},
+ "scale": "1",
+ "smartupdate": "$TexturePacker:SmartUpdate:f5f4c00eb32fae603057f0d9dc5c7b73:ca39697f48630ecdea6d81a8fdc48cf6:c79a4cc8e4ba9657462e67dafcaf93d2$"
+}
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/TexturePackerJsonImporterProcessorTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/TexturePackerJsonImporterProcessorTests.cs
new file mode 100644
index 0000000..4172ee2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Content.Pipeline.Tests/TexturePackerJsonImporterProcessorTests.cs
@@ -0,0 +1,33 @@
+using Microsoft.Xna.Framework.Content.Pipeline;
+using MonoGame.Extended.Content.Pipeline.TextureAtlases;
+using NSubstitute;
+using Xunit;
+
+namespace MonoGame.Extended.Content.Pipeline.Tests
+{
+
+ public class TexturePackerJsonImporterProcessorTests
+ {
+ [Fact]
+ public void TexturePackerJsonImporter_Import_Test()
+ {
+ var filePath = PathExtensions.GetApplicationFullPath(@"TestData/test-tileset.json");
+ var importer = new TexturePackerJsonImporter();
+ var data = importer.Import(filePath, Substitute.For<ContentImporterContext>());
+
+ Assert.NotNull(data);
+ }
+
+ [Fact]
+ public void TexturePackerJsonImporter_Processor_Test()
+ {
+ var filePath = PathExtensions.GetApplicationFullPath(@"TestData/test-tileset.json");
+ var importer = new TexturePackerJsonImporter();
+ var input = importer.Import(filePath, Substitute.For<ContentImporterContext>());
+ var processor = new TexturePackerProcessor();
+ var output = processor.Process(input, Substitute.For<ContentProcessorContext>());
+
+ Assert.NotNull(output);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/AspectBuilderTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/AspectBuilderTests.cs
new file mode 100644
index 0000000..1b3e971
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/AspectBuilderTests.cs
@@ -0,0 +1,70 @@
+using System;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.Sprites;
+using Xunit;
+
+namespace MonoGame.Extended.Entities.Tests
+{
+ public class AspectBuilderTests
+ {
+ [Fact]
+ public void MatchAllTypes()
+ {
+ var builder = new AspectBuilder()
+ .All(typeof(Transform2), typeof(Sprite));
+
+ Assert.Equal(2, builder.AllTypes.Count);
+ Assert.Contains(typeof(Transform2), builder.AllTypes);
+ Assert.Contains(typeof(Sprite), builder.AllTypes);
+ }
+
+ [Fact]
+ public void MatchAllTypesIsEmpty()
+ {
+ var builder = new AspectBuilder()
+ .All();
+
+ Assert.Empty(builder.AllTypes);
+ Assert.Empty(builder.OneTypes);
+ Assert.Empty(builder.ExclusionTypes);
+ }
+
+ [Fact]
+ public void MatchOneOfType()
+ {
+ var builder = new AspectBuilder()
+ .One(typeof(Transform2), typeof(Sprite));
+
+ Assert.Equal(2, builder.OneTypes.Count);
+ Assert.Contains(typeof(Transform2), builder.OneTypes);
+ Assert.Contains(typeof(Sprite), builder.OneTypes);
+ }
+
+ [Fact]
+ public void ExcludeTypes()
+ {
+ var builder = new AspectBuilder()
+ .Exclude(typeof(Transform2), typeof(Sprite));
+
+ Assert.Equal(2, builder.ExclusionTypes.Count);
+ Assert.Contains(typeof(Transform2), builder.ExclusionTypes);
+ Assert.Contains(typeof(Sprite), builder.ExclusionTypes);
+ }
+
+ [Fact]
+ public void BuildAspect()
+ {
+ var componentManager = new ComponentManager();
+ var builder = new AspectBuilder()
+ .All(typeof(Transform2), typeof(Sprite))
+ .One(typeof(string))
+ .Exclude(typeof(Texture2D));
+
+ var aspect = builder.Build(componentManager);
+
+ Assert.True(aspect.AllSet.Data != 0);
+ Assert.True(aspect.OneSet.Data != 0);
+ Assert.True(aspect.ExclusionSet.Data != 0);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/AspectTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/AspectTests.cs
new file mode 100644
index 0000000..b06780b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/AspectTests.cs
@@ -0,0 +1,88 @@
+using System.Collections.Specialized;
+using MonoGame.Extended.Sprites;
+using Xunit;
+
+namespace MonoGame.Extended.Entities.Tests
+{
+ public class DummyComponent
+ {
+ }
+
+ public class AspectTests
+ {
+ private readonly ComponentManager _componentManager;
+ private readonly BitVector32 _entityA;
+ private readonly BitVector32 _entityB;
+
+ public AspectTests()
+ {
+ _componentManager = new ComponentManager();
+ _entityA = new BitVector32
+ {
+ [1 << _componentManager.GetComponentTypeId(typeof(Transform2))] = true,
+ [1 << _componentManager.GetComponentTypeId(typeof(Sprite))] = true,
+ [1 << _componentManager.GetComponentTypeId(typeof(DummyComponent))] = true
+ };
+ _entityB = new BitVector32
+ {
+ [1 << _componentManager.GetComponentTypeId(typeof(Transform2))] = true,
+ [1 << _componentManager.GetComponentTypeId(typeof(Sprite))] = true,
+ };
+ }
+
+ [Fact]
+ public void EmptyAspectMatchesAllComponents()
+ {
+ var componentManager = new ComponentManager();
+ var emptyAspect = Aspect.All()
+ .Build(componentManager);
+
+ Assert.True(emptyAspect.IsInterested(_entityA));
+ Assert.True(emptyAspect.IsInterested(_entityB));
+ }
+
+ [Fact]
+ public void IsInterestedInAllComponents()
+ {
+ var allAspect = Aspect
+ .All(typeof(Sprite), typeof(Transform2), typeof(DummyComponent))
+ .Build(_componentManager);
+
+ Assert.True(allAspect.IsInterested(_entityA));
+ Assert.False(allAspect.IsInterested(_entityB));
+ }
+
+ [Fact]
+ public void IsInterestedInEitherOneOfTheComponents()
+ {
+ var eitherOneAspect = Aspect
+ .One(typeof(Transform2), typeof(DummyComponent))
+ .Build(_componentManager);
+
+ Assert.True(eitherOneAspect.IsInterested(_entityA));
+ Assert.True(eitherOneAspect.IsInterested(_entityB));
+ }
+
+ [Fact]
+ public void IsInterestedInJustOneComponent()
+ {
+ var oneAspect = Aspect
+ .One(typeof(DummyComponent))
+ .Build(_componentManager);
+
+ Assert.True(oneAspect.IsInterested(_entityA));
+ Assert.False(oneAspect.IsInterested(_entityB));
+ }
+
+ [Fact]
+ public void IsInterestedInExcludingOneComponent()
+ {
+ var oneAspect = Aspect
+ .Exclude(typeof(DummyComponent))
+ .Build(_componentManager);
+
+ Assert.False(oneAspect.IsInterested(_entityA));
+ Assert.True(oneAspect.IsInterested(_entityB));
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/BitArrayExtensionsTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/BitArrayExtensionsTests.cs
new file mode 100644
index 0000000..44ec4af
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/BitArrayExtensionsTests.cs
@@ -0,0 +1,20 @@
+using System.Collections;
+using Xunit;
+
+namespace MonoGame.Extended.Entities.Tests
+{
+ public class BitArrayExtensionsTests
+ {
+ [Fact]
+ public void BitArrayIsEmpty()
+ {
+ Assert.True(new BitArray(1).IsEmpty());
+ Assert.False(new BitArray(new[] { true }).IsEmpty());
+ Assert.True(new BitArray(new[] { false }).IsEmpty());
+
+ var bitArray = new BitArray(new[] { true });
+ bitArray.Set(0, false);
+ Assert.True(bitArray.IsEmpty());
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/ComponentManagerTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/ComponentManagerTests.cs
new file mode 100644
index 0000000..8d988e4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/ComponentManagerTests.cs
@@ -0,0 +1,47 @@
+using MonoGame.Extended.Sprites;
+using Xunit;
+
+namespace MonoGame.Extended.Entities.Tests
+{
+ public class ComponentManagerTests
+ {
+ [Fact]
+ public void GetMapperForType()
+ {
+ var componentManager = new ComponentManager();
+ var transformMapper = componentManager.GetMapper<Transform2>();
+ var spriteMapper = componentManager.GetMapper<Sprite>();
+
+ Assert.IsType<ComponentMapper<Transform2>>(transformMapper);
+ Assert.IsType<ComponentMapper<Sprite>>(spriteMapper);
+ Assert.Equal(0, transformMapper.Id);
+ Assert.Equal(1, spriteMapper.Id);
+ Assert.Same(spriteMapper, componentManager.GetMapper<Sprite>());
+ }
+
+ [Fact]
+ public void GetComponentTypeId()
+ {
+ var componentManager = new ComponentManager();
+
+ Assert.Equal(0, componentManager.GetComponentTypeId(typeof(Transform2)));
+ Assert.Equal(1, componentManager.GetComponentTypeId(typeof(Sprite)));
+ Assert.Equal(0, componentManager.GetComponentTypeId(typeof(Transform2)));
+ }
+
+ //[Fact]
+ //public void GetCompositionIdentity()
+ //{
+ // var compositionBits = new BitArray(3)
+ // {
+ // [0] = true,
+ // [1] = false,
+ // [2] = true
+ // };
+ // var componentManager = new ComponentManager();
+ // var identity = componentManager.GetCompositionIdentity(compositionBits);
+
+ // Assert.Equal(0b101, identity);
+ //}
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/ComponentMapperTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/ComponentMapperTests.cs
new file mode 100644
index 0000000..54fab04
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/ComponentMapperTests.cs
@@ -0,0 +1,95 @@
+using Xunit;
+
+namespace MonoGame.Extended.Entities.Tests
+{
+ public class ComponentMapperTests
+ {
+ [Fact]
+ public void CreateComponentMapper()
+ {
+ var mapper = new ComponentMapper<object>(0, _ => {});
+
+ Assert.Equal(typeof(object), mapper.ComponentType);
+ Assert.Empty(mapper.Components);
+ }
+
+ [Fact]
+ public void OnPut()
+ {
+ const int entityId = 3;
+
+ var mapper = new ComponentMapper<Transform2>(1, _ => { });
+ var component = new Transform2();
+
+ mapper.OnPut += (entId) =>
+ {
+ Assert.Equal(entityId, entId);
+ Assert.Same(component, mapper.Get(entityId));
+ };
+
+ mapper.Put(entityId, component);
+ }
+
+ [Fact]
+ public void PutAndGetComponent()
+ {
+ const int entityId = 3;
+
+ var mapper = new ComponentMapper<Transform2>(1, _ => { });
+ var component = new Transform2();
+
+ mapper.Put(entityId, component);
+
+ Assert.Equal(typeof(Transform2), mapper.ComponentType);
+ Assert.True(mapper.Components.Count >= 1);
+ Assert.Same(component, mapper.Get(entityId));
+ }
+
+ [Fact]
+ public void OnDelete()
+ {
+ const int entityId = 1;
+
+ var mapper = new ComponentMapper<Transform2>(2, _ => { });
+ var component = new Transform2();
+
+ mapper.OnDelete += (entId) =>
+ {
+ Assert.Equal(entityId, entId);
+ Assert.False(mapper.Has(entityId));
+ };
+
+ mapper.Put(entityId, component);
+ mapper.Delete(entityId);
+ }
+
+ [Fact]
+ public void DeleteComponent()
+ {
+ const int entityId = 1;
+
+ var mapper = new ComponentMapper<Transform2>(2, _ => { });
+ var component = new Transform2();
+
+ mapper.Put(entityId, component);
+ mapper.Delete(entityId);
+
+ Assert.False(mapper.Has(entityId));
+ }
+
+ [Fact]
+ public void HasComponent()
+ {
+ const int entityId = 0;
+
+ var mapper = new ComponentMapper<Transform2>(3, _ => { });
+ var component = new Transform2();
+
+ Assert.False(mapper.Has(entityId));
+
+ mapper.Put(entityId, component);
+
+ Assert.True(mapper.Has(entityId));
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/ComponentTypeTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/ComponentTypeTests.cs
new file mode 100644
index 0000000..8953804
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/ComponentTypeTests.cs
@@ -0,0 +1,20 @@
+using MonoGame.Extended.Sprites;
+using Xunit;
+
+namespace MonoGame.Extended.Entities.Tests
+{
+ //public class ComponentTypeTests
+ //{
+ // [Fact]
+ // public void CreateComponentType()
+ // {
+ // var type = typeof(Sprite);
+ // var componentType = new ComponentType(type, 3);
+
+ // Assert.Same(type, componentType.Type);
+ // Assert.Equal(3, componentType.Id);
+ // Assert.Equal(new ComponentType(typeof(Sprite), 3), componentType);
+ // Assert.Equal(componentType.Id, componentType.GetHashCode());
+ // }
+ //}
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/MonoGame.Extended.Entities.Tests.csproj b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/MonoGame.Extended.Entities.Tests.csproj
new file mode 100644
index 0000000..9eac8c4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/MonoGame.Extended.Entities.Tests.csproj
@@ -0,0 +1,7 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\source\MonoGame.Extended.Entities\MonoGame.Extended.Entities.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/WorldManagerTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/WorldManagerTests.cs
new file mode 100644
index 0000000..7620f1e
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Entities.Tests/WorldManagerTests.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Entities.Systems;
+using MonoGame.Extended.Sprites;
+using Xunit;
+
+namespace MonoGame.Extended.Entities.Tests;
+
+public class WorldManagerTests
+{
+ private GameTime _gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromMilliseconds(16));
+
+ [Fact]
+ public void CrudEntity()
+ {
+ var dummySystem = new DummySystem();
+ var worldBuilder = new WorldBuilder();
+ worldBuilder.AddSystem(dummySystem);
+ var world = worldBuilder.Build();
+
+ world.Initialize();
+
+ var entity = world.CreateEntity();
+ entity.Attach(new Transform2());
+ world.Update(_gameTime);
+ world.Draw(_gameTime);
+ var otherEntity = world.GetEntity(entity.Id);
+ Assert.Equal(entity, otherEntity);
+ Assert.True(otherEntity.Has<Transform2>());
+ Assert.Contains(entity.Id, dummySystem.AddedEntitiesId);
+
+ entity.Destroy();
+ world.Update(_gameTime);
+ world.Draw(_gameTime);
+ otherEntity = world.GetEntity(entity.Id);
+ Assert.Null(otherEntity);
+ Assert.Contains(entity.Id, dummySystem.RemovedEntitiesId);
+ }
+
+ private class DummyComponent { }
+
+ private class DummySystem : EntitySystem, IUpdateSystem, IDrawSystem
+ {
+ public List<int> AddedEntitiesId { get; } = new ();
+ public List<int> RemovedEntitiesId { get; } = new ();
+
+ public DummySystem() : base(Aspect.All(typeof(DummyComponent))) { }
+
+ public override void Initialize(IComponentMapperService mapperService)
+ {
+ // Do NOT initialize mapper in order to test: https://github.com/craftworkgames/MonoGame.Extended/issues/707
+ }
+
+ public void Draw(GameTime gameTime) { }
+
+ public void Update(GameTime gameTime) { }
+
+ protected override void OnEntityAdded(int entityId)
+ {
+ base.OnEntityAdded(entityId);
+ AddedEntitiesId.Add(entityId);
+ }
+
+ protected override void OnEntityRemoved(int entityId)
+ {
+ base.OnEntityRemoved(entityId);
+ RemovedEntitiesId.Add(entityId);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Gui.Tests/Controls/GuiButtonTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Gui.Tests/Controls/GuiButtonTests.cs
new file mode 100644
index 0000000..7defe1c
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Gui.Tests/Controls/GuiButtonTests.cs
@@ -0,0 +1,122 @@
+//using System.Collections.Generic;
+//using Microsoft.Xna.Framework.Graphics;
+//using MonoGame.Extended.BitmapFonts;
+//using MonoGame.Extended.Gui.Controls;
+//using MonoGame.Extended.Tests;
+//using MonoGame.Extended.TextureAtlases;
+//using NSubstitute;
+//using Xunit;
+
+//namespace MonoGame.Extended.Gui.Tests.Controls
+//{
+// public class GuiButtonTests
+// {
+// [Fact]
+// public void DesiredSizeShouldBeEmptyByDefault()
+// {
+// var availableSize = new Size2(800, 480);
+// var context = Substitute.For<IGuiContext>();
+// var button = new Button();
+// var desiredSize = button.GetDesiredSize(context, availableSize);
+
+// Assert.That(desiredSize, Is.EqualTo(Size2.Empty));
+// }
+
+// [Fact]
+// public void DesiredSizeShouldBeTheSizeOfTheBackgroundRegion()
+// {
+// var availableSize = new Size2(800, 480);
+// var context = Substitute.For<IGuiContext>();
+// var backgroundRegion = MockTextureRegion();
+// var button = new GuiButton { BackgroundRegion = backgroundRegion };
+// var desiredSize = button.GetDesiredSize(context, availableSize);
+
+// Assert.That(desiredSize, Is.EqualTo(backgroundRegion.Size));
+// }
+
+// [Fact]
+// public void DesiredSizeShouldBeTheSizeOfTheMarginsInANinePatchRegion()
+// {
+// var availableSize = new Size2(800, 480);
+// var context = Substitute.For<IGuiContext>();
+// var texture = new Texture2D(new TestGraphicsDevice(), 512, 512);
+// var backgroundRegion = new NinePatchRegion2D(new TextureRegion2D(texture), new Thickness(10, 20));
+// var button = new GuiButton() { BackgroundRegion = backgroundRegion };
+// var desiredSize = button.GetDesiredSize(context, availableSize);
+
+// Assert.That(desiredSize, Is.EqualTo(new Size2(20, 40)));
+// }
+
+// [Fact]
+// public void DesiredSizeShouldAtLeastBeTheSizeOfTheText()
+// {
+// const string text = "abcdefg";
+
+// var availableSize = new Size2(800, 480);
+// var context = Substitute.For<IGuiContext>();
+// var font = CreateMockFont(text, lineHeight: 32);
+// var expectedSize = font.MeasureString(text);
+// var button = new GuiButton {Text = text, Font = font};
+
+// var desiredSize = button.GetDesiredSize(context, availableSize);
+
+// Assert.That(desiredSize, Is.EqualTo(expectedSize));
+// }
+
+// [Fact]
+// public void DesiredSizeShouldAtLeastBeTheSizeOfTheIcon()
+// {
+// var texture = new Texture2D(new TestGraphicsDevice(), 35, 38);
+// var icon = new TextureRegion2D(texture);
+
+// var availableSize = new Size2(800, 480);
+// var context = Substitute.For<IGuiContext>();
+// var button = new GuiButton { IconRegion = icon };
+// var desiredSize = button.GetDesiredSize(context, availableSize);
+
+// Assert.That(desiredSize, Is.EqualTo(icon.Size));
+// }
+
+// [Fact]
+// public void DesiredSizeShouldBeTheSizeOfTheBiggestTextOrIcon()
+// {
+// const string text = "abcdefg";
+
+// var texture = new Texture2D(new TestGraphicsDevice(), 35, 38);
+// var icon = new TextureRegion2D(texture);
+// var iconExpectedSize = icon.Size;
+
+// var availableSize = new Size2(800, 480);
+// var context = Substitute.For<IGuiContext>();
+
+// var font = CreateMockFont(text, 32);
+// var fontExpectedSize = font.MeasureString(text);
+
+// var button = new GuiButton { Text = text, Font = font, IconRegion = icon };
+// var desiredSize = button.GetDesiredSize(context, availableSize);
+
+// Assert.That(desiredSize.Width, Is.EqualTo(fontExpectedSize.Width));
+// Assert.That(desiredSize.Height, Is.EqualTo(iconExpectedSize.Height));
+// }
+
+// private static BitmapFont CreateMockFont(string text, int lineHeight)
+// {
+// var regions = new List<BitmapFontRegion>();
+// var xOffset = 0;
+
+// foreach (var character in text)
+// {
+// regions.Add(new BitmapFontRegion(MockTextureRegion(10, 10), character, xOffset, yOffset: 0, xAdvance: 0));
+// xOffset += 10;
+// }
+
+// return new BitmapFont("font", regions, lineHeight);
+// }
+
+// private static TextureRegion2D MockTextureRegion(int width = 100, int height = 200)
+// {
+// var texture = new Texture2D(new TestGraphicsDevice(), width, height);
+// return new TextureRegion2D(texture, 0, 0, width, height);
+// }
+// }
+//} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Gui.Tests/Controls/GuiControlCollectionTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Gui.Tests/Controls/GuiControlCollectionTests.cs
new file mode 100644
index 0000000..a2145b8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Gui.Tests/Controls/GuiControlCollectionTests.cs
@@ -0,0 +1,58 @@
+//using MonoGame.Extended.Gui.Controls;
+//using NSubstitute;
+//using Xunit;
+
+//namespace MonoGame.Extended.Gui.Tests.Controls
+//{
+
+// public class GuiControlCollectionTests
+// {
+// [Fact]
+// public void GuiControlCollection_Add_SetsTheParent_Test()
+// {
+// var parent = Substitute.For<GuiControl>();
+// var child = Substitute.For<GuiControl>();
+
+// var controls = new GuiControlCollection(parent) { child };
+// Assert.IsTrue(controls.Contains(child));
+// Assert.AreSame(parent, child.Parent);
+// }
+
+// [Fact]
+// public void GuiControlCollection_Remove_SetsTheParentToNull_Test()
+// {
+// var parent = Substitute.For<GuiControl>();
+// var child = Substitute.For<GuiControl>();
+
+// new GuiControlCollection(parent) { child }.Remove(child);
+
+// Assert.IsNull(child.Parent);
+// }
+
+// [Fact]
+// public void GuiControlCollection_Insert_SetsTheParent_Test()
+// {
+// var parent = Substitute.For<GuiControl>();
+// var child = Substitute.For<GuiControl>();
+
+// var controls = new GuiControlCollection(parent);
+
+// controls.Insert(0, child);
+// Assert.IsTrue(controls.Contains(child));
+// Assert.AreSame(parent, child.Parent);
+// }
+
+// [Fact]
+// public void GuiControlCollection_Clear_SetsAllTheParentsToNull_Test()
+// {
+// var parent = Substitute.For<GuiControl>();
+// var child0 = Substitute.For<GuiControl>();
+// var child1 = Substitute.For<GuiControl>();
+
+// new GuiControlCollection(parent) { child0, child1 }.Clear();
+
+// Assert.IsNull(child0.Parent);
+// Assert.IsNull(child1.Parent);
+// }
+// }
+//}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Gui.Tests/GuiRendererTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Gui.Tests/GuiRendererTests.cs
new file mode 100644
index 0000000..44b3bc3
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Gui.Tests/GuiRendererTests.cs
@@ -0,0 +1,34 @@
+using MonoGame.Extended.Gui.Controls;
+using Xunit;
+
+namespace MonoGame.Extended.Gui.Tests
+{
+ //
+ //public class GuiRendererTests
+ //{
+ // [Fact]
+ // public void GuiRenderer_TargetScreen_Test()
+ // {
+ // var screen = new GuiScreen();
+ // var renderer = new GuiTestRenderer(screen);
+
+ // Assert.That(renderer.TargetScreen, Is.SameAs(screen));
+ // }
+ //}
+
+ //public class GuiTestRenderer : GuiRenderer<TestDrawer>
+ //{
+ // public GuiTestRenderer(GuiScreen targetScreen)
+ // : base(targetScreen)
+ // {
+ // }
+
+ // protected override void DrawControl(TestDrawer drawer, GuiControl control)
+ // {
+ // }
+ //}
+
+ //public class TestDrawer
+ //{
+ //}
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Gui.Tests/MonoGame.Extended.Gui.Tests.csproj b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Gui.Tests/MonoGame.Extended.Gui.Tests.csproj
new file mode 100644
index 0000000..6e96c86
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Gui.Tests/MonoGame.Extended.Gui.Tests.csproj
@@ -0,0 +1,7 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\source\MonoGame.Extended.Gui\MonoGame.Extended.Gui.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/AngleTest.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/AngleTest.cs
new file mode 100644
index 0000000..2f2762b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/AngleTest.cs
@@ -0,0 +1,92 @@
+using Microsoft.Xna.Framework;
+using Xunit;
+
+namespace MonoGame.Extended.Tests
+{
+ public class AngleTest
+ {
+ private const float _delta = 0.00001f;
+ private readonly WithinDeltaEqualityComparer _withinDeltaEqualityComparer = new(_delta);
+
+ [Fact]
+ public void ConstructorTest()
+ {
+ const float value = 0.5f;
+
+ // ReSharper disable once RedundantArgumentDefaultValue
+ var radians = new Angle(value, AngleType.Radian);
+ var degrees = new Angle(value, AngleType.Degree);
+ var gradians = new Angle(value, AngleType.Gradian);
+ var revolutions = new Angle(value, AngleType.Revolution);
+
+ Assert.Equal(0.5f, radians.Radians, _withinDeltaEqualityComparer);
+ Assert.Equal(0.5f, degrees.Degrees, _withinDeltaEqualityComparer);
+ Assert.Equal(0.5f, gradians.Gradians, _withinDeltaEqualityComparer);
+ Assert.Equal(0.5f, revolutions.Revolutions, _withinDeltaEqualityComparer);
+ }
+
+ [Fact]
+ public void ConversionTest()
+ {
+ //from radians
+ var radians = new Angle(MathHelper.Pi);
+ Assert.Equal(180f, radians.Degrees, _withinDeltaEqualityComparer);
+ Assert.Equal(200f, radians.Gradians, _withinDeltaEqualityComparer);
+ Assert.Equal(0.5f, radians.Revolutions, _withinDeltaEqualityComparer);
+
+ //to radians
+ var degrees = new Angle(180f, AngleType.Degree);
+ var gradians = new Angle(200f, AngleType.Gradian);
+ var revolutions = new Angle(0.5f, AngleType.Revolution);
+
+ Assert.Equal(MathHelper.Pi, degrees.Radians, _withinDeltaEqualityComparer);
+ Assert.Equal(MathHelper.Pi, gradians.Radians, _withinDeltaEqualityComparer);
+ Assert.Equal(MathHelper.Pi, revolutions.Radians, _withinDeltaEqualityComparer);
+ }
+
+ [Fact]
+ public void WrapTest()
+ {
+ for (var f = -10f; f < 10f; f += 0.1f)
+ {
+ var wrappositive = new Angle(f);
+ wrappositive.WrapPositive();
+
+ var wrap = new Angle(f);
+ wrap.Wrap();
+
+ Assert.True(wrappositive.Radians >= 0);
+ Assert.True(wrappositive.Radians < 2d * MathHelper.Pi);
+
+ Assert.True(wrap.Radians >= -MathHelper.Pi);
+ Assert.True(wrap.Radians < MathHelper.Pi);
+ }
+ }
+
+ [Fact]
+ public void VectorTest()
+ {
+ var angle = Angle.FromVector(Vector2.One);
+ Assert.Equal(-MathHelper.Pi / 4f, angle.Radians, _withinDeltaEqualityComparer);
+ Assert.Equal(10f, angle.ToVector(10f).Length());
+
+ angle = Angle.FromVector(Vector2.UnitX);
+ Assert.Equal(0, angle.Radians, _withinDeltaEqualityComparer);
+ Assert.True(Vector2.UnitX.EqualsWithTolerence(angle.ToUnitVector()));
+
+ angle = Angle.FromVector(-Vector2.UnitY);
+ Assert.Equal(MathHelper.Pi / 2f, angle.Radians, _withinDeltaEqualityComparer);
+ Assert.True((-Vector2.UnitY).EqualsWithTolerence(angle.ToUnitVector()));
+ }
+
+ [Fact]
+ public void EqualsTest()
+ {
+ var angle1 = new Angle(0);
+ var angle2 = new Angle(MathHelper.Pi * 2f);
+ Assert.True(angle1 == angle2);
+ angle2.Radians = MathHelper.Pi * 4f;
+ Assert.True(angle1.Equals(angle2));
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/AssertExtensions.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/AssertExtensions.cs
new file mode 100644
index 0000000..72347ca
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/AssertExtensions.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace MonoGame.Extended.Tests
+{
+ public static class AssertExtensions
+ {
+ public static bool AreApproximatelyEqual(Point2 firstPoint, Point2 secondPoint)
+ {
+ return Math.Abs(firstPoint.X - secondPoint.X) < float.Epsilon &&
+ Math.Abs(firstPoint.Y - secondPoint.Y) < float.Epsilon;
+ }
+
+ public static bool AreApproximatelyEqual(RectangleF firstRectangle, RectangleF secondRectangle)
+ {
+ return Math.Abs(firstRectangle.X - secondRectangle.X) < float.Epsilon &&
+ Math.Abs(firstRectangle.Y - secondRectangle.Y) < float.Epsilon &&
+ Math.Abs(firstRectangle.Width - secondRectangle.Width) < float.Epsilon &&
+ Math.Abs(firstRectangle.Height - secondRectangle.Height) < float.Epsilon;
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/BitmapFonts/BitmapFontTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/BitmapFonts/BitmapFontTests.cs
new file mode 100644
index 0000000..8f751bc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/BitmapFonts/BitmapFontTests.cs
@@ -0,0 +1,79 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.BitmapFonts;
+using MonoGame.Extended.TextureAtlases;
+using Xunit;
+
+namespace MonoGame.Extended.Tests.BitmapFonts
+{
+ public class BitmapFontTests
+ {
+ [Fact]
+ public void BitmapFont_Constructor_Test()
+ {
+ var font = CreateTestFont();
+
+ Assert.Equal("Impact", font.Name);
+ Assert.Equal(22, font.LineHeight);
+ }
+
+ [Fact]
+ public void BitmapFont_MeasureString_SingleWord_Test()
+ {
+ var font = CreateTestFont();
+ var size = font.MeasureString("fox");
+
+ Assert.Equal(40, size.Width);
+ Assert.Equal(font.LineHeight, size.Height);
+ }
+
+ [Fact]
+ public void BitmapFont_MeasureString_WithLetterSpacing_Test()
+ {
+ var font = CreateTestFont();
+ font.LetterSpacing = 3;
+
+ var size = font.MeasureString("fox");
+
+ Assert.Equal(46, size.Width);
+ Assert.Equal(size.Height, font.LineHeight);
+ }
+
+ [Fact]
+ public void BitmapFont_MeasureString_MultipleLines_Test()
+ {
+ var font = CreateTestFont();
+ var size = font.MeasureString("box fox\nbox of fox");
+
+ Assert.Equal(123, size.Width);
+ Assert.Equal(size.Height, font.LineHeight * 2);
+ }
+
+ [Fact]
+ public void BitmapFont_MeasureString_EmptyString_Test()
+ {
+ var font = CreateTestFont();
+ var size = font.MeasureString(string.Empty);
+
+ Assert.Equal(0, size.Width);
+ Assert.Equal(0, size.Height);
+ }
+
+ private static BitmapFont CreateTestFont()
+ {
+ var textureRegion = new TextureRegion2D(null, x: 219, y: 61, width: 16, height: 18);
+ var regions = new[]
+ {
+ // extracted from 'Impact' font. 'x' is particularly interesting because it has a negative x offset
+ new BitmapFontRegion(textureRegion, character: ' ', xOffset: 0, yOffset: 0, xAdvance: 6),
+ new BitmapFontRegion(textureRegion, character: 'b', xOffset: 0, yOffset: 7, xAdvance: 17),
+ new BitmapFontRegion(textureRegion, character: 'f', xOffset: 0, yOffset: 7, xAdvance: 9),
+ new BitmapFontRegion(textureRegion, character: 'o', xOffset: 0, yOffset: 11, xAdvance: 16),
+ new BitmapFontRegion(textureRegion, character: 'x', xOffset: -1, yOffset: 11, xAdvance: 13),
+ };
+
+ var font = new BitmapFont("Impact", regions, lineHeight: 22);
+ return font;
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Camera2DTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Camera2DTests.cs
new file mode 100644
index 0000000..2f188b8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Camera2DTests.cs
@@ -0,0 +1,97 @@
+//using Microsoft.Xna.Framework;
+//using MonoGame.Extended.ViewportAdapters;
+//using Xunit;
+
+//namespace MonoGame.Extended.Tests
+//{
+//
+// public class Camera2DTests
+// {
+// [Fact]
+// public void Camera2D_LookAt_Test()
+// {
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var viewportAdapter = new DefaultViewportAdapter(graphicsDevice);
+// var camera = new OrthographicCamera(viewportAdapter);
+
+// camera.LookAt(new Vector2(100, 200));
+
+// Assert.Equal(new Vector2(-300, -40), camera.Position);
+// }
+
+// [Fact]
+// public void Camera2D_GetBoundingFrustum_Test()
+// {
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var camera = new OrthographicCamera(graphicsDevice);
+// var boundingFrustum = camera.GetBoundingFrustum();
+// var corners = boundingFrustum.GetCorners();
+
+// const float delta = 0.01f;
+// TestHelper.AreEqual(new Vector3(0, 0, 1), corners[0], delta);
+// TestHelper.AreEqual(new Vector3(800, 0, 1), corners[1], delta);
+// TestHelper.AreEqual(new Vector3(800, 480, 1), corners[2], delta);
+// TestHelper.AreEqual(new Vector3(0, 480, 1), corners[3], delta);
+// TestHelper.AreEqual(new Vector3(0, 0, 0), corners[4], delta);
+// TestHelper.AreEqual(new Vector3(800, 0, 0), corners[5], delta);
+// TestHelper.AreEqual(new Vector3(800, 480, 0), corners[6], delta);
+// TestHelper.AreEqual(new Vector3(0, 480, 0), corners[7], delta);
+// }
+
+// [Fact]
+// public void Camera2D_BoundingRectangle_Test()
+// {
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var viewport = new DefaultViewportAdapter(graphicsDevice);
+// var camera = new OrthographicCamera(viewport);
+// camera.Move(new Vector2(2, 0));
+// camera.Move(new Vector2(0, 3));
+
+// var boundingRectangle = camera.BoundingRectangle;
+
+// Assert.Equal(2, boundingRectangle.Left, 0.01);
+// Assert.Equal(3, boundingRectangle.Top, 0.01);
+// Assert.Equal(802, boundingRectangle.Right, 0.01);
+// Assert.Equal(483, boundingRectangle.Bottom, 0.01);
+// }
+
+// [Fact]
+// public void Camera2D_ContainsPoint_Test()
+// {
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var camera = new OrthographicCamera(graphicsDevice);
+
+// Assert.Equal(ContainmentType.Contains, camera.Contains(new Point(1, 1)));
+// Assert.Equal(ContainmentType.Contains, camera.Contains(new Point(799, 479)));
+// Assert.Equal(ContainmentType.Disjoint, camera.Contains(new Point(-1, -1)));
+// Assert.Equal(ContainmentType.Disjoint, camera.Contains(new Point(801, 481)));
+// }
+
+// [Fact]
+// public void Camera2D_ContainsVector2_Test()
+// {
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var camera = new OrthographicCamera(graphicsDevice);
+
+// Assert.Equal(ContainmentType.Contains, camera.Contains(new Vector2(799.5f, 479.5f)));
+// Assert.Equal(ContainmentType.Contains, camera.Contains(new Vector2(0.5f, 0.5f)));
+// Assert.Equal(ContainmentType.Disjoint, camera.Contains(new Vector2(-0.5f, -0.5f)));
+// Assert.Equal(ContainmentType.Disjoint, camera.Contains(new Vector2(800.5f, 480.5f)));
+// Assert.Equal(ContainmentType.Disjoint, camera.Contains(new Vector2(-0.5f, 240f)));
+// Assert.Equal(ContainmentType.Contains, camera.Contains(new Vector2(0.5f, 240f)));
+// Assert.Equal(ContainmentType.Contains, camera.Contains(new Vector2(799.5f, 240f)));
+// Assert.Equal(ContainmentType.Disjoint, camera.Contains(new Vector2(800.5f, 240f)));
+// }
+
+// [Fact]
+// public void Camera2D_ContainsRectangle_Test()
+// {
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var camera = new OrthographicCamera(graphicsDevice);
+
+// Assert.Equal(ContainmentType.Intersects, camera.Contains(new Rectangle(-50, -50, 100, 100)));
+// Assert.Equal(ContainmentType.Contains, camera.Contains(new Rectangle(50, 50, 100, 100)));
+// Assert.Equal(ContainmentType.Disjoint, camera.Contains(new Rectangle(850, 500, 100, 100)));
+// }
+// }
+//} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/CollectionAssert.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/CollectionAssert.cs
new file mode 100644
index 0000000..15c105b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/CollectionAssert.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+using Xunit;
+
+namespace MonoGame.Extended.Tests;
+
+public static class CollectionAssert
+{
+ public static void Equal<T>(IReadOnlyList<T> expected, IReadOnlyList<T> actual)
+ {
+ Assert.True(expected.Count == actual.Count, "The number of items in the collections does not match.");
+
+ Assert.All(actual, x => Assert.Contains(x, expected));
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Collections/BagTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Collections/BagTests.cs
new file mode 100644
index 0000000..f89b45b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Collections/BagTests.cs
@@ -0,0 +1,48 @@
+using MonoGame.Extended.Collections;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using Xunit;
+
+namespace MonoGame.Extended.Tests.Collections
+{
+ public class BagTests
+ {
+ [Fact]
+ public void Bag_Enumeration_Does_Not_Allocate()
+ {
+ var bag = new Bag<int>();
+ for (int i = 0; i < 100; i++) bag.Add(i);
+ // ensure we have plenty of memory and that the heap only increases for the duration of this test
+ Assert.True(GC.TryStartNoGCRegion(Unsafe.SizeOf<Bag<int>.BagEnumerator>() * 1000));
+ var heapSize = GC.GetAllocatedBytesForCurrentThread();
+
+ // this should NOT allocate
+ foreach (int i in bag)
+ {
+ // assert methods cause the NoGCRegion to fail, so do this manually
+ if (GC.GetAllocatedBytesForCurrentThread() != heapSize)
+ Assert.True(false);
+ }
+
+ // sanity check: this SHOULD allocate
+ foreach (int _ in (IEnumerable<int>)bag)
+ {
+ // assert methods cause the NoGCRegion to fail, so do this manually
+ if (GC.GetAllocatedBytesForCurrentThread() == heapSize)
+ Assert.True(false);
+ }
+
+ // Wrap in if statement due to exception thrown when running test through
+ // cake build script or when debugging test script manually.
+ if(GCSettings.LatencyMode == GCLatencyMode.NoGCRegion)
+ {
+ GC.EndNoGCRegion();
+ }
+ }
+
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Collections/DequeTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Collections/DequeTests.cs
new file mode 100644
index 0000000..3196309
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Collections/DequeTests.cs
@@ -0,0 +1,408 @@
+using System;
+using System.Linq;
+using MonoGame.Extended.Collections;
+using Xunit;
+
+namespace MonoGame.Extended.Tests.Collections
+{
+ public class DequeTests
+ {
+ private class TestDequeElement
+ {
+ public int Value { get; set; }
+ }
+
+ private readonly Random _random;
+
+ public DequeTests()
+ {
+ _random = new Random();
+ }
+
+ [Fact]
+ public void Deque_Constructor_Default()
+ {
+ var deque = new Deque<object>();
+ Assert.True(deque.Count == 0);
+ Assert.True(deque.Capacity == 0);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(50)]
+ public void Deque_Constructor_Collection(int count)
+ {
+ var elements = new TestDequeElement[count];
+ for (var i = 0; i < count; i++)
+ {
+ elements[i] = new TestDequeElement
+ {
+ Value = i
+ };
+ }
+ var deque = new Deque<TestDequeElement>(elements);
+ Assert.True(deque.Count == count);
+ Assert.True(deque.Capacity == count);
+ for (var index = 0; index < deque.Count; index++)
+ {
+ Assert.True(deque[index].Value == index);
+ }
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(50)]
+ public void Deque_Constructor_Capacity(int capacity)
+ {
+ var deque = new Deque<TestDequeElement>(capacity);
+ Assert.True(deque.Count == 0);
+ Assert.True(deque.Capacity == capacity);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(50)]
+ public void Deque_Clear(int count)
+ {
+ var elements = new TestDequeElement[count];
+ for (var i = 0; i < count; i++)
+ {
+ elements[i] = new TestDequeElement
+ {
+ Value = i
+ };
+ }
+ var deque = new Deque<TestDequeElement>(elements);
+ deque.Clear();
+ Assert.True(deque.Count == 0);
+ Assert.True(deque.Capacity >= count);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(50)]
+ public void Deque_Trim_And_Clear(int count)
+ {
+ var elements = new TestDequeElement[count];
+ for (var i = 0; i < count; i++)
+ {
+ elements[i] = new TestDequeElement
+ {
+ Value = i
+ };
+ }
+ var deque = new Deque<TestDequeElement>(elements);
+ deque.Clear();
+ deque.TrimExcess();
+ Assert.True(deque.Count == 0);
+ Assert.True(deque.Capacity == 0);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(50)]
+ public void Deque_Trim_Front(int count)
+ {
+ var elements = new TestDequeElement[count];
+ for (var i = 0; i < count; i++)
+ {
+ elements[i] = new TestDequeElement
+ {
+ Value = i
+ };
+ }
+ var deque = new Deque<TestDequeElement>(elements);
+
+ for (var i = 0; i < count; i++)
+ {
+ deque.RemoveFromFront(out _);
+ deque.Capacity = deque.Count;
+ Assert.True(deque.Count == count - 1 - i);
+ Assert.True(deque.Capacity == count - 1 - i);
+ }
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(50)]
+ public void Deque_Trim_Back(int count)
+ {
+ var elements = new TestDequeElement[count];
+ for (var i = 0; i < count; i++)
+ {
+ elements[i] = new TestDequeElement
+ {
+ Value = i
+ };
+ }
+ var deque = new Deque<TestDequeElement>(elements);
+
+ for (var i = 0; i < count; i++)
+ {
+ deque.RemoveFromBack(out _);
+ deque.Capacity = deque.Count;
+ Assert.True(deque.Count == count - 1 - i);
+ Assert.True(deque.Capacity == count - 1 - i);
+ }
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(50)]
+ public void Deque_Add_Front(int count)
+ {
+ var deque = new Deque<TestDequeElement>();
+ for (var i = 0; i < count; i++)
+ {
+ deque.AddToFront(new TestDequeElement
+ {
+ Value = i
+ });
+ }
+ Assert.True(deque.Count == count);
+ Assert.True(deque.Capacity >= count);
+ for (var index = 0; index < deque.Count; index++)
+ {
+ var element = deque[index];
+ Assert.True(element.Value == deque.Count - 1 - index);
+ }
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(50)]
+ public void Deque_Add_Back(int count)
+ {
+ var deque = new Deque<TestDequeElement>();
+ for (var i = 0; i < count; i++)
+ {
+ deque.AddToBack(new TestDequeElement
+ {
+ Value = i
+ });
+ }
+ Assert.True(deque.Count == count);
+ Assert.True(deque.Capacity >= count);
+ for (var index = 0; index < deque.Count; index++)
+ {
+ var element = deque[index];
+ Assert.True(element.Value == index);
+ }
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(50)]
+ public void Deque_Remove_Front(int count)
+ {
+ var elements = new TestDequeElement[count];
+ for (var i = 0; i < count; i++)
+ {
+ elements[i] = new TestDequeElement
+ {
+ Value = i
+ };
+ }
+ var deque = new Deque<TestDequeElement>(elements);
+
+ var index = 0;
+ while (deque.RemoveFromFront(out var element))
+ {
+ Assert.True(element.Value == index);
+ index++;
+ }
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(50)]
+ public void Deque_Remove_Back(int count)
+ {
+ var elements = new TestDequeElement[count];
+ for (var i = 0; i < count; i++)
+ {
+ elements[i] = new TestDequeElement
+ {
+ Value = i
+ };
+ }
+ var deque = new Deque<TestDequeElement>(elements);
+
+ var index = 0;
+ while (deque.RemoveFromBack(out var element))
+ {
+ Assert.True(element.Value == elements.Length - 1 - index);
+ index++;
+ }
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(50)]
+ public void Deque_Get_Front(int count)
+ {
+ var elements = new TestDequeElement[count];
+ for (var i = 0; i < count; i++)
+ {
+ elements[i] = new TestDequeElement
+ {
+ Value = i
+ };
+ }
+ var deque = new Deque<TestDequeElement>(elements);
+ var indices = Enumerable.Range(0, count);
+ foreach (var index in indices)
+ {
+ deque.GetFront(out var element);
+ deque.RemoveFromFront();
+ Assert.True(element.Value == index);
+ }
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(50)]
+ public void Deque_Get_Back(int count)
+ {
+ var elements = new TestDequeElement[count];
+ for (var i = 0; i < count; i++)
+ {
+ elements[i] = new TestDequeElement
+ {
+ Value = i
+ };
+ }
+ var deque = new Deque<TestDequeElement>(elements);
+ var indices = Enumerable.Range(0, count);
+ foreach (var index in indices)
+ {
+ deque.GetBack(out var element);
+ deque.RemoveFromBack();
+ Assert.True(element.Value == count - 1 - index);
+ }
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(50)]
+ public void Deque_Get_Index(int count)
+ {
+ var elements = new TestDequeElement[count];
+ for (var i = 0; i < count; i++)
+ {
+ elements[i] = new TestDequeElement
+ {
+ Value = i
+ };
+ }
+ var deque = new Deque<TestDequeElement>(elements);
+ var indices = Enumerable.Range(0, count).ToList().Shuffle(_random);
+ foreach (var index in indices)
+ {
+ deque.Get(index, out var element);
+ Assert.True(element.Value == index);
+ }
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(50)]
+ public void Deque_ForEach_Iteration(int count)
+ {
+ var elements = new TestDequeElement[count];
+ for (var i = 0; i < count; i++)
+ {
+ elements[i] = new TestDequeElement
+ {
+ Value = i
+ };
+ }
+ var deque = new Deque<TestDequeElement>(elements);
+ var counter = 0;
+ foreach (var element in deque)
+ {
+ Assert.True(element.Value == counter);
+ counter++;
+ }
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(50)]
+ public void Deque_ForEach_Iteration_Modified(int count)
+ {
+ var elements = new TestDequeElement[count];
+ for (var i = 0; i < count; i++)
+ {
+ elements[i] = new TestDequeElement
+ {
+ Value = i
+ };
+ }
+ var deque = new Deque<TestDequeElement>(elements);
+ var counter = 0;
+ foreach (var element in deque)
+ {
+ Assert.True(element.Value == counter);
+ counter++;
+ deque.RemoveFromFront();
+ }
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(50)]
+ public void Deque_Remove(int count)
+ {
+ var elements = new TestDequeElement[count];
+ for (var i = 0; i < count; i++)
+ {
+ elements[i] = new TestDequeElement
+ {
+ Value = i
+ };
+ }
+ var deque = new Deque<TestDequeElement>(elements);
+ var counter = count;
+ while (deque.Count > 0)
+ {
+ var index = _random.Next(0, deque.Count - 1);
+ deque.RemoveAt(index);
+ counter--;
+ Assert.True(deque.Count == counter);
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Content/ContentReaderExtensionsTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Content/ContentReaderExtensionsTests.cs
new file mode 100644
index 0000000..e1c701d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Content/ContentReaderExtensionsTests.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Linq;
+using MonoGame.Extended.Collections;
+using MonoGame.Extended.Content;
+using Xunit;
+
+namespace MonoGame.Extended.Tests.Content
+{
+ public class ContentReaderExtensionsTests
+ {
+ [Theory]
+ [InlineData("testsuperdir/testsubdir1/testsubdir2/resource", "testsuperdir/testsubdir1/testsubdir2/resource")]
+ [InlineData("testsuperdir/testsubdir1/../testsubdir2/resource","testsuperdir/testsubdir2/resource")]
+ [InlineData("testsuperdir/../resource","resource")]
+ [InlineData("../testsuperdir/testsubdir1/../testsubdir2/resource","../testsuperdir/testsubdir2/resource")]
+ [InlineData("testsuperdir/testsubdir1/testsubdir2/../../testsubdir3/resource","testsuperdir/testsubdir3/resource")]
+ [InlineData("testsuperdir/testsubdir1/../testsubdir2/../testsubdir3/resource", "testsuperdir/testsubdir3/resource")]
+ public void ContentReaderExtensions_ShortenRelativePath(string input, string expectedoutput)
+ {
+ Assert.True(ContentReaderExtensions.ShortenRelativePath(input) == expectedoutput);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/MockGameWindow.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/MockGameWindow.cs
new file mode 100644
index 0000000..a65479b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/MockGameWindow.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Drawing;
+using Microsoft.Xna.Framework;
+using Point = Microsoft.Xna.Framework.Point;
+using Rectangle = Microsoft.Xna.Framework.Rectangle;
+
+namespace MonoGame.Extended.Tests
+{
+ public class MockGameWindow : GameWindow
+ {
+ public override bool AllowUserResizing { get; set; }
+ public override Rectangle ClientBounds { get; }
+ public override Point Position { get; set; }
+ public override DisplayOrientation CurrentOrientation { get; }
+ public override IntPtr Handle { get; }
+ public override string ScreenDeviceName { get; }
+
+ public MockGameWindow()
+ {
+ }
+
+ public override void BeginScreenDeviceChange(bool willBeFullScreen)
+ {
+ }
+
+ public override void EndScreenDeviceChange(string screenDeviceName, int clientWidth, int clientHeight)
+ {
+ }
+
+ protected override void SetSupportedOrientations(DisplayOrientation orientations)
+ {
+ }
+
+ protected override void SetTitle(string title)
+ {
+ }
+
+#if __MonoCS__
+ public override Icon Icon { get; set; }
+#endif
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/MonoGame.Extended.Tests.csproj b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/MonoGame.Extended.Tests.csproj
new file mode 100644
index 0000000..9e831ff
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/MonoGame.Extended.Tests.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\source\MonoGame.Extended\MonoGame.Extended.csproj" />
+ <ProjectReference Include="..\..\source\MonoGame.Extended.Particles\MonoGame.Extended.Particles.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Folder Include="Content\" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/OpenTK.dll.config b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/OpenTK.dll.config
new file mode 100644
index 0000000..1dc6f45
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/OpenTK.dll.config
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+ <dllmap os="linux" dll="opengl32.dll" target="libGL.so.1" />
+ <dllmap os="linux" dll="glu32.dll" target="libGLU.so.1" />
+ <dllmap os="linux" dll="openal32.dll" target="libopenal.so.1" />
+ <dllmap os="linux" dll="alut.dll" target="libalut.so.0" />
+ <dllmap os="linux" dll="opencl.dll" target="libOpenCL.so" />
+ <dllmap os="linux" dll="libX11" target="libX11.so.6" />
+ <dllmap os="linux" dll="libXi" target="libXi.so.6" />
+ <dllmap os="linux" dll="SDL2.dll" target="libSDL2-2.0.so.0" />
+ <dllmap os="osx" dll="opengl32.dll" target="/System/Library/Frameworks/OpenGL.framework/OpenGL" />
+ <dllmap os="osx" dll="openal32.dll" target="/System/Library/Frameworks/OpenAL.framework/OpenAL" />
+ <dllmap os="osx" dll="alut.dll" target="/System/Library/Frameworks/OpenAL.framework/OpenAL" />
+ <dllmap os="osx" dll="libGLES.dll" target="/System/Library/Frameworks/OpenGLES.framework/OpenGLES" />
+ <dllmap os="osx" dll="libGLESv1_CM.dll" target="/System/Library/Frameworks/OpenGLES.framework/OpenGLES" />
+ <dllmap os="osx" dll="libGLESv2.dll" target="/System/Library/Frameworks/OpenGLES.framework/OpenGLES" />
+ <dllmap os="osx" dll="opencl.dll" target="/System/Library/Frameworks/OpenCL.framework/OpenCL" />
+ <dllmap os="osx" dll="SDL2.dll" target="libSDL2.dylib" />
+ <!-- XQuartz compatibility (X11 on Mac) -->
+ <dllmap os="osx" dll="libGL.so.1" target="/usr/X11/lib/libGL.dylib" />
+ <dllmap os="osx" dll="libX11" target="/usr/X11/lib/libX11.dylib" />
+ <dllmap os="osx" dll="libXcursor.so.1" target="/usr/X11/lib/libXcursor.dylib" />
+ <dllmap os="osx" dll="libXi" target="/usr/X11/lib/libXi.dylib" />
+ <dllmap os="osx" dll="libXinerama" target="/usr/X11/lib/libXinerama.dylib" />
+ <dllmap os="osx" dll="libXrandr.so.2" target="/usr/X11/lib/libXrandr.dylib" />
+</configuration>
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/AssertionModifier.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/AssertionModifier.cs
new file mode 100644
index 0000000..ff23d8b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/AssertionModifier.cs
@@ -0,0 +1,25 @@
+//using System;
+//using MonoGame.Extended.Particles;
+//using MonoGame.Extended.Particles.Modifiers;
+//using Xunit;
+
+//namespace MonoGame.Extended.Tests.Particles
+//{
+// internal class AssertionModifier : Modifier
+// {
+// private readonly Predicate<Particle> _predicate;
+
+// public AssertionModifier(Predicate<Particle> predicate)
+// {
+// _predicate = predicate;
+// }
+
+// public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator)
+// {
+// while (iterator.HasNext) {
+// var particle = iterator.Next();
+// Assert.IsTrue(_predicate(*particle));
+// }
+// }
+// }
+//} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/ColourTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/ColourTests.cs
new file mode 100644
index 0000000..49b5e03
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/ColourTests.cs
@@ -0,0 +1,126 @@
+using System;
+using Microsoft.Xna.Framework;
+using Xunit;
+
+namespace MonoGame.Extended.Tests.Particles
+{
+ public class ColourTests
+ {
+ public class Constructor
+ {
+ [Fact]
+ public void WhenGivenValues_ReturnsInitializedColour()
+ {
+ var colour = new HslColor(1f, 1f, 1f);
+ Assert.Equal(1f, colour.H);
+ Assert.Equal(1f, colour.S);
+ Assert.Equal(1f, colour.L);
+ }
+ }
+
+ public class AreEqualColourMethod
+ {
+ [Fact]
+ public void WhenGivenEqualValues_ReturnsTrue()
+ {
+ var x = new HslColor(360f, 1f, 1f);
+ var y = new HslColor(360f, 1f, 1f);
+ Assert.Equal(x, y);
+ }
+
+ [Fact]
+ public void WhenGivenDifferentValues_ReturnsFalse()
+ {
+ var x = new HslColor(0f, 1f, 0f);
+ var y = new HslColor(360f, 1f, 1f);
+ Assert.False(x.Equals(y));
+ }
+ }
+
+ public class AreEqualObjectMethod
+ {
+ [Fact]
+ public void WhenGivenEqualColour_ReturnsTrue()
+ {
+ var x = new HslColor(360f, 1f, 1f);
+
+ Object y = new HslColor(360f, 1f, 1f);
+ Assert.Equal(x, y);
+ }
+
+ [Fact]
+ public void WhenGivenDifferentColour_ReturnsFalse()
+ {
+ var x = new HslColor(360f, 1f, 1f);
+
+ Object y = new HslColor(0f, 1f, 0f);
+ Assert.False(x.Equals(y));
+ }
+
+ [Fact]
+ public void WhenGivenObjectOfAntotherType_ReturnsFalse()
+ {
+ var colour = new HslColor(360f, 1f, 1f);
+
+ // ReSharper disable once SuspiciousTypeConversion.Global
+ Assert.False(colour.Equals(DateTime.Now));
+ }
+ }
+
+ public class GetHashCodeMethod
+ {
+ [Fact]
+ public void WhenObjectsAreDifferent_YieldsDifferentHashCodes()
+ {
+ var x = new HslColor(0f, 1f, 0f);
+ var y = new HslColor(360f, 1f, 1f);
+ Assert.NotEqual(x.GetHashCode(), y.GetHashCode());
+ }
+
+ [Fact]
+ public void WhenObjectsAreSame_YieldsIdenticalHashCodes()
+ {
+ var x = new HslColor(180f, 0.5f, 0.5f);
+ var y = new HslColor(180f, 0.5f, 0.5f);
+ Assert.Equal(x.GetHashCode(), y.GetHashCode());
+ }
+ }
+
+ public class ToStringMethod
+ {
+ [Theory]
+ [InlineData(360f, 1f, 1f, "H:0.0° S:100.0 L:100.0")]
+ [InlineData(180f, 0.5f, 0.5f, "H:180.0° S:50.0 L:50.0")]
+ [InlineData(0f, 0f, 0f, "H:0.0° S:0.0 L:0.0")]
+ public void ReturnsCorrectValue(float h, float s, float l, string expected)
+ {
+ var colour = new HslColor(h, s, l);
+ Assert.Equal(expected, colour.ToString());
+ }
+ }
+
+ public class ToRgbMethod
+ {
+ [Theory]
+ [InlineData(0f, 1f, 0.5f, "{R:255 G:0 B:0 A:255}")] // Color.Red
+ [InlineData(360f, 1f, 0.5f, "{R:255 G:0 B:0 A:255}")] // Color.Red
+ [InlineData(120f, 1f, 0.5f, "{R:0 G:255 B:0 A:255}")] // Color.Lime
+ public void ReturnsCorrectValue(float h, float s, float l, string expected)
+ {
+ var hslColour = new HslColor(h, s, l);
+ Color rgbColor = hslColour.ToRgb();
+
+ Assert.Equal(expected, rgbColor.ToString());
+ }
+
+ [Fact]
+ public void FromRgbAndToRgbWorksCorrectly()
+ {
+ HslColor blueHsl = HslColor.FromRgb(Color.Blue);
+ Color blueRgb = blueHsl.ToRgb();
+
+ Assert.Equal(Color.Blue, blueRgb);
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/EmitterTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/EmitterTests.cs
new file mode 100644
index 0000000..4ec9b65
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/EmitterTests.cs
@@ -0,0 +1,131 @@
+//using System;
+//using Microsoft.Xna.Framework;
+//using MonoGame.Extended.Particles;
+//using MonoGame.Extended.Particles.Modifiers;
+//using MonoGame.Extended.Particles.Profiles;
+//using Xunit;
+
+//namespace MonoGame.Extended.Tests.Particles
+//{
+//
+// public class EmitterTests
+// {
+// public class UpdateMethod
+// {
+// [Fact]
+// public void WhenThereAreParticlesToExpire_DecreasesActiveParticleCount()
+// {
+// var subject = new ParticleEmitter(null, 100, TimeSpan.FromSeconds(1), Profile.Point())
+// {
+// AutoTrigger = false,
+// Parameters = new ParticleReleaseParameters
+// {
+// Quantity = 1
+// }
+// };
+
+// subject.Trigger(new Vector2(0f, 0f));
+// Assert.Equal(subject.ActiveParticles, 1);
+
+// subject.Update(2f);
+// Assert.Equal(subject.ActiveParticles, 0);
+// }
+
+// [Fact]
+// public void WhenThereAreParticlesToExpire_DoesNotPassExpiredParticlesToModifiers()
+// {
+// var subject = new ParticleEmitter(null, 100, TimeSpan.FromSeconds(1), Profile.Point())
+// {
+// Parameters = new ParticleReleaseParameters()
+// {
+// Quantity = 1
+// },
+// Modifiers =
+// {
+// new AssertionModifier(particle => particle.Age <= 1f)
+// }
+// };
+
+// subject.Trigger(new Vector2(0f, 0f));
+// subject.Update(0.5f);
+// subject.Trigger(new Vector2(0f, 0f));
+// subject.Update(0.5f);
+// subject.Trigger(new Vector2(0f, 0f));
+// subject.Update(0.5f);
+// }
+
+// [Fact]
+// public void WhenThereAreNoActiveParticles_GracefullyDoesNothing()
+// {
+// var subject = new ParticleEmitter(null, 100, TimeSpan.FromSeconds(1), Profile.Point()) { AutoTrigger = false };
+
+// subject.Update(0.5f);
+// Assert.Equal(subject.ActiveParticles, 0);
+// }
+// }
+
+// public class TriggerMethod
+// {
+// [Fact]
+// public void WhenEnoughHeadroom_IncreasesActiveParticlesCountByReleaseQuantity()
+// {
+// var subject = new ParticleEmitter(null, 100, TimeSpan.FromSeconds(1), Profile.Point())
+// {
+// Parameters = new ParticleReleaseParameters
+// {
+// Quantity = 10
+// }
+// };
+// Assert.Equal(subject.ActiveParticles, 0);
+// subject.Trigger(new Vector2(0f, 0f));
+// Assert.Equal(subject.ActiveParticles, 10);
+// }
+
+// [Fact]
+// public void WhenNotEnoughHeadroom_IncreasesActiveParticlesCountByRemainingParticles()
+// {
+// var subject = new ParticleEmitter(null, 15, TimeSpan.FromSeconds(1), Profile.Point())
+// {
+// Parameters = new ParticleReleaseParameters
+// {
+// Quantity = 10
+// }
+// };
+
+// subject.Trigger(new Vector2(0f, 0f));
+// Assert.Equal(subject.ActiveParticles, 10);
+// subject.Trigger(new Vector2(0f, 0f));
+// Assert.Equal(subject.ActiveParticles, 15);
+// }
+
+// [Fact]
+// public void WhenNoRemainingParticles_DoesNotIncreaseActiveParticlesCount()
+// {
+// var subject = new ParticleEmitter(null, 10, TimeSpan.FromSeconds(1), Profile.Point())
+// {
+// Parameters = new ParticleReleaseParameters
+// {
+// Quantity = 10
+// }
+// };
+
+// subject.Trigger(new Vector2(0f, 0f));
+// Assert.Equal(subject.ActiveParticles, 10);
+// subject.Trigger(new Vector2(0f, 0f));
+// Assert.Equal(subject.ActiveParticles, 10);
+// }
+// }
+
+// public class DisposeMethod
+// {
+// [Fact]
+// public void IsIdempotent()
+// {
+// var subject = new ParticleEmitter(null, 10, TimeSpan.FromSeconds(1), Profile.Point());
+
+// subject.Dispose();
+// subject.Dispose();
+// }
+// }
+// }
+//} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/ParticleBufferTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/ParticleBufferTests.cs
new file mode 100644
index 0000000..ef2e159
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/ParticleBufferTests.cs
@@ -0,0 +1,184 @@
+//using System;
+//using MonoGame.Extended.Particles;
+//using Xunit;
+
+//namespace MonoGame.Extended.Tests.Particles
+//{
+//
+// public class ParticleBufferTests
+// {
+// public class AvailableProperty
+// {
+// [Fact]
+// public void WhenNoParticlesReleased_ReturnsBufferSize()
+// {
+// var subject = new ParticleBuffer(100);
+
+// Assert.Equal(subject.Available, 100);
+// }
+
+// [Fact]
+// public void WhenSomeParticlesReleased_ReturnsAvailableCount()
+// {
+// var subject = new ParticleBuffer(100);
+
+// subject.Release(10);
+// Assert.Equal(subject.Available, 90);
+// }
+
+// [Fact]
+// public void WhenAllParticlesReleased_ReturnsZero()
+// {
+// var subject = new ParticleBuffer(100);
+
+// subject.Release(100);
+// Assert.Equal(subject.Available, 0);
+
+// }
+// }
+
+// public class CountProperty
+// {
+// [Fact]
+// public void WhenNoParticlesReleased_ReturnsZero()
+// {
+// var subject = new ParticleBuffer(100);
+// Assert.Equal(subject.Count, 0);
+// }
+
+// [Fact]
+// public void WhenSomeParticlesReleased_ReturnsCount()
+// {
+// var subject = new ParticleBuffer(100);
+
+// subject.Release(10);
+// Assert.Equal(subject.Count, 10);
+
+// }
+
+// [Fact]
+// public void WhenAllParticlesReleased_ReturnsZero()
+// {
+// var subject = new ParticleBuffer(100);
+
+// subject.Release(100);
+// Assert.Equal(subject.Count, 100);
+
+// }
+// }
+
+// public class ReleaseMethod
+// {
+// [Fact]
+// public void WhenPassedReasonableQuantity_ReturnsNumberReleased()
+// {
+// var subject = new ParticleBuffer(100);
+
+// var count = subject.Release(50);
+
+// Assert.Equal(count.Total, 50);
+// }
+
+// [Fact]
+// public void WhenPassedImpossibleQuantity_ReturnsNumberActuallyReleased()
+// {
+// var subject = new ParticleBuffer(100);
+
+// var count = subject.Release(200);
+// Assert.Equal(count.Total, 100);
+// }
+// }
+
+// public class ReclaimMethod
+// {
+// [Fact]
+// public void WhenPassedReasonableNumber_ReclaimsParticles()
+// {
+// var subject = new ParticleBuffer(100);
+
+// subject.Release(100);
+// Assert.Equal(subject.Count, 100);
+
+// subject.Reclaim(50);
+// Assert.Equal(subject.Count, 50);
+// }
+// }
+
+// //public class CopyToMethod
+// //{
+// // [Fact]
+// // public void WhenBufferIsSequential_CopiesParticlesInOrder()
+// // {
+// // unsafe
+// // {
+// // var subject = new ParticleBuffer(10);
+// // var iterator = subject.Release(5);
+
+// // do
+// // {
+// // var particle = iterator.Next();
+// // particle->Age = 1f;
+// // }
+// // while (iterator.HasNext);
+
+// // var destination = new Particle[10];
+
+// // fixed (Particle* buffer = destination)
+// // {
+// // subject.CopyTo((IntPtr)buffer);
+// // }
+
+// // Assert.Equal(destination[0].Age, 1f, 0.0001);
+// // Assert.Equal(destination[1].Age, 1f, 0.0001);
+// // Assert.Equal(destination[2].Age, 1f, 0.0001);
+// // Assert.Equal(destination[3].Age, 1f, 0.0001);
+// // Assert.Equal(destination[4].Age, 1f, 0.0001);
+// // }
+// // }
+// //}
+
+// //public class CopyToReverseMethod
+// //{
+// // [Fact]
+// // public void WhenBufferIsSequential_CopiesParticlesInReverseOrder()
+// // {
+// // unsafe
+// // {
+// // var subject = new ParticleBuffer(10);
+// // var iterator = subject.Release(5);
+
+// // do
+// // {
+// // var particle = iterator.Next();
+// // particle->Age = 1f;
+// // }
+// // while (iterator.HasNext);
+
+// // var destination = new Particle[10];
+
+// // fixed (Particle* buffer = destination)
+// // {
+// // subject.CopyToReverse((IntPtr)buffer);
+// // }
+
+// // Assert.Equal(destination[0].Age, 1f, 0.0001);
+// // Assert.Equal(destination[1].Age, 1f, 0.0001);
+// // Assert.Equal(destination[2].Age, 1f, 0.0001);
+// // Assert.Equal(destination[3].Age, 1f, 0.0001);
+// // Assert.Equal(destination[4].Age, 1f, 0.0001);
+// // }
+// // }
+// //}
+
+// public class DisposeMethod
+// {
+// [Fact]
+// public void IsIdempotent()
+// {
+// var subject = new ParticleBuffer(100);
+// subject.Dispose();
+// subject.Dispose();
+// }
+// }
+// }
+//} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/Profiles/PointProfileTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/Profiles/PointProfileTests.cs
new file mode 100644
index 0000000..cb22b3b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/Profiles/PointProfileTests.cs
@@ -0,0 +1,33 @@
+using System;
+using MonoGame.Extended.Particles.Profiles;
+using Xunit;
+
+namespace MonoGame.Extended.Tests.Particles.Profiles
+{
+
+ public class PointProfileTests
+ {
+ [Fact]
+ public void ReturnsZeroOffset()
+ {
+ var subject = new PointProfile();
+
+ subject.GetOffsetAndHeading(out var offset, out _);
+
+ Assert.Equal(0f, offset.X);
+ Assert.Equal(0f, offset.Y);
+ }
+
+ [Fact]
+ public void ReturnsHeadingAsUnitVector()
+ {
+ var subject = new PointProfile();
+
+ subject.GetOffsetAndHeading(out _, out var heading);
+
+ var length = Math.Sqrt(heading.X * heading.X + heading.Y * heading.Y);
+ Assert.Equal(1f, length, 6);
+ }
+
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/Profiles/RingProfileTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/Profiles/RingProfileTests.cs
new file mode 100644
index 0000000..a18f5b7
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Particles/Profiles/RingProfileTests.cs
@@ -0,0 +1,38 @@
+using System;
+using MonoGame.Extended.Particles.Profiles;
+using Xunit;
+
+namespace MonoGame.Extended.Tests.Particles.Profiles
+{
+ public class RingProfileTests
+ {
+ [Fact]
+ public void ReturnsOffsetEqualToRadius()
+ {
+ var subject = new RingProfile
+ {
+ Radius = 10f
+ };
+ subject.GetOffsetAndHeading(out var offset, out _);
+
+ var length = Math.Sqrt(offset.X * offset.X + offset.Y * offset.Y);
+ Assert.Equal(10f, length, 6);
+ }
+
+ [Fact]
+ public void WhenRadiateIsTrue_HeadingIsEqualToNormalizedOffset()
+ {
+ var subject = new RingProfile
+ {
+ Radius = 10f,
+ Radiate = Profile.CircleRadiation.Out
+ };
+ subject.GetOffsetAndHeading(out var offset, out var heading);
+
+ Assert.Equal(heading.X, offset.X / 10, 6);
+ Assert.Equal(heading.Y, offset.Y / 10, 6);
+
+ }
+
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/BoundingRectangleTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/BoundingRectangleTests.cs
new file mode 100644
index 0000000..6f95cf5
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/BoundingRectangleTests.cs
@@ -0,0 +1,396 @@
+//using System.Collections.Generic;
+//using System.Globalization;
+//using Microsoft.Xna.Framework;
+//using Xunit;
+
+//namespace MonoGame.Extended.Tests.Primitives
+//{
+//
+// public class BoundingRectangleTests
+// {
+// public IEnumerable<TestCaseData> ConstructorTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Point2(), new Vector2()).SetName(
+// "The empty bounding rectangle has the expected position and radii.");
+// yield return
+// new TestCaseData(new Point2(5, 5), new Vector2(15, 15)).SetName(
+// "A non-empty bounding rectangle has the expected position and radii.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(ConstructorTestCases))]
+// public void Constructor(Point2 centre, Vector2 radii)
+// {
+// var boundingRectangle = new BoundingRectangle(centre, radii);
+// Assert.Equal(centre, boundingRectangle.Center);
+// Assert.Equal(radii, boundingRectangle.HalfExtents);
+// }
+
+// public IEnumerable<TestCaseData> CreateFromMinimumMaximumTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Point2(), new Point2(), new BoundingRectangle()).SetName(
+// "The bounding rectangle created from the zero minimum point and zero maximum point is the empty bounding rectangle.")
+// ;
+// yield return
+// new TestCaseData(new Point2(5, 5), new Point2(15, 15),
+// new BoundingRectangle(new Point2(10, 10), new Size2(5, 5))).SetName(
+// "The bounding rectangle created from the non-zero minimum point and the non-zero maximum point is the expected bounding rectangle.")
+// ;
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(CreateFromMinimumMaximumTestCases))]
+// public void CreateFromMinimumMaximum(Point2 minimum, Point2 maximum, BoundingRectangle expectedBoundingRectangle)
+// {
+// var actualBoundingRectangle = BoundingRectangle.CreateFrom(minimum, maximum);
+// Assert.Equal(expectedBoundingRectangle, actualBoundingRectangle);
+// }
+
+// public IEnumerable<TestCaseData> CreateFromPointsTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(null, new BoundingRectangle()).SetName(
+// "The bounding rectangle created from null points is the empty bounding rectangle.");
+// yield return
+// new TestCaseData(new Point2[0], new BoundingRectangle()).SetName(
+// "The bounding rectangle created from the empty set of points is the empty bounding rectangle.");
+// yield return
+// new TestCaseData(
+// new[]
+// {
+// new Point2(5, 5), new Point2(10, 10), new Point2(15, 15), new Point2(-5, -5),
+// new Point2(-15, -15)
+// }, new BoundingRectangle(new Point2(0, 0), new Size2(15, 15))).SetName(
+// "The bounding rectangle created from a non-empty set of points is the expected bounding rectangle.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(CreateFromPointsTestCases))]
+// public void CreateFromPoints(Point2[] points, BoundingRectangle expectedBoundingRectangle)
+// {
+// var actualBoundingRectangle = BoundingRectangle.CreateFrom(points);
+// Assert.Equal(expectedBoundingRectangle, actualBoundingRectangle);
+// }
+
+// public IEnumerable<TestCaseData> CreateFromTransformedTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new BoundingRectangle(), Matrix2.Identity, new BoundingRectangle()).SetName(
+// "The bounding rectangle created from the empty bounding rectangle transformed by the identity matrix is the empty bounding rectangle.")
+// ;
+// yield return
+// new TestCaseData(new BoundingRectangle(new Point2(0, 0), new Size2(20, 20)), Matrix2.CreateScale(2), new BoundingRectangle(new Point2(0, 0), new Size2(40, 40))).SetName(
+// "The bounding rectangle created from a non-empty bounding rectangle transformed by a non-identity matrix is the expected bounding rectangle.")
+// ;
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(CreateFromTransformedTestCases))]
+// public void CreateFromTransformed(BoundingRectangle boundingRectangle, Matrix2 transformMatrix,
+// BoundingRectangle expectedBoundingRectangle)
+// {
+// var actualBoundingRectangle = BoundingRectangle.Transform(boundingRectangle, ref transformMatrix);
+// Assert.Equal(expectedBoundingRectangle, actualBoundingRectangle);
+// }
+
+// public IEnumerable<TestCaseData> UnionTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new BoundingRectangle(), new BoundingRectangle(), new BoundingRectangle()).SetName(
+// "The union of two empty bounding rectangles is the empty bounding rectangle.");
+// yield return
+// new TestCaseData(new BoundingRectangle(new Point2(0, 0), new Size2(15, 15)),
+// new BoundingRectangle(new Point2(20, 20), new Size2(40, 40)), new BoundingRectangle(new Point2(20, 20), new Size2(40, 40)))
+// .SetName(
+// "The union of two non-empty bounding rectangles is the expected bounding rectangle.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(UnionTestCases))]
+// public void Union(BoundingRectangle boundingRectangle1, BoundingRectangle boundingRectangle2, BoundingRectangle expectedBoundingRectangle)
+// {
+// Assert.Equal(expectedBoundingRectangle, boundingRectangle1.Union(boundingRectangle2));
+// Assert.Equal(expectedBoundingRectangle, BoundingRectangle.Union(boundingRectangle1, boundingRectangle2));
+// }
+
+// public IEnumerable<TestCaseData> IntersectionTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new BoundingRectangle(), new BoundingRectangle(), new BoundingRectangle()).SetName(
+// "The intersection of two empty bounding rectangles is the empty bounding box.");
+// yield return
+// new TestCaseData(new BoundingRectangle(new Point2(-10, -10), new Size2(15, 15)),
+// new BoundingRectangle(new Point2(20, 20), new Size2(40, 40)),
+// new BoundingRectangle(new Point2(-7.5f, -7.5f), new Size2(12.5f, 12.5f))).SetName(
+// "The intersection of two overlapping non-empty bounding rectangles is the expected bounding rectangle.");
+// yield return
+// new TestCaseData(new BoundingRectangle(new Point2(-30, -30), new Size2(15, 15)),
+// new BoundingRectangle(new Point2(20, 20), new Size2(10, 10)),
+// BoundingRectangle.Empty).SetName(
+// "The intersection of two non-overlapping non-empty bounding rectangles is the empty bounding rectangle.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(IntersectionTestCases))]
+// public void Intersection(BoundingRectangle boundingRectangle1, BoundingRectangle boundingRectangle2,
+// BoundingRectangle? expectedBoundingRectangle)
+// {
+// Assert.Equal(expectedBoundingRectangle, boundingRectangle1.Intersection(boundingRectangle2));
+// Assert.Equal(expectedBoundingRectangle, BoundingRectangle.Intersection(boundingRectangle1, boundingRectangle2));
+// }
+
+// public IEnumerable<TestCaseData> IntersectsTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new BoundingRectangle(), new BoundingRectangle(), true).SetName(
+// "Two empty bounding rectangles intersect.");
+// yield return
+// new TestCaseData(new BoundingRectangle(new Point2(-10, -10), new Size2(15, 15)),
+// new BoundingRectangle(new Point2(20, 20), new Size2(40, 40)), true).SetName(
+// "Two overlapping non-empty bounding rectangles intersect.");
+// yield return
+// new TestCaseData(new BoundingRectangle(new Point2(-40, -50), new Size2(15, 15)),
+// new BoundingRectangle(new Point2(20, 20), new Size2(15, 15)), false).SetName(
+// "Two non-overlapping non-empty bounding rectangles do not intersect.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(IntersectsTestCases))]
+// public void Intersects(BoundingRectangle boundingRectangle1, BoundingRectangle boundingRectangle2, bool expectedToIntersect)
+// {
+// Assert.Equal(expectedToIntersect, boundingRectangle1.Intersects(boundingRectangle2));
+// Assert.Equal(expectedToIntersect, BoundingRectangle.Intersects(boundingRectangle1, boundingRectangle2));
+// }
+
+// public IEnumerable<TestCaseData> ContainsPointTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new BoundingRectangle(), new Point2(), true).SetName(
+// "The empty bounding rectangle contains the zero point.");
+// yield return
+// new TestCaseData(new BoundingRectangle(new Point2(0, 0), new Size2(15, 15)), new Point2(-15, -15), true)
+// .SetName(
+// "A non-empty bounding rectangle contains a point inside it.");
+// yield return
+// new TestCaseData(new BoundingRectangle(new Point2(0, 0), new Size2(15, 15)), new Point2(-16, 15), false)
+// .SetName(
+// "A non-empty bounding rectangle does not contain a point outside it.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(ContainsPointTestCases))]
+// public void ContainsPoint(BoundingRectangle boundingRectangle, Point2 point, bool expectedToContainPoint)
+// {
+// Assert.Equal(expectedToContainPoint, boundingRectangle.Contains(point));
+// Assert.Equal(expectedToContainPoint, BoundingRectangle.Contains(boundingRectangle, point));
+// }
+
+// public IEnumerable<TestCaseData> ClosestPointTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new BoundingRectangle(), new Point2(), new Point2()).SetName(
+// "The closest point on the empty bounding rectangle to the zero point is the zero point.");
+// yield return
+// new TestCaseData(new BoundingRectangle(new Point2(0, 0), new Point2(50, 50)), new Point2(25, 25),
+// new Point2(25, 25)).SetName(
+// "The closest point on a non-empty bounding rectangle to a point which is inside the bounding rectangle is that point.")
+// ;
+// yield return
+// new TestCaseData(new BoundingRectangle(new Point2(0, 0), new Point2(50, 50)), new Point2(400, 0),
+// new Point2(50, 0)).SetName(
+// "The closest point on a non-empty bounding rectangle to a point which is outside the bounding rectangle is the expected point.")
+// ;
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(ClosestPointTestCases))]
+// public void ClosestPoint(BoundingRectangle boundingRectangle, Point2 point, Point2 expectedClosestPoint)
+// {
+// var actualClosestPoint = boundingRectangle.ClosestPointTo(point);
+// Assert.Equal(expectedClosestPoint, actualClosestPoint);
+// }
+
+// public IEnumerable<TestCaseData> EqualityTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new BoundingRectangle(), new BoundingRectangle(), true).SetName(
+// "Empty bounding rectangles are equal.")
+// ;
+// yield return
+// new TestCaseData(
+// new BoundingRectangle(new Point2(0, 0), new Size2(float.MaxValue, float.MinValue)),
+// new BoundingRectangle(new Point2(0, 0),
+// new Point2(float.MinValue, float.MaxValue)), false).SetName(
+// "Two different non-empty bounding rectangles are not equal.");
+// yield return
+// new TestCaseData(
+// new BoundingRectangle(new Point2(0, 0), new Size2(float.MinValue, float.MaxValue)),
+// new BoundingRectangle(new Point2(0, 0),
+// new Size2(float.MinValue, float.MaxValue)), true).SetName(
+// "Two identical non-empty bounding rectangles are equal.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(EqualityTestCases))]
+// public void Equality(BoundingRectangle boundingRectangle1, BoundingRectangle boundingRectangle2, bool expectedToBeEqual)
+// {
+// Assert.IsTrue(Equals(boundingRectangle1, boundingRectangle2) == expectedToBeEqual);
+// Assert.IsTrue(boundingRectangle1 == boundingRectangle2 == expectedToBeEqual);
+// Assert.IsFalse(boundingRectangle1 == boundingRectangle2 != expectedToBeEqual);
+// Assert.IsTrue(boundingRectangle1.Equals(boundingRectangle2) == expectedToBeEqual);
+
+// if (expectedToBeEqual)
+// Assert.Equal(boundingRectangle1.GetHashCode(), boundingRectangle2.GetHashCode());
+// }
+
+// public IEnumerable<TestCaseData> InequalityTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new BoundingRectangle(), null, false).SetName(
+// "A bounding rectangle is not equal to a null object.");
+// yield return
+// new TestCaseData(new BoundingRectangle(), new object(), false).SetName(
+// "A bounding rectangle is not equal to an instantiated object.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(InequalityTestCases))]
+// public void Inequality(BoundingRectangle boundingRectangle, object obj, bool expectedToBeEqual)
+// {
+// Assert.IsTrue(boundingRectangle.Equals(obj) == expectedToBeEqual);
+// }
+
+// public IEnumerable<TestCaseData> HashCodeTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new BoundingRectangle(), new BoundingRectangle(), true).SetName(
+// "Two empty bounding rectangles have the same hash code.");
+// yield return
+// new TestCaseData(new BoundingRectangle(new Point2(0, 0), new Size2(50, 50)),
+// new BoundingRectangle(new Point2(0, 0), new Size2(50, 50)), true).SetName(
+// "Two indentical non-empty bounding rectangles have the same hash code.");
+// yield return
+// new TestCaseData(new BoundingRectangle(new Point2(0, 0), new Size2(50, 50)),
+// new BoundingRectangle(new Point2(50, 50), new Size2(50, 50)), false).SetName(
+// "Two different non-empty bounding rectangles do not have the same hash code.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(HashCodeTestCases))]
+// public void HashCode(BoundingRectangle boundingRectangle1, BoundingRectangle boundingRectangle2, bool expectedThatHashCodesAreEqual)
+// {
+// var hashCode1 = boundingRectangle1.GetHashCode();
+// var hashCode2 = boundingRectangle2.GetHashCode();
+// if (expectedThatHashCodesAreEqual)
+// Assert.Equal(hashCode1, hashCode2);
+// else
+// Assert.AreNotEqual(hashCode1, hashCode2);
+// }
+
+// public IEnumerable<TestCaseData> ToRectangleTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new BoundingRectangle(), new Rectangle()).SetName(
+// "The empty bounding rectangle point converted to a rectangle is the empty rectangle.");
+// yield return
+// new TestCaseData(new BoundingRectangle(new Point2(25, 25), new Size2(25, 25)),
+// new Rectangle(0, 0, 50, 50)).SetName(
+// "A non-empty bounding rectangle converted to a rectangle is the expected rectangle.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(ToRectangleTestCases))]
+// public void ToRectangle(BoundingRectangle boundingRectangle, Rectangle expectedRectangle)
+// {
+// var actualRectangle = (Rectangle)boundingRectangle;
+// Assert.Equal(expectedRectangle, actualRectangle);
+// }
+
+// public IEnumerable<TestCaseData> FromRectangleTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Rectangle(), new BoundingRectangle()).SetName(
+// "The empty rectangle converted to a bounding rectangle is the empty bounding rectangle.");
+// yield return
+// new TestCaseData(new Rectangle(0, 0, 50, 50),
+// new BoundingRectangle(new Point2(25, 25), new Size2(25, 25))).SetName(
+// "A non-empty rectangle converted to a bounding rectangle is the expected bounding rectangle.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(FromRectangleTestCases))]
+// public void FromRectangle(Rectangle rectangle, BoundingRectangle expectedBoundingRectangle)
+// {
+// var actualBoundingRectangle = (BoundingRectangle)rectangle;
+// Assert.Equal(expectedBoundingRectangle, actualBoundingRectangle);
+// }
+
+// public IEnumerable<TestCaseData> StringCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new BoundingRectangle(),
+// string.Format(CultureInfo.CurrentCulture, "Centre: {0}, Radii: {1}", new Point2(),
+// new Vector2())).SetName(
+// "The empty bounding rectangle has the expected string representation using the current culture.");
+// yield return new TestCaseData(new BoundingRectangle(new Point2(5.1f, -5.123f), new Size2(5.4f, -5.4123f)),
+// string.Format(CultureInfo.CurrentCulture, "Centre: {0}, Radii: {1}", new Point2(5.1f, -5.123f),
+// new Vector2(5.4f, -5.4123f))).SetName(
+// "A non-empty bounding rectangle has the expected string representation using the current culture.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(StringCases))]
+// public void String(BoundingRectangle boundingRectangle, string expectedString)
+// {
+// var actualString = boundingRectangle.ToString();
+// Assert.Equal(expectedString, actualString);
+// }
+// }
+//}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/CircleFTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/CircleFTests.cs
new file mode 100644
index 0000000..f88155f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/CircleFTests.cs
@@ -0,0 +1,420 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using Microsoft.Xna.Framework;
+using Xunit;
+
+namespace MonoGame.Extended.Tests.Primitives
+{
+
+public class CircleFTests
+{
+
+ [Fact]
+ public void CircCircIntersectionDiagonalCircleTest()
+ {
+ var circle = new CircleF(new Point2(16.0f, 16.0f), 16.0f);
+ var point = new Point2(0, 0);
+
+ Assert.False(circle.Contains(point));
+ }
+// public IEnumerable<TestCaseData> ConstructorTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Point2(), 0.0f).SetName(
+// "The empty circle has the expected position and radius.");
+// yield return
+// new TestCaseData(new Point2(5, 5), 15f).SetName(
+// "A non-empty circle has the expected position and radius.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(ConstructorTestCases))]
+// public void Constructor(Point2 centre, float radius)
+// {
+// var circle = new CircleF(centre, radius);
+// Assert.Equal(centre, circle.Center);
+// Assert.Equal(radius, circle.Radius);
+// }
+
+// public IEnumerable<TestCaseData> CreateFromMinimumMaximumTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Point2(), new Point2(), new CircleF()).SetName(
+// "The bounding circle created from the zero minimum point and zero maximum point is the empty bounding circle.")
+// ;
+// yield return
+// new TestCaseData(new Point2(5, 5), new Point2(15, 15),
+// new CircleF(new Point2(10, 10), 5f)).SetName(
+// "The bounding circle created from the non-zero minimum point and the non-zero maximum point is the expected bounding circle.")
+// ;
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(CreateFromMinimumMaximumTestCases))]
+// public void CreateFromMinimumMaximum(Point2 minimum, Point2 maximum, CircleF expectedBoundingCircle)
+// {
+// var actualBoundingCircle = CircleF.CreateFrom(minimum, maximum);
+// Assert.Equal(expectedBoundingCircle, actualBoundingCircle);
+// }
+
+// public IEnumerable<TestCaseData> CreateFromPointsTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(null, new CircleF()).SetName(
+// "The bounding circle created from null points is the empty bounding circle.");
+// yield return
+// new TestCaseData(new Point2[0], new CircleF()).SetName(
+// "The bounding circle created from the empty set of points is the empty bounding circle.");
+// yield return
+// new TestCaseData(
+// new[]
+// {
+// new Point2(5, 5), new Point2(10, 10), new Point2(15, 15), new Point2(-5, -5),
+// new Point2(-15, -15)
+// }, new CircleF(new Point2(0, 0), 15)).SetName(
+// "The bounding circle created from a non-empty set of points is the expected bounding circle.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(CreateFromPointsTestCases))]
+// public void CreateFromPoints(Point2[] points, CircleF expectedCircle)
+// {
+// var actualCircle = CircleF.CreateFrom(points);
+// Assert.Equal(expectedCircle, actualCircle);
+// }
+
+// public IEnumerable<TestCaseData> IntersectsCircleTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new CircleF(), new CircleF(), true).SetName(
+// "Two empty circles intersect.");
+// yield return
+// new TestCaseData(new CircleF(new Point2(-10, -10), 15),
+// new CircleF(new Point2(20, 20), 40), true).SetName(
+// "Two overlapping non-empty circles intersect.");
+// yield return
+// new TestCaseData(new CircleF(new Point2(-40, -50), 15),
+// new CircleF(new Point2(20, 20), 15), false).SetName(
+// "Two non-overlapping non-empty circles do not intersect.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(IntersectsCircleTestCases))]
+// public void Intersects(CircleF circle, CircleF circle2, bool expectedToIntersect)
+// {
+// Assert.Equal(expectedToIntersect, circle.Intersects(circle2));
+// Assert.Equal(expectedToIntersect, CircleF.Intersects(circle, circle2));
+// }
+
+// public IEnumerable<TestCaseData> IntersectsRectangleTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new CircleF(), new RectangleF(), true).SetName(
+// "The empty circle and the empty rectangle intersect.");
+// yield return
+// new TestCaseData(new CircleF(new Point2(0, 0), 15),
+// new RectangleF(new Point2(0, 0), new Size2(40, 40)), true).SetName(
+// "The non-empty circle and a non-empty overlapping rectangle intersect.");
+// yield return
+// new TestCaseData(new CircleF(new Point2(-40, -50), 15),
+// new RectangleF(new Point2(20, 20), new Size2(15, 15)), false).SetName(
+// "The non-empty circle and a non-empty non-overlapping rectangle do not intersect.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(IntersectsRectangleTestCases))]
+// public void Intersects(CircleF circle, RectangleF rectangle, bool expectedToIntersect)
+// {
+// Assert.Equal(expectedToIntersect, circle.Intersects((BoundingRectangle)rectangle));
+// Assert.Equal(expectedToIntersect, CircleF.Intersects(circle, (BoundingRectangle)rectangle));
+// }
+
+// public IEnumerable<TestCaseData> ContainsPointTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new CircleF(), new Point2(), true).SetName(
+// "The empty circle contains the zero point.");
+// yield return
+// new TestCaseData(new CircleF(new Point2(0, 0), 15), new Point2(-15, -15), true)
+// .SetName(
+// "A non-empty circle contains a point inside it.");
+// yield return
+// new TestCaseData(new CircleF(new Point2(0, 0), 15), new Point2(-16, 15), false)
+// .SetName(
+// "A non-empty circle does not contain a point outside it.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(ContainsPointTestCases))]
+// public void ContainsPoint(CircleF circle, Point2 point, bool expectedToContainPoint)
+// {
+// Assert.Equal(expectedToContainPoint, circle.Contains(point));
+// Assert.Equal(expectedToContainPoint, CircleF.Contains(circle, point));
+// }
+
+// public IEnumerable<TestCaseData> ClosestPointTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new CircleF(), new Point2(), new Point2()).SetName(
+// "The closest point on the empty circle to the zero point is the zero point.");
+// yield return
+// new TestCaseData(new CircleF(new Point2(0, 0), 50), new Point2(25, 25),
+// new Point2(25, 25)).SetName(
+// "The closest point on a non-empty circle to a point which is inside the circle is that point.")
+// ;
+// yield return
+// new TestCaseData(new CircleF(new Point2(0, 0), 50), new Point2(400, 0),
+// new Point2(50, 0)).SetName(
+// "The closest point on a non-empty circle to a point which is outside the circle is the expected point.")
+// ;
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(ClosestPointTestCases))]
+// public void ClosestPoint(CircleF circle, Point2 point, Point2 expectedClosestPoint)
+// {
+// var actualClosestPoint = circle.ClosestPointTo(point);
+// Assert.Equal(expectedClosestPoint, actualClosestPoint);
+// }
+
+// public IEnumerable<TestCaseData> BoundaryPointTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new CircleF(), 0.0f, new Point2()).SetName(
+// "The boundary point on the empty circle at an angle is the zero point.");
+// yield return
+// new TestCaseData(new CircleF(new Point2(0, 0), 50), MathHelper.PiOver2,
+// new Point2(0, 50)).SetName(
+// "The boundary point on a non-empty circle at an angle is the expected point.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(BoundaryPointTestCases))]
+// public void BoundaryPointAt(CircleF circle, float angle, Point2 expectedPoint)
+// {
+// var actualPoint = circle.BoundaryPointAt(angle);
+// AssertExtensions.AreApproximatelyEqual(expectedPoint, actualPoint);
+// }
+
+// public IEnumerable<TestCaseData> EqualityTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new CircleF(), new CircleF(), true).SetName(
+// "Empty circles are equal.")
+// ;
+// yield return
+// new TestCaseData(
+// new CircleF(new Point2(0, 0), float.MaxValue),
+// new CircleF(new Point2(0, 0), float.MinValue), false).SetName(
+// "Two different non-empty circles are not equal.");
+// yield return
+// new TestCaseData(
+// new CircleF(new Point2(0, 0), float.MinValue),
+// new CircleF(new Point2(0, 0), float.MinValue), true).SetName(
+// "Two identical non-empty circles are equal.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(EqualityTestCases))]
+// public void Equality(CircleF circle1, CircleF circle2, bool expectedToBeEqual)
+// {
+// Assert.IsTrue(Equals(circle1, circle2) == expectedToBeEqual);
+// Assert.IsTrue(circle1 == circle2 == expectedToBeEqual);
+// Assert.IsFalse(circle1 == circle2 != expectedToBeEqual);
+// Assert.IsTrue(circle1.Equals(circle2) == expectedToBeEqual);
+
+// if (expectedToBeEqual)
+// Assert.Equal(circle1.GetHashCode(), circle2.GetHashCode());
+// }
+
+// public IEnumerable<TestCaseData> InequalityTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new CircleF(), null, false).SetName(
+// "A circle is not equal to a null object.");
+// yield return
+// new TestCaseData(new CircleF(), new object(), false).SetName(
+// "A circle is not equal to an instantiated object.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(InequalityTestCases))]
+// public void Inequality(CircleF circle, object obj, bool expectedToBeEqual)
+// {
+// Assert.IsTrue(circle.Equals(obj) == expectedToBeEqual);
+// }
+
+// public IEnumerable<TestCaseData> HashCodeTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new CircleF(), new CircleF(), true).SetName(
+// "Two empty circles have the same hash code.");
+// yield return
+// new TestCaseData(new CircleF(new Point2(0, 0), 50),
+// new CircleF(new Point2(0, 0), 50), true).SetName(
+// "Two indentical non-empty circles have the same hash code.");
+// yield return
+// new TestCaseData(new CircleF(new Point2(0, 0), 50),
+// new CircleF(new Point2(50, 50), 50), false).SetName(
+// "Two different non-empty circles do not have the same hash code.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(HashCodeTestCases))]
+// public void HashCode(CircleF circle1, CircleF circle2, bool expectedThatHashCodesAreEqual)
+// {
+// var hashCode1 = circle1.GetHashCode();
+// var hashCode2 = circle2.GetHashCode();
+// if (expectedThatHashCodesAreEqual)
+// Assert.Equal(hashCode1, hashCode2);
+// else
+// Assert.AreNotEqual(hashCode1, hashCode2);
+// }
+
+// public IEnumerable<TestCaseData> ToRectangleTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new CircleF(), new Rectangle()).SetName(
+// "The empty circle converted to a rectangle is the empty integer rectangle.");
+// yield return
+// new TestCaseData(new CircleF(new Point2(25, 25), 25),
+// new Rectangle(0, 0, 50, 50)).SetName(
+// "A non-empty circle converted to a rectangle is the expected integer rectangle.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(ToRectangleTestCases))]
+// public void ToRectangle(CircleF circle, Rectangle expectedRectangle)
+// {
+// var actualRectangle = (Rectangle)circle;
+// Assert.Equal(expectedRectangle, actualRectangle);
+// }
+
+// public IEnumerable<TestCaseData> ToRectangleFTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new CircleF(), new RectangleF()).SetName(
+// "The empty circle converted to a rectangle is the empty float rectangle.");
+// yield return
+// new TestCaseData(new CircleF(new Point2(25, 25), 25),
+// new RectangleF(0, 0, 50, 50)).SetName(
+// "A non-empty circle converted to a rectangle is the expected float rectangle.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(ToRectangleFTestCases))]
+// public void ToRectangleF(CircleF circle, RectangleF expectedRectangle)
+// {
+// var actualRectangle = (RectangleF)circle;
+// Assert.Equal(expectedRectangle, actualRectangle);
+// }
+
+// public IEnumerable<TestCaseData> FromRectangleTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Rectangle(), new CircleF()).SetName(
+// "The empty rectangle converted to a circle is the empty circle.");
+// yield return
+// new TestCaseData(new Rectangle(0, 0, 50, 50),
+// new CircleF(new Point2(25, 25), 25)).SetName(
+// "A non-empty rectangle converted to a circle is the expected circle.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(FromRectangleTestCases))]
+// public void FromRectangle(Rectangle rectangle, CircleF expectedCircle)
+// {
+// var actualCircle = (CircleF)rectangle;
+// Assert.Equal(expectedCircle, actualCircle);
+// }
+
+// public IEnumerable<TestCaseData> FromRectangleFTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new RectangleF(), new CircleF()).SetName(
+// "The empty rectangle converted to a circle is the empty circle.");
+// yield return
+// new TestCaseData(new RectangleF(0, 0, 50, 50),
+// new CircleF(new Point2(25, 25), 25)).SetName(
+// "A non-empty rectangle converted to a circle is the expected circle.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(FromRectangleFTestCases))]
+// public void FromRectangleF(RectangleF rectangle, CircleF expectedCircle)
+// {
+// var actualCircle = (CircleF)rectangle;
+// Assert.Equal(expectedCircle, actualCircle);
+// }
+
+// public IEnumerable<TestCaseData> StringCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new CircleF(),
+// string.Format(CultureInfo.CurrentCulture, "Centre: {0}, Radius: {1}", new Point2(),
+// 0)).SetName(
+// "The empty circle has the expected string representation using the current culture.");
+// yield return new TestCaseData(new CircleF(new Point2(5.1f, -5.123f), 5.4f),
+// string.Format(CultureInfo.CurrentCulture, "Centre: {0}, Radius: {1}", new Point2(5.1f, -5.123f),
+// 5.4f)).SetName(
+// "A non-empty circle has the expected string representation using the current culture.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(StringCases))]
+// public void String(CircleF circle, string expectedString)
+// {
+// var actualString = circle.ToString();
+// Assert.Equal(expectedString, actualString);
+// }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/EllipseFTest.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/EllipseFTest.cs
new file mode 100644
index 0000000..4a4ecdf
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/EllipseFTest.cs
@@ -0,0 +1,38 @@
+using Microsoft.Xna.Framework;
+using Xunit;
+
+namespace MonoGame.Extended.Tests.Primitives
+{
+
+ public class EllipseFTest
+ {
+ [Theory]
+ [InlineData(-1, -1, false)]
+ [InlineData(110, 300, true)]
+ [InlineData(200, 300, true)]
+ [InlineData(290, 300, true)]
+ [InlineData(400, 400, false)]
+ public void ContainsPoint_Circle(int x, int y, bool expected)
+ {
+ var ellipse = new EllipseF(new Vector2(200.0f, 300.0f), 100.0f, 100.0f);
+
+ Assert.Equal(expected, ellipse.Contains(x, y));
+ }
+
+ [Theory]
+ [InlineData(299, 400, false)]
+ [InlineData(501, 400, false)]
+ [InlineData(400, 199, false)]
+ [InlineData(400, 601, false)]
+ [InlineData(301, 400, true)]
+ [InlineData(499, 400, true)]
+ [InlineData(400, 201, true)]
+ [InlineData(400, 599, true)]
+ public void ContainsPoint_NonCircle(int x, int y, bool expected)
+ {
+ var ellipse = new EllipseF(new Vector2(400.0f, 400.0f), 100.0f, 200.0f);
+
+ Assert.Equal(expected, ellipse.Contains(x, y));
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/OrientedRectangleTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/OrientedRectangleTests.cs
new file mode 100644
index 0000000..e3b40c8
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/OrientedRectangleTests.cs
@@ -0,0 +1,234 @@
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using Xunit;
+using Vector2 = Microsoft.Xna.Framework.Vector2;
+
+namespace MonoGame.Extended.Tests.Primitives;
+
+public class OrientedRectangleTests
+{
+ [Fact]
+ public void Initializes_oriented_rectangle()
+ {
+ var rectangle = new OrientedRectangle(new Point2(1, 2), new Size2(3, 4), new Matrix2(5, 6, 7, 8, 9, 10));
+
+ Assert.Equal(new Point2(1, 2), rectangle.Center);
+ Assert.Equal(new Vector2(3, 4), rectangle.Radii);
+ Assert.Equal(new Matrix2(5, 6, 7, 8, 9, 10), rectangle.Orientation);
+ CollectionAssert.Equal(
+ new List<Vector2>
+ {
+ new(-3, -2),
+ new(-33, -38),
+ new(23, 26),
+ new(53, 62)
+ },
+ rectangle.Points);
+ }
+
+ public static readonly IEnumerable<object[]> _equalsComparisons = new[]
+ {
+ new object[]
+ {
+ "empty compared with empty is true",
+ new OrientedRectangle(Point2.Zero, Size2.Empty, Matrix2.Identity),
+ new OrientedRectangle(Point2.Zero, Size2.Empty, Matrix2.Identity)
+ },
+ new object[]
+ {
+ "initialized compared with initialized true",
+ new OrientedRectangle(new Point2(1, 2), new Size2(3, 4), new Matrix2(5, 6, 7, 8, 9, 10)),
+ new OrientedRectangle(new Point2(1, 2), new Size2(3, 4), new Matrix2(5, 6, 7, 8, 9, 10))
+ }
+ };
+
+ [Theory]
+ [MemberData(nameof(_equalsComparisons))]
+#pragma warning disable xUnit1026
+ public void Equals_comparison(string name, OrientedRectangle first, OrientedRectangle second)
+#pragma warning restore xUnit1026
+ {
+ Assert.True(first == second);
+ Assert.False(first != second);
+ }
+
+ public class Transform
+ {
+ [Fact]
+ public void Center_point_is_not_translated()
+ {
+ var rectangle = new OrientedRectangle(new Point2(1, 2), new Size2(), Matrix2.Identity);
+ var transform = Matrix2.Identity;
+
+ var result = OrientedRectangle.Transform(rectangle, ref transform);
+
+ Assert.Equal(new Point2(1, 2), result.Center);
+ }
+
+ [Fact]
+ public void Center_point_is_translated()
+ {
+ var rectangle = new OrientedRectangle(new Point2(0, 0), new Size2(), new Matrix2());
+ var transform = Matrix2.CreateTranslation(1, 2);
+
+ var result = OrientedRectangle.Transform(rectangle, ref transform);
+
+ Assert.Equal(new Point2(1, 2), result.Center);
+ }
+
+ [Fact]
+ public void Radii_is_not_changed_by_identity_transform()
+ {
+ var rectangle = new OrientedRectangle(new Point2(), new Size2(10, 20), new Matrix2());
+ var transform = Matrix2.Identity;
+
+ var result = OrientedRectangle.Transform(rectangle, ref transform);
+
+ Assert.Equal(new Vector2(10, 20), result.Radii);
+ }
+
+ [Fact]
+ public void Radii_is_not_changed_by_translation()
+ {
+ var rectangle = new OrientedRectangle(new Point2(1, 2), new Size2(10, 20), new Matrix2());
+ var transform = Matrix2.CreateTranslation(1, 2);
+
+ var result = OrientedRectangle.Transform(rectangle, ref transform);
+
+ Assert.Equal(new Vector2(10, 20), result.Radii);
+ }
+
+ [Fact]
+ public void Orientation_is_rotated_45_degrees_left()
+ {
+ var rectangle = new OrientedRectangle(new Point2(), new Size2(), Matrix2.Identity);
+ var transform = Matrix2.CreateRotationZ(MathHelper.PiOver4);
+
+ var result = OrientedRectangle.Transform(rectangle, ref transform);
+
+ Assert.Equal(new Point2(), result.Center);
+ Assert.Equal(new Vector2(), result.Radii);
+ Assert.Equal(Matrix2.CreateRotationZ(MathHelper.PiOver4), result.Orientation);
+ }
+
+ [Fact]
+ public void Orientation_is_rotated_to_45_degrees_from_180()
+ {
+ var rectangle = new OrientedRectangle(new Point2(), new Size2(), Matrix2.CreateRotationZ(MathHelper.Pi));
+ var transform = Matrix2.CreateRotationZ(-3 * MathHelper.PiOver4);
+ var expectedOrientation = Matrix2.CreateRotationZ(MathHelper.PiOver4);
+
+ var result = OrientedRectangle.Transform(rectangle, ref transform);
+
+ Assert.Equal(new Point2(), result.Center);
+ Assert.Equal(new Vector2(), result.Radii);
+ Assert.Equal(expectedOrientation.M11, result.Orientation.M11, 6);
+ Assert.Equal(expectedOrientation.M12, result.Orientation.M12, 6);
+ Assert.Equal(expectedOrientation.M21, result.Orientation.M21, 6);
+ Assert.Equal(expectedOrientation.M22, result.Orientation.M22, 6);
+ Assert.Equal(expectedOrientation.M31, result.Orientation.M31, 6);
+ Assert.Equal(expectedOrientation.M32, result.Orientation.M32, 6);
+ }
+
+ [Fact]
+ public void Points_are_same_as_center()
+ {
+ var rectangle = new OrientedRectangle(new Point2(1, 2), new Size2(), Matrix2.Identity);
+ var transform = Matrix2.Identity;
+
+ var result = OrientedRectangle.Transform(rectangle, ref transform);
+
+ CollectionAssert.Equal(
+ new List<Vector2>
+ {
+ new(1, 2),
+ new(1, 2),
+ new(1, 2),
+ new(1, 2)
+ },
+ result.Points);
+ }
+
+ [Fact]
+ public void Points_are_translated()
+ {
+ var rectangle = new OrientedRectangle(new Point2(0, 0), new Size2(2, 4), Matrix2.Identity);
+ var transform = Matrix2.CreateTranslation(10, 20);
+
+ var result = OrientedRectangle.Transform(rectangle, ref transform);
+
+ CollectionAssert.Equal(
+ new List<Vector2>
+ {
+ new(8, 16),
+ new(8, 24),
+ new(12, 24),
+ new(12, 16)
+ },
+ result.Points);
+ }
+
+ [Fact]
+ public void Applies_rotation_and_translation()
+ {
+ /* Rectangle with center point p, aligned in coordinate system with origin 0.
+ *
+ * :
+ * :
+ * +-+
+ * | |
+ * |p|
+ * | |
+ * ...............0-+............
+ * :
+ * :
+ * :
+ *
+ * Rotate around center point p, 90 degrees around origin 0.
+ *
+ * :
+ * :
+ * +---+
+ * | p |
+ * ...........+---0..............
+ * :
+ * :
+ * :
+ *
+ * Then translate rectangle by x=10 and y=20.
+ * :
+ * : +---+
+ * : | p |
+ * y=21 - - - - - - - -> +---+
+ * .
+ * :
+ * ...............0..............
+ * :
+ * :
+ * :
+ */
+ var rectangle = new OrientedRectangle(new Point2(1, 2), new Size2(2, 4), Matrix2.Identity);
+ var transform =
+ Matrix2.CreateRotationZ(MathHelper.PiOver2)
+ *
+ Matrix2.CreateTranslation(10, 20);
+
+ var result = OrientedRectangle.Transform(rectangle, ref transform);
+
+ Assert.Equal(8, result.Center.X, 6);
+ Assert.Equal(21, result.Center.Y, 6);
+ Assert.Equal(2, result.Radii.X, 6);
+ Assert.Equal(4, result.Radii.Y, 6);
+ Assert.Equal(Matrix2.CreateRotationZ(MathHelper.PiOver2), result.Orientation);
+ CollectionAssert.Equal(
+ new List<Vector2>
+ {
+ new(4, 23),
+ new(4, 19),
+ new(12, 19),
+ new(12, 23)
+ },
+ result.Points);
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/Point2Tests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/Point2Tests.cs
new file mode 100644
index 0000000..ce5f89b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/Point2Tests.cs
@@ -0,0 +1,356 @@
+//using System.Collections.Generic;
+//using System.Globalization;
+//using Microsoft.Xna.Framework;
+//using Xunit;
+
+//namespace MonoGame.Extended.Tests.Primitives
+//{
+//
+// public class Point2Tests
+// {
+// public IEnumerable<TestCaseData> ConstructorTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(0, 0).SetName(
+// "The zero point has the expected coordinates.");
+// yield return
+// new TestCaseData(float.MinValue, float.MaxValue).SetName
+// (
+// "A non-zero point has the expected coordinates.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(ConstructorTestCases))]
+// public void Constructor(float x, float y)
+// {
+// var point = new Point2(x, y);
+// Assert.Equal(x, point.X);
+// Assert.Equal(y, point.Y);
+// }
+
+// public IEnumerable<TestCaseData> CoordinatesTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Point2(), 0, 0).SetName(
+// "The zero point has the expected coordinates.");
+// yield return
+// new TestCaseData(new Point2(float.MinValue, float.MaxValue), float.MinValue, float.MaxValue).SetName
+// (
+// "A non-zero point has the expected coordinates.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(CoordinatesTestCases))]
+// public void Coordinates(Point2 point, float expectedX, float expecetedY)
+// {
+// Assert.Equal(expectedX, point.X);
+// Assert.Equal(expecetedY, point.Y);
+
+// point.X = 10;
+// Assert.Equal(10, point.X);
+
+// point.Y = -10.123f;
+// Assert.Equal(-10.123f, point.Y);
+// }
+
+// public IEnumerable<TestCaseData> VectorAdditionTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Point2(), new Vector2(), new Point2()).SetName(
+// "The addition of the zero point and the zero vector is the zero point.");
+// yield return
+// new TestCaseData(new Point2(5, 5), new Vector2(15, 15), new Point2(20, 20)).SetName(
+// "The addition of a non-zero point and a non-zero vector is the expected point.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(VectorAdditionTestCases))]
+// public void VectorAddition(Point2 point, Vector2 vector, Point2 expectedPoint)
+// {
+// Assert.Equal(expectedPoint, point + vector);
+// Assert.Equal(expectedPoint, Point2.Add(point, vector));
+// }
+
+// public IEnumerable<TestCaseData> VectorSubtractionTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Point2(), new Vector2(), new Point2()).SetName(
+// "The vector subtraction of two zero points is the zero vector.");
+// yield return
+// new TestCaseData(new Point2(5, 5), new Vector2(15, 15), new Point2(-10, -10)).SetName(
+// "The vector subtraction of two non-zero points is the expected vector.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(VectorSubtractionTestCases))]
+// public void VectorSubtraction(Point2 point, Vector2 vector, Point2 expectedPoint)
+// {
+// Assert.Equal(expectedPoint, point - vector);
+// Assert.Equal(expectedPoint, Point2.Subtract(point, vector));
+// }
+
+
+// public IEnumerable<TestCaseData> DisplacementTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Point2(), new Point2(), new Vector2()).SetName(
+// "The displacement between two zero points is the zero vector.");
+// yield return
+// new TestCaseData(new Point2(5, 5), new Point2(15, 15), new Vector2(10, 10)).SetName(
+// "The displacement between two non-zero points is the expected vector.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(DisplacementTestCases))]
+// public void Displacement(Point2 point1, Point2 point2, Vector2 expectedVector)
+// {
+// Assert.Equal(expectedVector, point2 - point1);
+// Assert.Equal(expectedVector, Point2.Displacement(point2, point1));
+// }
+
+// public IEnumerable<TestCaseData> SizeAdditionTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Point2(), new Size2(), new Point2()).SetName(
+// "The size addition of the zero point with the empty size is the zero point.");
+// yield return
+// new TestCaseData(new Point2(5, 5), new Size2(15, 15), new Point2(20, 20)).SetName(
+// "The size addition of a non-zero point with a non-empty size is the expected point.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(SizeAdditionTestCases))]
+// public void SizeAdditon(Point2 point, Size2 size, Point2 expectedPoint)
+// {
+// Assert.Equal(expectedPoint, point + size);
+// Assert.Equal(expectedPoint, Point2.Add(point, size));
+// }
+
+// public IEnumerable<TestCaseData> SizeSubtractionTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Point2(), new Size2(), new Point2()).SetName(
+// "The size substraction of the zero point with the empty size is the zero point.");
+// yield return
+// new TestCaseData(new Point2(5, 5), new Size2(15, 15), new Point2(-10, -10)).SetName(
+// "The size subscration of a non-zero point with a non-empty size is the expected point.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(SizeSubtractionTestCases))]
+// public void SizeSubtraction(Point2 point, Size2 size, Point2 expectedPoint)
+// {
+// Assert.Equal(expectedPoint, point - size);
+// Assert.Equal(expectedPoint, Point2.Subtract(point, size));
+// }
+
+// public IEnumerable<TestCaseData> MinimumTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Point2(), new Point2(), new Point2()).SetName(
+// "The minimum coordinates of two zero points is the coordinates of the zero point.");
+// yield return
+// new TestCaseData(new Point2(float.MaxValue, float.MinValue), new Point2(int.MaxValue, int.MinValue),
+// new Point2(int.MaxValue, float.MinValue)).SetName(
+// "The minimum coordaintes of two non-zero points is the expected coordinates.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(MinimumTestCases))]
+// public void Minimum(Point2 point1, Point2 point2, Point2 expectedPoint)
+// {
+// var actualPoint = Point2.Minimum(point1, point2);
+// Assert.Equal(expectedPoint, actualPoint);
+// }
+
+// public IEnumerable<TestCaseData> MaximumTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Point2(), new Point2(), new Point2()).SetName(
+// "The maximum coordinates of two zero points is the coordinates of the zero point.");
+// yield return
+// new TestCaseData(new Point2(float.MaxValue, float.MinValue), new Point2(int.MaxValue, int.MinValue),
+// new Point2(float.MaxValue, int.MinValue)).SetName(
+// "The maximum coordaintes of two non-zero points is the expected coordinates.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(MaximumTestCases))]
+// public void Maximum(Point2 point1, Point2 point2, Point2 expectedPoint)
+// {
+// var actualPoint = Point2.Maximum(point1, point2);
+// Assert.Equal(expectedPoint, actualPoint);
+// }
+
+// public IEnumerable<TestCaseData> EqualityTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Point2(), new Point2(), true).SetName("Two zero points are equal.");
+// yield return
+// new TestCaseData(new Point2(float.MinValue, float.MaxValue),
+// new Point2(float.MaxValue, float.MinValue), false).SetName(
+// "Two different non-zero points are not equal.");
+// yield return
+// new TestCaseData(
+// new Point2(float.MinValue, float.MaxValue), new Point2(float.MinValue, float.MaxValue), true)
+// .SetName(
+// "Two identical non-zero points are equal.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(EqualityTestCases))]
+// public void Equality(Point2 point1, Point2 point2, bool expectedToBeEqual)
+// {
+// Assert.IsTrue(Equals(point1, point2) == expectedToBeEqual);
+// Assert.IsTrue(point1 == point2 == expectedToBeEqual);
+// Assert.IsFalse(point1 == point2 != expectedToBeEqual);
+// Assert.IsTrue(point1.Equals(point2) == expectedToBeEqual);
+
+// if (expectedToBeEqual)
+// Assert.Equal(point1.GetHashCode(), point2.GetHashCode());
+// }
+
+// public IEnumerable<TestCaseData> InequalityTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Point2(), null, false).SetName("A point is not equal to a null object.");
+// yield return
+// new TestCaseData(new Point2(), new object(), false).SetName(
+// "A point is not equal to an instantiated object.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(InequalityTestCases))]
+// public void Inequality(Point2 point, object obj, bool expectedToBeEqual)
+// {
+// Assert.IsTrue(point.Equals(obj) == expectedToBeEqual);
+// }
+
+// public IEnumerable<TestCaseData> HashCodeTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Point2(), new Point2(), true).SetName(
+// "Two zero points have the same hash code.");
+// yield return
+// new TestCaseData(new Point2(50, 50), new Point2(50, 50), true).SetName(
+// "Two indentical non-zero points have the same hash code.");
+// yield return
+// new TestCaseData(new Point2(0, 0), new Point2(50, 50), false).SetName(
+// "Two different non-zero points do not have the same hash code.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(HashCodeTestCases))]
+// public void HashCode(Point2 point1, Point2 point2, bool expectedThatHashCodesAreEqual)
+// {
+// var hashCode1 = point1.GetHashCode();
+// var hashCode2 = point2.GetHashCode();
+// if (expectedThatHashCodesAreEqual)
+// Assert.Equal(hashCode1, hashCode2);
+// else
+// Assert.AreNotEqual(hashCode1, hashCode2);
+// }
+
+// public IEnumerable<TestCaseData> ToVectorTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Point2(), new Vector2()).SetName(
+// "The zero point converted to a vector is the zero vector.");
+// yield return
+// new TestCaseData(new Point2(float.MinValue, float.MaxValue),
+// new Vector2(float.MinValue, float.MaxValue)).SetName(
+// "A non-zero point converted to a vector is the expected vector.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(ToVectorTestCases))]
+// public void ToVector(Point2 point, Vector2 expectedVector)
+// {
+// var actualVector = (Vector2)point;
+// Assert.Equal(expectedVector, actualVector);
+// }
+
+// public IEnumerable<TestCaseData> FromVectorTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Vector2(), new Point2()).SetName(
+// "The zero vector converted to a point is the zero point.");
+// yield return
+// new TestCaseData(new Vector2(float.MinValue, float.MaxValue),
+// new Point2(float.MinValue, float.MaxValue)).SetName(
+// "A non-zero vector converted to a point is the expected point.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(FromVectorTestCases))]
+// public void FromVector(Vector2 vector, Point2 expectedPoint)
+// {
+// var actualPoint = (Point2)vector;
+// Assert.Equal(expectedPoint, actualPoint);
+// }
+
+// public IEnumerable<TestCaseData> StringCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Point2(),
+// string.Format(CultureInfo.CurrentCulture, "({0}, {1})", 0, 0)).SetName(
+// "The zero point has the expected string representation using the current culture.");
+// yield return new TestCaseData(new Point2(5.1f, -5.123f),
+// string.Format(CultureInfo.CurrentCulture, "({0}, {1})", 5.1f, -5.123f)).SetName(
+// "A non-zero point has the expected string representation using the current culture.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(StringCases))]
+// public void String(Point2 point, string expectedString)
+// {
+// var actualString = point.ToString();
+// Assert.Equal(expectedString, actualString);
+// }
+// }
+//}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/Ray2DTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/Ray2DTests.cs
new file mode 100644
index 0000000..04e8b00
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/Ray2DTests.cs
@@ -0,0 +1,217 @@
+//using System.Collections.Generic;
+//using System.Globalization;
+//using Microsoft.Xna.Framework;
+//using Xunit;
+
+//namespace MonoGame.Extended.Tests.Primitives
+//{
+//
+// public class Ray2Tests
+// {
+// public IEnumerable<TestCaseData> ConstructorTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Point2(), new Vector2()).SetName(
+// "The degenerate ray has the expected position and direction.");
+// yield return
+// new TestCaseData(new Point2(5, 5), new Vector2(15, 15)).SetName(
+// "A non-degenerate ray has the expected position and direction.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(ConstructorTestCases))]
+// public void Constructor(Point2 position, Vector2 direction)
+// {
+// var ray = new Ray2(position, direction);
+// Assert.Equal(position, ray.Position);
+// Assert.Equal(direction, ray.Direction);
+// }
+
+// public IEnumerable<TestCaseData> PositionDirectionTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Ray2(), new Point2(), new Vector2()).SetName(
+// "The degenerate ray has the expected position and direction.");
+// yield return
+// new TestCaseData(new Ray2(new Point2(5, 5), new Vector2(15, 15)), new Point2(5, 5),
+// new Vector2(15, 15)).SetName
+// (
+// "A non-degenerate ray has the expected position and direction.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(PositionDirectionTestCases))]
+// public void PositionDirection(Ray2 ray, Point2 expectedPosition, Vector2 expecetedDirection)
+// {
+// Assert.Equal(expectedPosition, ray.Position);
+// Assert.Equal(expecetedDirection, ray.Direction);
+
+// ray.Position.X = 10;
+// ray.Position.Y = 10;
+// Assert.Equal(new Point2(10, 10), ray.Position);
+
+// ray.Direction.X = -10.123f;
+// ray.Direction.Y = 10.123f;
+// Assert.Equal(new Vector2(-10.123f, 10.123f), ray.Direction);
+// }
+
+// public IEnumerable<TestCaseData> IntersectsBoundingRectangleTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Ray2(), new BoundingRectangle(), true, Point2.Zero, Point2.Zero).SetName(
+// "The degenerate ray intersects the empty bounding box.");
+// yield return
+// new TestCaseData(new Ray2(new Point2(-75, -75), new Vector2(75, -75)),
+// new BoundingRectangle(new Point2(), new Size2(50, 50)), false, Point2.NaN, Point2.NaN).SetName(
+// "A non-degenerate ray that does not cross a non-empty bounding box does not intersect the bounding box.");
+// yield return
+// new TestCaseData(new Ray2(new Point2(0, 0), new Vector2(25, 0)), new BoundingRectangle(new Point2(), new Size2(50, 50)),
+// true, new Point2(0, 0), new Point2(50, 0)).SetName(
+// "A non-degenerate ray starting from inside a non-empty bounding box intersects the bounding box.");
+// yield return
+// new TestCaseData(new Ray2(new Point2(-100, 0), new Vector2(100, 0)),
+// new BoundingRectangle(new Point2(), new Size2(50, 50)),
+// true, new Point2(-50, 0), new Point2(50, 0)).SetName(
+// "A non-degenerate ray crossing a non-empty bounding box intersects the bounding box.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(IntersectsBoundingRectangleTestCases))]
+// public void IntersectsBoundingRectangle(Ray2 ray, BoundingRectangle boundingRectangle, bool expectedResult,
+// Point2 firstExpectedIntersectionPoint, Point2 secondExpectedIntersectionPoint)
+// {
+// float rayNearDistance, rayFarDistance;
+// var actualResult = ray.Intersects(boundingRectangle, out rayNearDistance, out rayFarDistance);
+// Assert.Equal(expectedResult, actualResult);
+
+// if (actualResult)
+// {
+// var firstActualIntersectionPoint = ray.Position + ray.Direction * rayNearDistance;
+// Assert.Equal(firstExpectedIntersectionPoint, firstActualIntersectionPoint);
+// var secondActualIntersectionPoint = ray.Position + ray.Direction * rayFarDistance;
+// Assert.Equal(secondExpectedIntersectionPoint, secondActualIntersectionPoint);
+// }
+// else
+// {
+// Assert.IsTrue(float.IsNaN(rayNearDistance));
+// Assert.IsTrue(float.IsNaN(rayFarDistance));
+// }
+// }
+
+// public IEnumerable<TestCaseData> EqualityTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Ray2(), new Ray2(), true).SetName("Two degenerate rays are equal.");
+// yield return
+// new TestCaseData(new Ray2(new Point2(float.MinValue, float.MaxValue),
+// new Vector2(float.MaxValue, float.MinValue)), new Ray2(new Point2(float.MaxValue, float.MinValue),
+// new Vector2(float.MaxValue, float.MinValue)), false).SetName(
+// "Two different non-degenerate rays are not equal.");
+// yield return
+// new TestCaseData(
+// new Ray2(new Point2(float.MinValue, float.MaxValue),
+// new Vector2(float.MinValue, float.MaxValue)), new Ray2(new Point2(float.MinValue, float.MaxValue),
+// new Vector2(float.MinValue, float.MaxValue)), true)
+// .SetName(
+// "Two identical non-degenerate rays are equal.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(EqualityTestCases))]
+// public void Equality(Ray2 ray1, Ray2 ray2, bool expectedToBeEqual)
+// {
+// Assert.IsTrue(Equals(ray1, ray2) == expectedToBeEqual);
+// Assert.IsTrue(ray1 == ray2 == expectedToBeEqual);
+// Assert.IsFalse(ray1 == ray2 != expectedToBeEqual);
+// Assert.IsTrue(ray1.Equals(ray2) == expectedToBeEqual);
+
+// if (expectedToBeEqual)
+// Assert.Equal(ray1.GetHashCode(), ray2.GetHashCode());
+// }
+
+// public IEnumerable<TestCaseData> InequalityTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Ray2(), null, false).SetName("A ray is not equal to a null object.");
+// yield return
+// new TestCaseData(new Ray2(), new object(), false).SetName(
+// "A ray is not equal to an instantiated object.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(InequalityTestCases))]
+// public void Inequality(Ray2 ray, object obj, bool expectedToBeEqual)
+// {
+// Assert.IsTrue(ray.Equals(obj) == expectedToBeEqual);
+// }
+
+// public IEnumerable<TestCaseData> HashCodeTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Ray2(), new Ray2(), true).SetName(
+// "Two degenerate rays have the same hash code.");
+// yield return
+// new TestCaseData(new Ray2(new Point2(50, 50), new Vector2(50, 50)),
+// new Ray2(new Point2(50, 50), new Vector2(50, 50)), true).SetName(
+// "Two indentical non-zero points have the same hash code.");
+// yield return
+// new TestCaseData(new Ray2(new Point2(0, 0), new Vector2(50, 50)),
+// new Ray2(new Point2(50, 50), new Vector2(50, 50)), false).SetName(
+// "Two different non-zero points do not have the same hash code.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(HashCodeTestCases))]
+// public void HashCode(Ray2 ray1, Ray2 ray2, bool expectedThatHashCodesAreEqual)
+// {
+// var hashCode1 = ray1.GetHashCode();
+// var hashCode2 = ray2.GetHashCode();
+// if (expectedThatHashCodesAreEqual)
+// Assert.Equal(hashCode1, hashCode2);
+// else
+// Assert.AreNotEqual(hashCode1, hashCode2);
+// }
+
+// public IEnumerable<TestCaseData> StringCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Ray2(),
+// string.Format(CultureInfo.CurrentCulture, "Position: {0}, Direction: {1}", new Point2(),
+// new Vector2())).SetName(
+// "The degenerate ray has the expected string representation using the current culture.");
+// yield return new TestCaseData(new Ray2(new Point2(5.1f, -5.123f), new Vector2(0, 1)),
+// string.Format(CultureInfo.CurrentCulture, "Position: {0}, Direction: {1}", new Point2(5.1f, -5.123f),
+// new Vector2(0, 1))).SetName(
+// "A non-degenerate ray has the expected string representation using the current culture.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(StringCases))]
+// public void String(Ray2 ray, string expectedString)
+// {
+// var actualString = ray.ToString();
+// Assert.Equal(expectedString, actualString);
+// }
+// }
+//}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/RectangleFTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/RectangleFTests.cs
new file mode 100644
index 0000000..9121d6a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/RectangleFTests.cs
@@ -0,0 +1,135 @@
+using Microsoft.Xna.Framework;
+using Xunit;
+
+namespace MonoGame.Extended.Tests.Primitives
+{
+ public class RectangleFTests
+ {
+ [Fact]
+ public void Rectangle_Intersects_Test()
+ {
+ var rect1 = new Rectangle(0, 0, 32, 32);
+ var rect2 = new Rectangle(32, 32, 32, 32);
+
+ Assert.False(rect1.Intersects(rect2));
+ }
+
+ [Fact]
+ public void PassVector2AsConstructorParameter_Test()
+ {
+ var rect1 = new RectangleF(new Vector2(0, 0), new Size2(12.34f, 56.78f));
+ var rect2 = new RectangleF(new Vector2(0, 0), new Vector2(12.34f, 56.78f));
+
+ Assert.Equal(rect1, rect2);
+ }
+
+ [Fact]
+ public void PassPointAsConstructorParameter_Test()
+ {
+ var rect1 = new RectangleF(new Vector2(0, 0), new Size2(12, 56));
+ var rect2 = new RectangleF(new Vector2(0, 0), new Size2(12, 56));
+
+ Assert.Equal(rect1, rect2);
+ }
+
+ public class Transform
+ {
+ [Fact]
+ public void Center_point_is_not_translated()
+ {
+ var rectangle = new RectangleF(new Point2(0, 0), new Size2(20, 30));
+ var transform = Matrix2.Identity;
+
+ var result = RectangleF.Transform(rectangle, ref transform);
+
+ Assert.Equal(new Point2(10, 15), result.Center);
+ }
+
+ [Fact]
+ public void Center_point_is_translated()
+ {
+ var rectangleF = new RectangleF(new Point2(0, 0), new Size2(20, 30));
+ var transform = Matrix2.CreateTranslation(1, 2);
+
+ var result = RectangleF.Transform(rectangleF, ref transform);
+
+ Assert.Equal(new Point2(11, 17), result.Center);
+ }
+
+ [Fact]
+ public void Size_is_not_changed_by_identity_transform()
+ {
+ var rectangle = new RectangleF(new Point2(0, 0), new Size2(20, 30));
+ var transform = Matrix2.Identity;
+
+ var result = RectangleF.Transform(rectangle, ref transform);
+
+ Assert.Equal(new Size2(20, 30), result.Size);
+ }
+
+ [Fact]
+ public void Size_is_not_changed_by_translation()
+ {
+ var rectangle = new RectangleF(new Point2(0, 0), new Size2(20, 30));
+ var transform = Matrix2.CreateTranslation(1, 2);
+
+ var result = RectangleF.Transform(rectangle, ref transform);
+
+ Assert.Equal(new Size2(20, 30), result.Size);
+ }
+
+ [Fact]
+ public void Applies_rotation_and_translation()
+ {
+ /* Rectangle with center point aligned in coordinate system with origin 0.
+ *
+ * :
+ * :
+ * +-+
+ * | |
+ * |p|
+ * | |
+ * ...............0-+............
+ * :
+ * :
+ * :
+ *
+ * Rotate center point p, 90 degrees around origin 0.
+ *
+ * :
+ * :
+ * +---+
+ * | p |
+ * ...........+---0..............
+ * :
+ * :
+ * :
+ *
+ * Then translate rectangle by x=10 and y=20.
+ * :
+ * : +---+
+ * : | p |
+ * y=21 - - - - - - - -> +---+
+ * .
+ * :
+ * ...............0..............
+ * :
+ * :
+ * :
+ */
+ var rectangle = new RectangleF(new Point2(0, 0), new Size2(2, 4));
+ var transform =
+ Matrix2.CreateRotationZ(MathHelper.PiOver2)
+ *
+ Matrix2.CreateTranslation(10, 20);
+
+ var result = RectangleF.Transform(rectangle, ref transform);
+
+ Assert.Equal(-2 + 10, result.Center.X, 6);
+ Assert.Equal(1 + 20, result.Center.Y, 6);
+ Assert.Equal(4, result.Size.Width, 6);
+ Assert.Equal(2, result.Size.Height, 6);
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/Segment2DTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/Segment2DTests.cs
new file mode 100644
index 0000000..b994527
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/Segment2DTests.cs
@@ -0,0 +1,251 @@
+//using System.Collections.Generic;
+//using System.Globalization;
+//using Xunit;
+
+//namespace MonoGame.Extended.Tests.Primitives
+//{
+// public class Segment2DTests
+// {
+// public IEnumerable<TestCaseData> ConstructorTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Point2(), new Point2()).SetName(
+// "The empty segment has expected starting and ending points.");
+// yield return
+// new TestCaseData(
+// new Point2(float.MaxValue, float.MinValue),
+// new Point2(int.MaxValue, int.MinValue)).SetName(
+// "A non-empty segment has the expected starting and ending points.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(ConstructorTestCases))]
+// public void Constructor(Point2 startingPoint, Point2 endingPoint)
+// {
+// var segment = new Segment2(startingPoint, endingPoint);
+// Assert.Equal(startingPoint, segment.Start);
+// Assert.Equal(endingPoint, segment.End);
+// }
+
+// public IEnumerable<TestCaseData> ClosestPointTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Segment2(), new Point2(), new Point2()).SetName(
+// "The closest point on the empty segment to the zero point is the zero point.");
+// yield return
+// new TestCaseData(new Segment2(new Point2(0, 0), new Point2(200, 0)), new Point2(-100, 200),
+// new Point2(0, 0)).SetName(
+// "The closest point on a non-empty segment to a point which is projected beyond the start of the segment is the segment's starting point.")
+// ;
+// yield return
+// new TestCaseData(new Segment2(new Point2(0, 0), new Point2(200, 0)), new Point2(400, 200),
+// new Point2(200, 0)).SetName(
+// "The closest point on a non-empty segment to a point which is projected beyond the end of the segment is the segment's ending point.")
+// ;
+// yield return
+// new TestCaseData(new Segment2(new Point2(0, 0), new Point2(200, 0)), new Point2(100, 200),
+// new Point2(100, 0)).SetName(
+// "The closest point on a non-empty segment to a point which is projected inside the segment is the projected point.")
+// ;
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(ClosestPointTestCases))]
+// public void ClosestPoint(Segment2 segment, Point2 point, Point2 expectedClosestPoint)
+// {
+// var actualClosestPoint = segment.ClosestPointTo(point);
+// Assert.Equal(expectedClosestPoint, actualClosestPoint);
+// }
+
+// public IEnumerable<TestCaseData> SquaredDistanceToPointTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Segment2(), new Point2(), 0).SetName(
+// "The squared distance of the zero point to the empty segment is 0.");
+// yield return
+// new TestCaseData(new Segment2(new Point2(0, 0), new Point2(20, 0)), new Point2(-10, 20), 500)
+// .SetName(
+// "The squared distance of a point projected beyond the start of a non-empty segment is the squared distance from the segment's starting point to the point.")
+// ;
+// yield return
+// new TestCaseData(new Segment2(new Point2(0, 0), new Point2(20, 0)), new Point2(40, 20), 400)
+// .SetName(
+// "The squared distance of a point projected beyond the end of a non-empty segment is the squared distance from the segment's ending point to the point.")
+// ;
+// yield return
+// new TestCaseData(new Segment2(new Point2(0, 0), new Point2(20, 0)), new Point2(10, 25), 625).SetName
+// (
+// "The squared distance of a point projected inside a non-empty segment is the squared distance from the projected point to the point.")
+// ;
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(SquaredDistanceToPointTestCases))]
+// public void SquaredDistanceToPoint(Segment2 segment, Point2 point,
+// float expectedDistance)
+// {
+// var actualDistance = segment.SquaredDistanceTo(point);
+// Assert.Equal(expectedDistance, actualDistance);
+// }
+
+// public IEnumerable<TestCaseData> IntersectsBoundingRectangleTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Segment2(), new BoundingRectangle(), true, Point2.Zero).SetName(
+// "The empty segment intersects the empty bounding box.");
+// yield return
+// new TestCaseData(new Segment2(new Point2(-75, -75), new Point2(75, -75)),
+// new BoundingRectangle(new Point2(), new Size2(50, 50)), false, Point2.NaN).SetName(
+// "A non-empty segment outside a non-empty bounding box does not intersect the bounding box.");
+// yield return
+// new TestCaseData(new Segment2(new Point2(0, 0), new Point2(25, 0)), new BoundingRectangle(new Point2(), new Size2(50, 50)),
+// true, new Point2(0, 0)).SetName(
+// "A non-empty segment inside a non-empty bounding box intersects the bounding box.");
+// yield return
+// new TestCaseData(new Segment2(new Point2(-100, 0), new Point2(100, 0)),
+// new BoundingRectangle(new Point2(), new Size2(50, 50)),
+// true, new Point2(-50, 0)).SetName(
+// "A non-empty segment crossing a non-empty bounding box intersects the bounding box.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(IntersectsBoundingRectangleTestCases))]
+// public void IntersectsBoundingRectangle(Segment2 segment, BoundingRectangle boundingRectangle, bool expectedResult,
+// Point2 expectedIntersectionPoint)
+// {
+// Point2 actualIntersectionPoint;
+// var actualResult = segment.Intersects(boundingRectangle, out actualIntersectionPoint);
+// Assert.Equal(expectedResult, actualResult);
+
+// if (actualResult)
+// {
+// Assert.Equal(expectedIntersectionPoint, actualIntersectionPoint);
+// }
+// else
+// {
+// Assert.IsTrue(float.IsNaN(actualIntersectionPoint.X));
+// Assert.IsTrue(float.IsNaN(actualIntersectionPoint.Y));
+// }
+// }
+
+// public IEnumerable<TestCaseData> EqualityTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Segment2(), new Segment2(), true).SetName("Empty segments are equal.")
+// ;
+// yield return
+// new TestCaseData(
+// new Segment2(new Point2(0, 0), new Point2(float.MaxValue, float.MinValue)),
+// new Segment2(new Point2(0, 0),
+// new Point2(float.MinValue, float.MaxValue)), false).SetName(
+// "Two different non-empty segments are not equal.");
+// yield return
+// new TestCaseData(
+// new Segment2(new Point2(0, 0), new Point2(float.MinValue, float.MaxValue)),
+// new Segment2(new Point2(0, 0),
+// new Point2(float.MinValue, float.MaxValue)), true).SetName(
+// "Two identical non-empty segments are equal.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(EqualityTestCases))]
+// public void Equality(Segment2 segment1, Segment2 segment2, bool expectedToBeEqual)
+// {
+// Assert.IsTrue(Equals(segment1, segment2) == expectedToBeEqual);
+// Assert.IsTrue(segment1 == segment2 == expectedToBeEqual);
+// Assert.IsFalse(segment1 == segment2 != expectedToBeEqual);
+// Assert.IsTrue(segment1.Equals(segment2) == expectedToBeEqual);
+
+// if (expectedToBeEqual)
+// Assert.Equal(segment1.GetHashCode(), segment2.GetHashCode());
+// }
+
+// public IEnumerable<TestCaseData> InequalityTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Segment2(), null, false).SetName("A segment is not equal to a null object.");
+// yield return
+// new TestCaseData(new Segment2(), new object(), false).SetName(
+// "A segment is not equal to an instantiated object.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(InequalityTestCases))]
+// public void Inequality(Segment2 segment, object obj, bool expectedToBeEqual)
+// {
+// Assert.IsTrue(segment.Equals(obj) == expectedToBeEqual);
+// }
+
+// public IEnumerable<TestCaseData> HashCodeTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Segment2(), new Segment2(), true).SetName(
+// "Two empty segments have the same hash code.");
+// yield return
+// new TestCaseData(new Segment2(new Point2(0, 0), new Point2(50, 50)),
+// new Segment2(new Point2(0, 0), new Point2(50, 50)), true).SetName(
+// "Two indentical non-empty segments have the same hash code.");
+// yield return
+// new TestCaseData(new Segment2(new Point2(0, 0), new Point2(50, 50)),
+// new Segment2(new Point2(50, 50), new Point2(50, 50)), false).SetName(
+// "Two different non-empty segments do not have the same hash code.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(HashCodeTestCases))]
+// public void HashCode(Segment2 segment1, Segment2 segment2, bool expectedThatHashCodesAreEqual)
+// {
+// var hashCode1 = segment1.GetHashCode();
+// var hashCode2 = segment2.GetHashCode();
+// if (expectedThatHashCodesAreEqual)
+// Assert.Equal(hashCode1, hashCode2);
+// else
+// Assert.AreNotEqual(hashCode1, hashCode2);
+// }
+
+// public IEnumerable<TestCaseData> StringCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Segment2(),
+// string.Format(CultureInfo.CurrentCulture, "{0} -> {1}", new Point2(),
+// new Point2())).SetName(
+// "The empty segment has the expected string representation using the current culture.");
+// yield return new TestCaseData(new Segment2(new Point2(5.1f, -5.123f), new Point2(5.4f, -5.4123f)),
+// string.Format(CultureInfo.CurrentCulture, "{0} -> {1}", new Point2(5.1f, -5.123f),
+// new Point2(5.4f, -5.4123f))).SetName(
+// "A non-empty segment has the expected string representation using the current culture.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(StringCases))]
+// public void String(Segment2 segment, string expectedString)
+// {
+// var actualString = segment.ToString();
+// Assert.Equal(expectedString, actualString);
+// }
+// }
+//}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/ShapeTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/ShapeTests.cs
new file mode 100644
index 0000000..37b2fc4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/ShapeTests.cs
@@ -0,0 +1,180 @@
+using Microsoft.Xna.Framework;
+using Xunit;
+
+namespace MonoGame.Extended.Tests.Primitives;
+
+public class ShapeTests
+{
+ public class CircleFTests
+ {
+ [Fact]
+ public void CircCircIntersectionSameCircleTest()
+ {
+ IShapeF shape1 = new CircleF(Point2.Zero, 2.0f);
+ IShapeF shape2 = new CircleF(Point2.Zero, 2.0f);
+
+ Assert.True(shape1.Intersects(shape2));
+ }
+
+ [Fact]
+ public void CircCircIntersectionOverlappingTest()
+ {
+ IShapeF shape1 = new CircleF(new Point2(1, 2), 2.0f);
+ IShapeF shape2 = new CircleF(Point2.Zero, 2.0f);
+
+ Assert.True(shape1.Intersects(shape2));
+ }
+
+ [Fact]
+ public void CircleCircleNotIntersectingTest()
+ {
+ IShapeF shape1 = new CircleF(new Point2(5, 5), 2.0f);
+ IShapeF shape2 = new CircleF(Point2.Zero, 2.0f);
+
+ Assert.False(shape1.Intersects(shape2));
+ }
+ }
+
+ public class RectangleFTests
+ {
+ [Fact]
+ public void RectRectSameRectTest()
+ {
+ IShapeF shape1 = new RectangleF(Point2.Zero, new Size2(5, 5));
+ IShapeF shape2 = new RectangleF(Point2.Zero, new Size2(5, 5));
+
+ Assert.True(shape1.Intersects(shape2));
+ }
+
+ [Fact]
+ public void RectRectIntersectingTest()
+ {
+ IShapeF shape1 = new RectangleF(Point2.Zero, new Size2(5, 5));
+ IShapeF shape2 = new RectangleF(new Point2(3, 3), new Size2(5, 5));
+
+ Assert.True(shape1.Intersects(shape2));
+ }
+
+ [Fact]
+ public void RectRectNotIntersectingTest()
+ {
+ IShapeF shape1 = new RectangleF(Point2.Zero, new Size2(5, 5));
+ IShapeF shape2 = new RectangleF(new Point2(10, 10), new Size2(5, 5));
+
+ Assert.False(shape1.Intersects(shape2));
+ }
+
+ [Fact]
+ public void RectCircContainedTest()
+ {
+ IShapeF shape1 = new RectangleF(Point2.Zero, new Size2(5, 5));
+ IShapeF shape2 = new CircleF(Point2.Zero, 4);
+
+ Assert.True(shape1.Intersects(shape2));
+ Assert.True(shape2.Intersects(shape1));
+ }
+
+ [Fact]
+ public void RectCircOnEdgeTest()
+ {
+ IShapeF shape1 = new RectangleF(Point2.Zero, new Size2(5, 5));
+ IShapeF shape2 = new CircleF(new Point2(5, 0), 4);
+
+ Assert.True(shape1.Intersects(shape2));
+ Assert.True(shape2.Intersects(shape1));
+ }
+ }
+
+ public class OrientedRectangleTests
+ {
+ [Fact]
+ public void Axis_aligned_rectangle_intersects_circle()
+ {
+ /*
+ * :
+ * :
+ * +*+
+ * ...........* *.........
+ * +*+
+ * :
+ * :
+ */
+ IShapeF rectangle = new OrientedRectangle(Point2.Zero, new Size2(1, 1), Matrix2.Identity);
+ var circle = new CircleF(Point2.Zero, 1);
+
+ Assert.True(rectangle.Intersects(circle));
+ }
+
+ [Fact]
+ public void Axis_aligned_rectangle_does_not_intersect_circle_in_top_left()
+ {
+ /*
+ * * :
+ * * *:
+ * *+-+
+ * ...........| |.........
+ * +-+
+ * :
+ * :
+ */
+ IShapeF rectangle = new OrientedRectangle(Point2.Zero, new Size2(1, 1), Matrix2.Identity);
+ var circle = new CircleF(new Point2(-2, 1), .99f);
+
+ Assert.False(rectangle.Intersects(circle));
+ }
+
+ [Fact]
+ public void Rectangle_rotated_45_degrees_does_not_intersect_circle_in_bottom_right()
+ {
+ /*
+ * :
+ * :
+ * +-.
+ * .........../ / ........
+ * +./* *
+ * * *
+ * :* *
+ */
+ IShapeF rectangle = new OrientedRectangle(new Point2(-1, 1), new Size2(1.42f, 1.42f), Matrix2.CreateRotationZ(-MathHelper.PiOver4));
+ var circle = new CircleF(new Point2(1, -1), 1.4f);
+
+ Assert.False(rectangle.Intersects(circle));
+ }
+
+ [Fact]
+ public void Axis_aligned_rectangle_does_not_intersect_rectangle()
+ {
+ /*
+ * :
+ * :
+ * +-+
+ * ..........| |**.......
+ * +-+ *
+ * :**
+ * :
+ */
+ IShapeF rectangle = new OrientedRectangle(new Point2(-1, 0), new Size2(1, 1), Matrix2.Identity);
+ var rect = new RectangleF(new Point2(0.001f, 0), new Size2(2, 2));
+
+ Assert.False(rectangle.Intersects(rect));
+ }
+
+ [Fact]
+ public void Axis_aligned_rectangle_intersects_rectangle()
+ {
+ /*
+ * :
+ * :
+ * +-+
+ * ..........| |**.......
+ * +-+ *
+ * :**
+ * :
+ */
+ IShapeF rectangle = new OrientedRectangle(new Point2(-1, 0), new Size2(1, 1), Matrix2.Identity);
+ var rect = new RectangleF(new Point2(0, 0), new Size2(2, 2));
+
+ Assert.True(rectangle.Intersects(rect));
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/Size2Tests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/Size2Tests.cs
new file mode 100644
index 0000000..faaa426
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Primitives/Size2Tests.cs
@@ -0,0 +1,304 @@
+//using System.Collections.Generic;
+//using System.Globalization;
+//using Microsoft.Xna.Framework;
+//using Xunit;
+
+//namespace MonoGame.Extended.Tests.Primitives
+//{
+//
+// public class Size2Tests
+// {
+// public IEnumerable<TestCaseData> ConstructorTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(0, 0).SetName(
+// "The empty size has the expected dimensions.");
+// yield return
+// new TestCaseData(float.MinValue, float.MaxValue).SetName
+// (
+// "A non-empty size has the expected dimensions.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(ConstructorTestCases))]
+// public void Constructor(float width, float height)
+// {
+// var size = new Size2(width, height);
+// Assert.Equal(width, size.Width);
+// Assert.Equal(height, size.Height);
+// }
+
+// public IEnumerable<TestCaseData> DimensionsTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Size2(), 0, 0).SetName(
+// "The empty size has the expected dimensions.");
+// yield return
+// new TestCaseData(new Size2(float.MinValue, float.MaxValue), float.MinValue, float.MaxValue).SetName
+// (
+// "A non-empty size has the expected dimensions.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(DimensionsTestCases))]
+// public void Dimensions(Size2 size, float expectedWidth, float expecetedHeight)
+// {
+// Assert.Equal(expectedWidth, size.Width);
+// Assert.Equal(expecetedHeight, size.Height);
+
+// size.Width = 10;
+// Assert.Equal(10, size.Width);
+
+// size.Height = -10.123f;
+// Assert.Equal(-10.123f, size.Height);
+// }
+
+// public IEnumerable<TestCaseData> AdditionTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Size2(), new Size2(), new Size2()).SetName(
+// "The addition of two empty sizes is the empty size.");
+// yield return
+// new TestCaseData(new Size2(5, 5), new Size2(15, 15), new Size2(20, 20)).SetName(
+// "The addition of two non-empty sizes is the expected size.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(AdditionTestCases))]
+// public void Addition(Size2 size1, Size2 size2, Size2 expectedSize)
+// {
+// Assert.Equal(expectedSize, size1 + size2);
+// Assert.Equal(expectedSize, Size2.Add(size1, size2));
+// }
+
+// public IEnumerable<TestCaseData> SubtractionTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Size2(), new Size2(), new Size2()).SetName(
+// "The subtraction of two empty sizes is the empty size.");
+// yield return
+// new TestCaseData(new Size2(5, 5), new Size2(15, 15), new Size2(-10, -10)).SetName(
+// "The subtraction of two non-empty sizes is the expected size.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(SubtractionTestCases))]
+// public void Subtraction(Size2 size1, Size2 size2, Size2 expectedSize)
+// {
+// Assert.Equal(expectedSize, size1 - size2);
+// Assert.Equal(expectedSize, Size2.Subtract(size1, size2));
+// }
+
+// public IEnumerable<TestCaseData> EqualityTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Size2(), new Size2(), true).SetName("Two empty sizes are equal.");
+// yield return
+// new TestCaseData(new Size2(float.MinValue, float.MaxValue),
+// new Size2(float.MaxValue, float.MinValue), false).SetName(
+// "Two different non-empty sizes are not equal.");
+// yield return
+// new TestCaseData(
+// new Size2(float.MinValue, float.MaxValue), new Size2(float.MinValue, float.MaxValue), true)
+// .SetName(
+// "Two identical non-empty sizes are equal.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(EqualityTestCases))]
+// public void Equality(Size2 size1, Size2 size2, bool expectedToBeEqual)
+// {
+// Assert.IsTrue(Equals(size1, size2) == expectedToBeEqual);
+// Assert.IsTrue(size1 == size2 == expectedToBeEqual);
+// Assert.IsFalse(size1 == size2 != expectedToBeEqual);
+// Assert.IsTrue(size1.Equals(size2) == expectedToBeEqual);
+
+// if (expectedToBeEqual)
+// Assert.Equal(size1.GetHashCode(), size2.GetHashCode());
+// }
+
+// public IEnumerable<TestCaseData> InequalityTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Size2(), null, false).SetName("A size is not equal to a null object.");
+// yield return
+// new TestCaseData(new Size2(), new object(), false).SetName(
+// "A size is not equal to an instantiated object.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(InequalityTestCases))]
+// public void Inequality(Size2 size, object obj, bool expectedToBeEqual)
+// {
+// Assert.IsTrue(size.Equals(obj) == expectedToBeEqual);
+// }
+
+// public IEnumerable<TestCaseData> HashCodeTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Size2(), new Size2(), true).SetName(
+// "Two empty sizes have the same hash code.");
+// yield return
+// new TestCaseData(new Size2(50, 50), new Size2(50, 50), true).SetName(
+// "Two indentical non-empty sizes have the same hash code.");
+// yield return
+// new TestCaseData(new Size2(0, 0), new Size2(50, 50), false).SetName(
+// "Two different non-empty sizes do not have the same hash code.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(HashCodeTestCases))]
+// public void HashCode(Size2 size1, Size2 size2, bool expectedThatHashCodesAreEqual)
+// {
+// var hashCode1 = size1.GetHashCode();
+// var hashCode2 = size2.GetHashCode();
+// if (expectedThatHashCodesAreEqual)
+// Assert.Equal(hashCode1, hashCode2);
+// else
+// Assert.AreNotEqual(hashCode1, hashCode2);
+// }
+
+// public IEnumerable<TestCaseData> ToPointTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Size2(), new Point2()).SetName("The empty size converted to a point is the zero point.");
+// yield return
+// new TestCaseData(new Size2(float.MinValue, float.MaxValue), new Point2(float.MinValue, float.MaxValue)).SetName(
+// "A non-empty size converted to a point is the expected point.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(ToPointTestCases))]
+// public void ToPoint(Size2 size, Point2 expectedPoint)
+// {
+// var actualPoint = (Point2)size;
+// Assert.Equal(expectedPoint, actualPoint);
+// }
+
+// public IEnumerable<TestCaseData> FromPointTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Point2(), new Size2()).SetName("The zero point converted to a size is the empty size.");
+// yield return
+// new TestCaseData(new Point2(float.MinValue, float.MaxValue), new Size2(float.MinValue, float.MaxValue)).SetName(
+// "A non-zero point converted to a size is the expected size.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(FromPointTestCases))]
+// public void FromPoint(Point2 point, Size2 expectedSize)
+// {
+// var actualSize = (Size2)point;
+// Assert.Equal(expectedSize, actualSize);
+// }
+
+// public IEnumerable<TestCaseData> ToVectorTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Size2(), new Vector2()).SetName("The empty size converted to a vector is the zero vector.");
+// yield return
+// new TestCaseData(new Size2(float.MinValue, float.MaxValue), new Vector2(float.MinValue, float.MaxValue)).SetName(
+// "A non-empty size converted to a vector is the expected vector.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(ToVectorTestCases))]
+// public void ToVector(Size2 size, Vector2 expectedVector)
+// {
+// var actualVector = (Vector2)size;
+// Assert.Equal(expectedVector, actualVector);
+// }
+
+// public IEnumerable<TestCaseData> FromVectorTestCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Vector2(), new Size2()).SetName("The zero vector converted to a size is the empty size.");
+// yield return
+// new TestCaseData(new Vector2(float.MinValue, float.MaxValue), new Size2(float.MinValue, float.MaxValue)).SetName(
+// "A non-zero vector converted to a size is the expected size.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(FromVectorTestCases))]
+// public void FromVector(Vector2 vector, Size2 expectedSize)
+// {
+// var actualSize = (Size2)vector;
+// Assert.Equal(expectedSize, actualSize);
+// }
+
+// //public IEnumerable<TestCaseData> FromSizeTestCases
+// //{
+// // get
+// // {
+// // yield return
+// // new TestCaseData(new Size2(), new Size2()).SetName("The empty size converted to a size is the empty size.");
+// // yield return
+// // new TestCaseData(new Size2(int.MinValue, int.MaxValue), new Size2(int.MinValue, int.MaxValue)).SetName(
+// // "A non-zero size converted to a size is the expected size.");
+// // }
+// //}
+
+// //[Fact]
+// //[TestCaseSource(nameof(FromSizeTestCases))]
+// //public void FromSize(Size2 size, Size2 expectedSize)
+// //{
+// // var actualSize = (Size2)size;
+// // Assert.Equal(expectedSize, actualSize);
+// //}
+
+// public IEnumerable<TestCaseData> StringCases
+// {
+// get
+// {
+// yield return
+// new TestCaseData(new Size2(),
+// string.Format(CultureInfo.CurrentCulture, "Width: {0}, Height: {1}", 0, 0)).SetName(
+// "The empty size has the expected string representation using the current culture.");
+// yield return new TestCaseData(new Size2(5.1f, -5.123f),
+// string.Format(CultureInfo.CurrentCulture, "Width: {0}, Height: {1}", 5.1f, -5.123f)).SetName(
+// "A non-empty size has the expected string representation using the current culture.");
+// }
+// }
+
+// [Fact]
+// [TestCaseSource(nameof(StringCases))]
+// public void String(Size2 size, string expectedString)
+// {
+// var actualString = size.ToString();
+// Assert.Equal(expectedString, actualString);
+// }
+// }
+//}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/RangeTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/RangeTests.cs
new file mode 100644
index 0000000..d022dcc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/RangeTests.cs
@@ -0,0 +1,102 @@
+using System;
+using Xunit;
+
+namespace MonoGame.Extended.Tests
+{
+
+ public class RangeTests
+ {
+ [Fact]
+ public void ConstructorTest()
+ {
+ //can pass min < max
+ var unused = new Range<int>(10, 100);
+ //can't pass min > max
+ Assert.Throws<ArgumentException>(() => new Range<int>(100, 10));
+ //can pass min == max
+ var unused1 = new Range<int>(10, 10);
+ }
+
+ [Fact]
+ public void DegenerateTest()
+ {
+ var proper = new Range<double>(0, 1);
+ Assert.True(proper.IsProper);
+ Assert.False(proper.IsDegenerate);
+
+ var degenerate = new Range<double>(1, 1);
+ Assert.False(degenerate.IsProper);
+ Assert.True(degenerate.IsDegenerate);
+ }
+
+ [Fact]
+ public void IntegerTest()
+ {
+ var range = new Range<int>(10, 100);
+
+ Assert.Equal(10, range.Min);
+ Assert.Equal(100, range.Max);
+
+ for (var i = 10; i <= 100; i++)
+ {
+ Assert.True(range.IsInBetween(i));
+ }
+
+ Assert.False(range.IsInBetween(9));
+ Assert.False(range.IsInBetween(101));
+ Assert.False(range.IsInBetween(10, true));
+ Assert.False(range.IsInBetween(100, maxValueExclusive: true));
+ }
+
+ [Fact]
+ public void FloatTest()
+ {
+ var range = new Range<float>(0f, 1f);
+
+ Assert.Equal(0f, range.Min);
+ Assert.Equal(1f, range.Max);
+
+ for (float i = 0; i <= 1f; i += 0.001f)
+ {
+ Assert.True(range.IsInBetween(i));
+ }
+
+ Assert.False(range.IsInBetween(-float.Epsilon));
+ Assert.False(range.IsInBetween(1.00001f));
+
+ Assert.False(range.IsInBetween(0f, true));
+ Assert.False(range.IsInBetween(1f, maxValueExclusive: true));
+ }
+
+ [Fact]
+ public void OperatorTest()
+ {
+ var rangeA = new Range<int>(0, 1);
+ var rangeB = new Range<int>(0, 1);
+ var rangeC = new Range<int>(1, 2);
+ var rangeD = new Range<double>(0, 1);
+
+ Assert.True(rangeA == rangeB);
+ Assert.False(rangeA == rangeC);
+
+ Assert.False(rangeA != rangeB);
+ Assert.True(rangeA != rangeC);
+
+ Assert.True(rangeA.Equals(rangeB));
+ Assert.False(rangeA.Equals(rangeC));
+ Assert.False(rangeA.Equals(rangeD));
+
+ Range<int> implict = 1;
+ Assert.Equal(1, implict.Max);
+ Assert.Equal(1, implict.Min);
+ }
+
+ [Fact]
+ public void ToStringTest()
+ {
+ var range = new Range<float>(0, 1);
+
+ Assert.Equal("Range<Single> [0 1]", range.ToString());
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Serialization/ColorJsonConverterTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Serialization/ColorJsonConverterTests.cs
new file mode 100644
index 0000000..ae8e1e7
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Serialization/ColorJsonConverterTests.cs
@@ -0,0 +1,66 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Text.Json;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Serialization;
+
+namespace MonoGame.Extended.Tests.Serialization;
+
+public sealed class ColorJsonConverterTests
+{
+ private readonly ColorJsonConverter _converter = new ColorJsonConverter();
+
+ [Fact]
+ public void CanConvert_ColorType_ReturnsTrue()
+ {
+ var colorType = typeof(Color);
+ var result = _converter.CanConvert(colorType);
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void CanConvert_NonColorType_ReturnsFalse()
+ {
+ var otherType = typeof(string);
+ var result = _converter.CanConvert(otherType);
+ Assert.False(result);
+ }
+
+ [Theory]
+ [InlineData("Red", 255, 0, 0, 255)]
+ [InlineData("#FF0000FF", 255, 0, 0, 255)]
+ public void Read_ValidColorJson_ReturnsExpectedColor(string jsonValue, byte r, byte g, byte b, byte a)
+ {
+ var json = $"\"{jsonValue}\"";
+ var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json));
+
+ reader.Read();
+ var actual = _converter.Read(ref reader, typeof(Color), new JsonSerializerOptions());
+
+ var expected = new Color(r, g, b, a);
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void Write_ValidColor_WritesExpectedJson()
+ {
+ var expected = "#ff000000";
+ var color = ColorHelper.FromHex(expected);
+ using var stream = new MemoryStream();
+ using var writer = new Utf8JsonWriter(stream);
+
+ _converter.Write(writer, color, new JsonSerializerOptions());
+ writer.Flush();
+ var actual = Encoding.UTF8.GetString(stream.ToArray());
+
+ Assert.Equal($"\"{expected}\"", actual);
+ }
+
+ [Fact]
+ public void Write_NullWrier_ThrowArgumentNullException()
+ {
+ var color = Color.MonoGameOrange;
+ Assert.Throws<ArgumentNullException>(() => _converter.Write(null, color, new JsonSerializerOptions()));
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Serialization/RectangleFJsonConverterTest.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Serialization/RectangleFJsonConverterTest.cs
new file mode 100644
index 0000000..96f9c4f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Serialization/RectangleFJsonConverterTest.cs
@@ -0,0 +1,36 @@
+using System.IO;
+using System.Text.Json;
+using MonoGame.Extended.Serialization;
+using Xunit;
+
+namespace MonoGame.Extended.Tests.Serialization;
+
+public class RectangleFJsonConverterTest
+{
+
+ public class TestContent
+ {
+ public RectangleF Box { get; set; }
+ }
+
+ [Fact]
+ public void ConstructorTest()
+ {
+ var jsonData = @"
+{
+ ""box"": ""1 1 10 10""
+}
+";
+ var options = new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ };
+ options.Converters.Add(new RectangleFJsonConverter());
+ var content = JsonSerializer.Deserialize<TestContent>(jsonData, options);
+
+ Assert.Equal(1, content.Box.Left);
+ Assert.Equal(1, content.Box.Top);
+ Assert.Equal(10, content.Box.Width);
+ Assert.Equal(10, content.Box.Height);
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Shapes/PolygonFTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Shapes/PolygonFTests.cs
new file mode 100644
index 0000000..d052908
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Shapes/PolygonFTests.cs
@@ -0,0 +1,87 @@
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Shapes;
+using Xunit;
+
+namespace MonoGame.Extended.Tests.Shapes
+{
+ public class PolygonFTests
+ {
+ [Fact]
+ public void Polygon_Contains_Point_Test()
+ {
+ var vertices = new[]
+ {
+ new Vector2(0, 0),
+ new Vector2(10, 0),
+ new Vector2(10, 10),
+ new Vector2(0, 10)
+ };
+
+ var polygon = new Polygon(vertices);
+
+ Assert.True(polygon.Contains(new Vector2(5, 5)));
+ Assert.True(polygon.Contains(new Vector2(0.01f, 0.01f)));
+ Assert.True(polygon.Contains(new Vector2(9.99f, 9.99f)));
+ Assert.False(polygon.Contains(new Vector2(-1f, -1f)));
+ Assert.False(polygon.Contains(new Vector2(-11f, -11f)));
+ }
+
+ [Fact]
+ public void Polygon_Transform_Translation_Test()
+ {
+ var vertices = new[]
+ {
+ new Vector2(0, 0),
+ new Vector2(10, 0),
+ new Vector2(10, 10),
+ new Vector2(0, 10)
+ };
+
+ var polygon = new Polygon(vertices);
+ polygon.Offset(new Vector2(2, 3));
+
+ Assert.Equal(new Vector2(2, 3), polygon.Vertices[0]);
+ Assert.Equal(new Vector2(12, 3), polygon.Vertices[1]);
+ Assert.Equal(new Vector2(12, 13), polygon.Vertices[2]);
+ Assert.Equal(new Vector2(2, 13), polygon.Vertices[3]);
+ }
+
+ [Fact]
+ public void Polygon_Transform_Rotation_Test()
+ {
+ var vertices = new[]
+ {
+ new Vector2(-5, -5),
+ new Vector2(5, 10),
+ new Vector2(-5, 10)
+ };
+
+ var polygon = new Polygon(vertices);
+ polygon.Rotate(MathHelper.ToRadians(90));
+
+ const float tolerance = 0.01f;
+ Assert.True(new Vector2(5, -5).EqualsWithTolerence(polygon.Vertices[0], tolerance));
+ Assert.True(new Vector2(-10, 5).EqualsWithTolerence(polygon.Vertices[1], tolerance));
+ Assert.True(new Vector2(-10, -5).EqualsWithTolerence(polygon.Vertices[2], tolerance));
+ }
+
+ [Fact]
+ public void Polygon_Transform_Scale_Test()
+ {
+ var vertices = new[]
+ {
+ new Vector2(0, -1),
+ new Vector2(1, 1),
+ new Vector2(-1, 1)
+ };
+
+ var polygon = new Polygon(vertices);
+ polygon.Scale(new Vector2(1, -0.5f));
+
+ const float tolerance = 0.01f;
+ Assert.True(new Vector2(0, -0.5f).EqualsWithTolerence(polygon.Vertices[0], tolerance), "0");
+ Assert.True(new Vector2(2f, 0.5f).EqualsWithTolerence(polygon.Vertices[1], tolerance), "1");
+ Assert.True(new Vector2(-2f, 0.5f).EqualsWithTolerence(polygon.Vertices[2], tolerance), "2");
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Sprites/SpriteSheetAnimationTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Sprites/SpriteSheetAnimationTests.cs
new file mode 100644
index 0000000..35ad3e3
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Sprites/SpriteSheetAnimationTests.cs
@@ -0,0 +1,910 @@
+using System;
+using Microsoft.Xna.Framework;
+using MonoGame.Extended.Sprites;
+using MonoGame.Extended.TextureAtlases;
+using Xunit;
+
+namespace MonoGame.Extended.Tests.Sprites
+{
+ public class SpriteSheetAnimationTests
+ {
+ [Theory]
+ [InlineData(0, 0)]
+ [InlineData(0, 0.9f)]
+ [InlineData(1, 1f)]
+ [InlineData(1, 1.9f)]
+ [InlineData(0, 2f)]
+ [InlineData(0, 2.9f)]
+ [InlineData(1, 3f)]
+ [InlineData(0, 4f)]
+ [InlineData(1, 5f)]
+ public void Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete(int expectedTextureRegionIndex, float time)
+ {
+ var textureRegion2D1 = new TextureRegion2D("Region 1", null, new Rectangle());
+ var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle());
+
+ var textureRegions = new[] { textureRegion2D1, textureRegion2D2 };
+
+ var spriteSheetAnimationData = new SpriteSheetAnimationData(
+ new[] { 0, 1 },
+ 1,
+ true
+ );
+
+ var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData);
+ var isCompleteFired = false;
+ spriteSheetAnimation.OnCompleted += () => isCompleteFired = true;
+
+ spriteSheetAnimation.Play();
+ var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time));
+
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+ }
+
+ [Fact]
+ public void Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete_Over_Multiple_Updates()
+ {
+ var textureRegion2D1 = new TextureRegion2D("Region 1", null, new Rectangle());
+ var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle());
+
+ var textureRegions = new[] { textureRegion2D1, textureRegion2D2 };
+
+ var spriteSheetAnimationData = new SpriteSheetAnimationData(
+ new[] { 0, 1 },
+ 1,
+ true
+ );
+
+ var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData);
+ var isCompleteFired = false;
+ spriteSheetAnimation.OnCompleted += () => isCompleteFired = true;
+
+ spriteSheetAnimation.Play();
+ var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(0));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+ }
+
+ [Theory]
+ [InlineData(0, 0.9f)]
+ [InlineData(1, 1f)]
+ [InlineData(1, 1.1f)]
+ [InlineData(1, 1.9f)]
+ public void Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete(int expectedTextureRegionIndex, float time)
+ {
+ var textureRegion2D1 = new TextureRegion2D("Region 1", null, new Rectangle());
+ var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle());
+
+ var textureRegions = new[] { textureRegion2D1, textureRegion2D2 };
+
+ var spriteSheetAnimationData = new SpriteSheetAnimationData(
+ new[] { 0, 1 },
+ 1,
+ false
+ );
+
+ var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData);
+ var isCompleteFired = false;
+ spriteSheetAnimation.OnCompleted += () => isCompleteFired = true;
+
+ spriteSheetAnimation.Play();
+ var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time));
+
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+ }
+
+ [Theory]
+ [InlineData(1, 2f)]
+ [InlineData(1, 3f)]
+ [InlineData(1, 4f)]
+ public void Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Complete_When_AnimationDuration_Is_Reached(int expectedTextureRegionIndex, float time)
+ {
+ var textureRegion2D1 = new TextureRegion2D("Region 1", null, new Rectangle());
+ var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle());
+
+ var textureRegions = new[] { textureRegion2D1, textureRegion2D2 };
+
+ var spriteSheetAnimationData = new SpriteSheetAnimationData(
+ new[] { 0, 1 },
+ 1,
+ false
+ );
+
+ var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData);
+ var isCompleteFired = false;
+ spriteSheetAnimation.OnCompleted += () => isCompleteFired = true;
+
+ spriteSheetAnimation.Play();
+ var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time));
+
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame);
+ Assert.True(spriteSheetAnimation.IsComplete);
+ Assert.True(isCompleteFired);
+ }
+
+ [Fact]
+ public void Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Complete_When_AnimationDuration_Is_Reached_Over_Multiple_Updates()
+ {
+ var textureRegion2D1 = new TextureRegion2D("Region 1", null, new Rectangle());
+ var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle());
+
+ var textureRegions = new[] { textureRegion2D1, textureRegion2D2 };
+
+ var spriteSheetAnimationData = new SpriteSheetAnimationData(
+ new[] { 0, 1 },
+ 1,
+ false
+ );
+
+ var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData);
+ var isCompleteFired = false;
+ spriteSheetAnimation.OnCompleted += () => isCompleteFired = true;
+
+ spriteSheetAnimation.Play();
+
+ var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(0));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame);
+ Assert.True(spriteSheetAnimation.IsComplete);
+ Assert.True(isCompleteFired);
+
+ isCompleteFired = false; // Reset isCompleteFired for next execution
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame);
+ Assert.True(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired); // Event is not fired again as animation was already completed
+ }
+
+ [Theory]
+ [InlineData(1, 0)]
+ [InlineData(1, 0.9f)]
+ [InlineData(0, 1f)]
+ [InlineData(0, 1.9f)]
+ [InlineData(1, 2f)]
+ [InlineData(1, 2.9f)]
+ [InlineData(0, 3f)]
+ [InlineData(1, 4f)]
+ [InlineData(0, 5f)]
+ public void Reversed_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete(int expectedTextureRegionIndex, float time)
+ {
+ var textureRegion2D1 = new TextureRegion2D("Region 1", null, new Rectangle());
+ var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle());
+
+ var textureRegions = new[] { textureRegion2D1, textureRegion2D2 };
+
+ var spriteSheetAnimationData = new SpriteSheetAnimationData(
+ new[] { 0, 1 },
+ 1,
+ true,
+ true
+ );
+
+ var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData);
+ var isCompleteFired = false;
+ spriteSheetAnimation.OnCompleted += () => isCompleteFired = true;
+
+ spriteSheetAnimation.Play();
+ var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time));
+
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+ }
+
+ [Fact]
+ public void Reversed_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete_Over_Multiple_Updates()
+ {
+ var textureRegion2D1 = new TextureRegion2D("Region 1", null, new Rectangle());
+ var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle());
+
+ var textureRegions = new[] { textureRegion2D1, textureRegion2D2 };
+
+ var spriteSheetAnimationData = new SpriteSheetAnimationData(
+ new[] { 0, 1 },
+ 1,
+ true,
+ true
+ );
+
+ var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData);
+ var isCompleteFired = false;
+ spriteSheetAnimation.OnCompleted += () => isCompleteFired = true;
+
+ spriteSheetAnimation.Play();
+ var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(0));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+ }
+
+ [Theory]
+ [InlineData(1, 0.9f)]
+ [InlineData(0, 1f)]
+ [InlineData(0, 1.1f)]
+ [InlineData(0, 1.9f)]
+ public void Reversed_Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete(int expectedTextureRegionIndex, float time)
+ {
+ var textureRegion2D1 = new TextureRegion2D("Region 1", null, new Rectangle());
+ var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle());
+
+ var textureRegions = new[] { textureRegion2D1, textureRegion2D2 };
+
+ var spriteSheetAnimationData = new SpriteSheetAnimationData(
+ new[] { 0, 1 },
+ 1,
+ false,
+ true
+ );
+
+ var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData);
+ var isCompleteFired = false;
+ spriteSheetAnimation.OnCompleted += () => isCompleteFired = true;
+
+ spriteSheetAnimation.Play();
+ var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time));
+
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+ }
+
+ [Theory]
+ [InlineData(0, 2f)]
+ [InlineData(0, 3f)]
+ [InlineData(0, 4f)]
+ public void Reversed_Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Complete_When_AnimationDuration_Is_Reached(int expectedTextureRegionIndex, float time)
+ {
+ var textureRegion2D1 = new TextureRegion2D("Region 1", null, new Rectangle());
+ var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle());
+
+ var textureRegions = new[] { textureRegion2D1, textureRegion2D2 };
+
+ var spriteSheetAnimationData = new SpriteSheetAnimationData(
+ new[] { 0, 1 },
+ 1,
+ false,
+ true
+ );
+
+ var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData);
+ var isCompleteFired = false;
+ spriteSheetAnimation.OnCompleted += () => isCompleteFired = true;
+
+ spriteSheetAnimation.Play();
+ var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time));
+
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame);
+ Assert.True(spriteSheetAnimation.IsComplete);
+ Assert.True(isCompleteFired);
+ }
+
+ [Fact]
+ public void Reversed_Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Complete_When_AnimationDuration_Is_Reached_Over_Multiple_Updates()
+ {
+ var textureRegion2D1 = new TextureRegion2D("Region 1", null, new Rectangle());
+ var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle());
+
+ var textureRegions = new[] { textureRegion2D1, textureRegion2D2 };
+
+ var spriteSheetAnimationData = new SpriteSheetAnimationData(
+ new[] { 0, 1 },
+ 1,
+ false,
+ true
+ );
+
+ var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData);
+ var isCompleteFired = false;
+ spriteSheetAnimation.OnCompleted += () => isCompleteFired = true;
+
+ spriteSheetAnimation.Play();
+
+ var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(0));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame);
+ Assert.True(spriteSheetAnimation.IsComplete);
+ Assert.True(isCompleteFired);
+
+ isCompleteFired = false; // Reset isCompleteFired for next execution
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame);
+ Assert.True(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired); // Event is not fired again as animation was already completed;
+ }
+
+ [Theory]
+ [InlineData(0, 0)]
+ [InlineData(0, 0.9f)]
+ [InlineData(1, 1f)]
+ [InlineData(1, 1.9f)]
+ [InlineData(0, 2f)]
+ [InlineData(0, 2.9f)]
+ [InlineData(1, 3f)]
+ [InlineData(0, 4f)]
+ [InlineData(1, 5f)]
+ [InlineData(0, 6f)]
+ [InlineData(1, 7f)]
+ [InlineData(0, 8f)]
+ public void PingPong_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete(int expectedTextureRegionIndex, float time)
+ {
+ var textureRegion2D1 = new TextureRegion2D("Region 1", null, new Rectangle());
+ var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle());
+
+ var textureRegions = new[] { textureRegion2D1, textureRegion2D2 };
+
+ var spriteSheetAnimationData = new SpriteSheetAnimationData(
+ new[] { 0, 1 },
+ 1,
+ true,
+ false,
+ true
+ );
+
+ var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData);
+ var isCompleteFired = false;
+ spriteSheetAnimation.OnCompleted += () => isCompleteFired = true;
+
+ spriteSheetAnimation.Play();
+ var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time));
+
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+ }
+
+ [Fact]
+ public void PingPong_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete_Over_Multiple_Updates()
+ {
+ var textureRegion2D1 = new TextureRegion2D("Region 1", null, new Rectangle());
+ var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle());
+
+ var textureRegions = new[] { textureRegion2D1, textureRegion2D2 };
+
+ var spriteSheetAnimationData = new SpriteSheetAnimationData(
+ new[] { 0, 1 },
+ 1,
+ true,
+ false,
+ true
+ );
+
+ var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData);
+ var isCompleteFired = false;
+ spriteSheetAnimation.OnCompleted += () => isCompleteFired = true;
+
+ spriteSheetAnimation.Play();
+ var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(0));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+ }
+
+ [Theory]
+ [InlineData(0, 0.9f)]
+ [InlineData(1, 1f)]
+ [InlineData(1, 1.9f)]
+ [InlineData(0, 2f)]
+ [InlineData(0, 2.9f)]
+ public void PingPong_Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete(int expectedTextureRegionIndex, float time)
+ {
+ var textureRegion2D1 = new TextureRegion2D("Region 1", null, new Rectangle());
+ var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle());
+
+ var textureRegions = new[] { textureRegion2D1, textureRegion2D2 };
+
+ var spriteSheetAnimationData = new SpriteSheetAnimationData(
+ new[] { 0, 1 },
+ 1,
+ false,
+ false,
+ true
+ );
+
+ var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData);
+ var isCompleteFired = false;
+ spriteSheetAnimation.OnCompleted += () => isCompleteFired = true;
+
+ spriteSheetAnimation.Play();
+ var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time));
+
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+ }
+
+ [Theory]
+ [InlineData(0, 3f)]
+ [InlineData(0, 4f)]
+ [InlineData(0, 5f)]
+ public void PingPong_Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Complete_When_AnimationDuration_Is_Reached(int expectedTextureRegionIndex, float time)
+ {
+ var textureRegion2D1 = new TextureRegion2D("Region 1", null, new Rectangle());
+ var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle());
+
+ var textureRegions = new[] { textureRegion2D1, textureRegion2D2 };
+
+ var spriteSheetAnimationData = new SpriteSheetAnimationData(
+ new[] { 0, 1 },
+ 1,
+ false,
+ false,
+ true
+ );
+
+ var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData);
+ var isCompleteFired = false;
+ spriteSheetAnimation.OnCompleted += () => isCompleteFired = true;
+
+ spriteSheetAnimation.Play();
+ var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time));
+
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame);
+ Assert.True(spriteSheetAnimation.IsComplete);
+ Assert.True(isCompleteFired);
+ }
+
+ [Fact]
+ public void PingPong_Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Complete_When_AnimationDuration_Is_Reached_Over_Multiple_Updates()
+ {
+ var textureRegion2D1 = new TextureRegion2D("Region 1", null, new Rectangle());
+ var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle());
+ var textureRegion2D3 = new TextureRegion2D("Region 3", null, new Rectangle());
+
+ var textureRegions = new[] { textureRegion2D1, textureRegion2D2, textureRegion2D3 };
+
+ var spriteSheetAnimationData = new SpriteSheetAnimationData(
+ new[] { 0, 1, 2 },
+ 1,
+ false,
+ false,
+ true
+ );
+
+ var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData);
+
+ var isCompleteFired = false;
+ spriteSheetAnimation.OnCompleted += () => isCompleteFired = true;
+
+ spriteSheetAnimation.Play();
+
+ var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(0));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[2], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame);
+ Assert.True(spriteSheetAnimation.IsComplete);
+ Assert.True(isCompleteFired);
+
+ isCompleteFired = false; // Reset isCompleteFired for next execution
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame);
+ Assert.True(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired); // Event is not fired again as animation was already completed
+ }
+
+ [Theory]
+ [InlineData(1, 0)]
+ [InlineData(1, 0.9f)]
+ [InlineData(0, 1f)]
+ [InlineData(0, 1.9f)]
+ [InlineData(1, 2f)]
+ [InlineData(1, 2.9f)]
+ [InlineData(0, 3f)]
+ [InlineData(1, 4f)]
+ [InlineData(0, 5f)]
+ [InlineData(1, 6f)]
+ [InlineData(0, 7f)]
+ [InlineData(1, 8f)]
+ public void Reversed_PingPong_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete(int expectedTextureRegionIndex, float time)
+ {
+ var textureRegion2D1 = new TextureRegion2D("Region 1", null, new Rectangle());
+ var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle());
+
+ var textureRegions = new[] { textureRegion2D1, textureRegion2D2 };
+
+ var spriteSheetAnimationData = new SpriteSheetAnimationData(
+ new[] { 0, 1 },
+ 1,
+ true,
+ true,
+ true
+ );
+
+ var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData);
+ var isCompleteFired = false;
+ spriteSheetAnimation.OnCompleted += () => isCompleteFired = true;
+
+ spriteSheetAnimation.Play();
+ var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time));
+
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+ }
+
+ [Fact]
+ public void Reversed_PingPong_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete_Over_Multiple_Updates()
+ {
+ var textureRegion2D1 = new TextureRegion2D("Region 1", null, new Rectangle());
+ var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle());
+
+ var textureRegions = new[] { textureRegion2D1, textureRegion2D2 };
+
+ var spriteSheetAnimationData = new SpriteSheetAnimationData(
+ new[] { 0, 1 },
+ 1,
+ true,
+ true,
+ true
+ );
+
+ var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData);
+ var isCompleteFired = false;
+ spriteSheetAnimation.OnCompleted += () => isCompleteFired = true;
+
+ spriteSheetAnimation.Play();
+ var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(0));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+ }
+
+ [Theory]
+ [InlineData(1, 0.9f)]
+ [InlineData(0, 1f)]
+ [InlineData(0, 1.9f)]
+ [InlineData(1, 2f)]
+ [InlineData(1, 2.9f)]
+ public void Reversed_PingPong_Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Not_Complete(int expectedTextureRegionIndex, float time)
+ {
+ var textureRegion2D1 = new TextureRegion2D("Region 1", null, new Rectangle());
+ var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle());
+
+ var textureRegions = new[] { textureRegion2D1, textureRegion2D2 };
+
+ var spriteSheetAnimationData = new SpriteSheetAnimationData(
+ new[] { 0, 1 },
+ 1,
+ false,
+ true,
+ true
+ );
+
+ var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData);
+ var isCompleteFired = false;
+ spriteSheetAnimation.OnCompleted += () => isCompleteFired = true;
+
+ spriteSheetAnimation.Play();
+ var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time));
+
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+ }
+
+ [Theory]
+ [InlineData(1, 3f)]
+ [InlineData(1, 4f)]
+ [InlineData(1, 5f)]
+ public void Reversed_PingPong_Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Complete_When_AnimationDuration_Is_Reached(int expectedTextureRegionIndex, float time)
+ {
+ var textureRegion2D1 = new TextureRegion2D("Region 1", null, new Rectangle());
+ var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle());
+
+ var textureRegions = new[] { textureRegion2D1, textureRegion2D2 };
+
+ var spriteSheetAnimationData = new SpriteSheetAnimationData(
+ new[] { 0, 1 },
+ 1,
+ false,
+ true,
+ true
+ );
+
+ var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData);
+ var isCompleteFired = false;
+ spriteSheetAnimation.OnCompleted += () => isCompleteFired = true;
+
+ spriteSheetAnimation.Play();
+ var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(time));
+
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[expectedTextureRegionIndex], spriteSheetAnimation.CurrentFrame);
+ Assert.True(spriteSheetAnimation.IsComplete);
+ Assert.True(isCompleteFired);
+ }
+
+ [Fact]
+ public void Reversed_PingPong_Non_Looping_SpriteSheetAnimation_Should_Return_Correct_Frame_And_Complete_When_AnimationDuration_Is_Reached_Over_Multiple_Updates()
+ {
+ var textureRegion2D1 = new TextureRegion2D("Region 1", null, new Rectangle());
+ var textureRegion2D2 = new TextureRegion2D("Region 2", null, new Rectangle());
+ var textureRegion2D3 = new TextureRegion2D("Region 3", null, new Rectangle());
+
+ var textureRegions = new[] { textureRegion2D1, textureRegion2D2, textureRegion2D3 };
+
+ var spriteSheetAnimationData = new SpriteSheetAnimationData(
+ new[] { 0, 1, 2 },
+ 1,
+ false,
+ true,
+ true
+ );
+
+ var spriteSheetAnimation = new SpriteSheetAnimation("Test", textureRegions, spriteSheetAnimationData);
+
+ var isCompleteFired = false;
+ spriteSheetAnimation.OnCompleted += () => isCompleteFired = true;
+
+ spriteSheetAnimation.Play();
+
+ var gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(0));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[2], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[0], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[1], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[2], spriteSheetAnimation.CurrentFrame);
+ Assert.False(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired);
+
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[2], spriteSheetAnimation.CurrentFrame);
+ Assert.True(spriteSheetAnimation.IsComplete);
+ Assert.True(isCompleteFired);
+
+ isCompleteFired = false; // Reset isCompleteFired for next execution
+ gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ spriteSheetAnimation.Update(gameTime);
+
+ Assert.Equal(textureRegions[2], spriteSheetAnimation.CurrentFrame);
+ Assert.True(spriteSheetAnimation.IsComplete);
+ Assert.False(isCompleteFired); // Event is not fired again as animation was already completed
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Sprites/SpriteTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Sprites/SpriteTests.cs
new file mode 100644
index 0000000..307e272
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Sprites/SpriteTests.cs
@@ -0,0 +1,91 @@
+//using Microsoft.Xna.Framework;
+//using Microsoft.Xna.Framework.Graphics;
+//using MonoGame.Extended.Sprites;
+//using MonoGame.Extended.TextureAtlases;
+//using NSubstitute;
+//using Xunit;
+
+//namespace MonoGame.Extended.Tests.Sprites
+//{
+//
+// public class SpriteTests
+// {
+// [Fact]
+// public void Sprite_BoundingRectangleAfterPosition_Test()
+// {
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var texture = Substitute.For<Texture2D>(graphicsDevice, 50, 200);
+// var sprite = new Sprite(texture);
+
+// Assert.Equal(new RectangleF(375, 140, 50, 200), sprite.GetBoundingRectangle(new Vector2(400, 240), 0, Vector2.One));
+// }
+
+// [Fact]
+// public void Sprite_BoundingRectangleAfterOrigin_Test()
+// {
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var texture = Substitute.For<Texture2D>(graphicsDevice, 50, 200);
+// var sprite = new Sprite(texture) { OriginNormalized = new Vector2(1.0f, 1.0f) };
+
+// Assert.Equal(new RectangleF(-50, -200, 50, 200), sprite.GetBoundingRectangle(Vector2.Zero, 0, Vector2.One));
+// }
+
+// [Fact]
+// public void Sprite_BoundingRectangleAfterScale_Test()
+// {
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var texture = Substitute.For<Texture2D>(graphicsDevice, 50, 200);
+// var sprite = new Sprite(texture);
+
+// Assert.Equal(new RectangleF(-50, -200, 100, 400), sprite.GetBoundingRectangle(Vector2.Zero, 0, Vector2.One * 2.0f));
+// }
+
+// [Fact]
+// public void Sprite_BoundingRectangleAfterRotation_Test()
+// {
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var texture = Substitute.For<Texture2D>(graphicsDevice, 50, 200);
+// var sprite = new Sprite(texture);
+
+// AssertExtensions.AreApproximatelyEqual(new RectangleF(-100, -25, 200, 50), sprite.GetBoundingRectangle(Vector2.Zero, MathHelper.ToRadians(90), Vector2.One * 2.0f));
+// }
+
+// [Fact]
+// public void Sprite_TextureRegionIsFullTextureWhenTextureConstructorIsUsed_Test()
+// {
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var texture = Substitute.For<Texture2D>(graphicsDevice, 100, 200);
+// var sprite = new Sprite(texture);
+
+// Assert.Equal(new Rectangle(0, 0, 100, 200), sprite.TextureRegion.Bounds);
+// }
+
+// [Fact]
+// public void Sprite_DefaultOriginIsCentre_Test()
+// {
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var texture = Substitute.For<Texture2D>(graphicsDevice, 100, 200);
+// var sprite = new Sprite(texture);
+
+// Assert.Equal(new Vector2(0.5f, 0.5f), sprite.OriginNormalized);
+// Assert.Equal(new Vector2(50, 100), sprite.Origin);
+// }
+
+// [Fact]
+// public void Sprite_PreserveNormalizedOriginWhenTextureRegionChanges_Test()
+// {
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var texture = Substitute.For<Texture2D>(graphicsDevice, 100, 100);
+// var textureRegion = new TextureRegion2D(texture, 10, 20, 30, 40);
+// var sprite = new Sprite(textureRegion);
+
+// Assert.Equal(new Vector2(0.5f, 0.5f), sprite.OriginNormalized);
+// Assert.Equal(new Vector2(15, 20), sprite.Origin);
+
+// sprite.TextureRegion = new TextureRegion2D(texture, 30, 40, 50, 60);
+
+// Assert.Equal(new Vector2(0.5f, 0.5f), sprite.OriginNormalized);
+// Assert.Equal(new Vector2(25, 30), sprite.Origin);
+// }
+// }
+//}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/TestGame.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/TestGame.cs
new file mode 100644
index 0000000..dad6e8a
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/TestGame.cs
@@ -0,0 +1,21 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tests
+{
+ public class TestGame : Game
+ {
+ private readonly GraphicsDeviceManager _graphicsDeviceManager;
+
+ public TestGame()
+ {
+ _graphicsDeviceManager = new GraphicsDeviceManager(this);
+ RunOneFrame();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ _graphicsDeviceManager.Dispose();
+ base.Dispose(disposing);
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/TestGraphicsDevice.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/TestGraphicsDevice.cs
new file mode 100644
index 0000000..8a8741b
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/TestGraphicsDevice.cs
@@ -0,0 +1,12 @@
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGame.Extended.Tests
+{
+ public class TestGraphicsDevice : GraphicsDevice
+ {
+ public TestGraphicsDevice()
+ : base(GraphicsAdapter.DefaultAdapter, GraphicsProfile.Reach, new PresentationParameters())
+ {
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/TestHelper.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/TestHelper.cs
new file mode 100644
index 0000000..b6a02bb
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/TestHelper.cs
@@ -0,0 +1,27 @@
+//using Microsoft.Xna.Framework;
+//using Microsoft.Xna.Framework.Graphics;
+//using Xunit;
+
+//namespace MonoGame.Extended.Tests
+//{
+// public static class TestHelper
+// {
+// public static void AreEqual(Vector3 a, Vector3 b, double delta)
+// {
+// Assert.Equal(a.X, b.X, delta);
+// Assert.Equal(a.Y, b.Y, delta);
+// Assert.Equal(a.Z, b.Z, delta);
+// }
+
+// public static GraphicsDevice CreateGraphicsDevice()
+// {
+// return new GraphicsDevice(
+// GraphicsAdapter.DefaultAdapter,
+// GraphicsProfile.HiDef,
+// new PresentationParameters())
+// {
+// Viewport = new Viewport(0, 0, 800, 480)
+// };
+// }
+// }
+//} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/TextureAtlases/TextureAtlasTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/TextureAtlases/TextureAtlasTests.cs
new file mode 100644
index 0000000..13d9dfc
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/TextureAtlases/TextureAtlasTests.cs
@@ -0,0 +1,236 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.TextureAtlases;
+using Xunit;
+
+namespace MonoGame.Extended.Tests.TextureAtlases
+{
+ //public class TextureAtlasTests : IDisposable
+ //{
+ // private readonly TestGame _game;
+
+ // public TextureAtlasTests()
+ // {
+ // _game = new TestGame();
+ // }
+
+ // public void Dispose()
+ // {
+ // _game.Dispose();
+ // }
+
+ // [Fact]
+ // public void TextureAtlas_CreateRegion_Test()
+ // {
+ // using (var texture = new Texture2D(_game.GraphicsDevice, 100, 200))
+ // {
+ // var atlas = new TextureAtlas(null, texture);
+
+ // var region = atlas.CreateRegion("region0", 10, 20, 30, 40);
+
+ // Assert.Same(texture, region.Texture);
+ // Assert.Equal(10, region.X);
+ // Assert.Equal(20, region.Y);
+ // Assert.Equal(30, region.Width);
+ // Assert.Equal(40, region.Height);
+ // }
+ // }
+
+ // [Fact]
+ // public void TextureAtlas_GetRegionsByIndex_Test()
+ // {
+ // using (var texture = new Texture2D(_game.GraphicsDevice, 100, 200))
+ // {
+ // var atlas = new TextureAtlas(null, texture);
+
+ // var region0 = atlas.CreateRegion("region0", 10, 20, 30, 40);
+ // var region1 = atlas.CreateRegion("region1", 50, 60, 35, 45);
+
+ // Assert.Same(region0, atlas[0]);
+ // Assert.Same(region1, atlas[1]);
+ // Assert.Same(region0, atlas.GetRegion(0));
+ // Assert.Same(region1, atlas.GetRegion(1));
+ // }
+ // }
+
+
+ // [Fact]
+ // public void TextureAtlas_GetRegionsByName_Test()
+ // {
+ // using (var texture = new Texture2D(_game.GraphicsDevice, 100, 200))
+ // {
+ // var atlas = new TextureAtlas(null, texture);
+
+ // var region0 = atlas.CreateRegion("region0", 10, 20, 30, 40);
+ // var region1 = atlas.CreateRegion("region1", 50, 60, 35, 45);
+
+ // Assert.Same(region0, atlas["region0"]);
+ // Assert.Same(region1, atlas["region1"]);
+ // Assert.Same(region0, atlas.GetRegion("region0"));
+ // Assert.Same(region1, atlas.GetRegion("region1"));
+ // }
+ // }
+
+ // [Fact]
+ // public void TextureAtlas_RemoveRegions_Test()
+ // {
+ // using (var texture = new Texture2D(_game.GraphicsDevice, 100, 200))
+ // {
+ // var atlas = new TextureAtlas(null, texture);
+
+ // var region0 = atlas.CreateRegion("region0", 10, 20, 30, 40);
+ // var region1 = atlas.CreateRegion("region1", 50, 60, 35, 45);
+ // var region2 = atlas.CreateRegion("region2", 32, 33, 34, 35);
+
+ // Assert.Same(texture, atlas.Texture);
+ // Assert.Equal(3, atlas.RegionCount);
+ // Assert.Equal(atlas.RegionCount, atlas.Regions.Count());
+ // Assert.Same(region1, atlas[1]);
+
+ // atlas.RemoveRegion(1);
+
+ // Assert.Equal(2, atlas.Regions.Count());
+ // Assert.Same(region0, atlas[0]);
+ // Assert.Same(region2, atlas[1]);
+
+ // atlas.RemoveRegion("region0");
+
+ // Assert.Single(atlas.Regions);
+ // Assert.Same(region2, atlas[0]);
+ // }
+ // }
+
+ // [Fact]
+ // public void TextureAtlas_CreateRegionThatAlreadyExistsThrowsException_Test()
+ // {
+ // using (var texture = new Texture2D(_game.GraphicsDevice, 100, 200))
+ // {
+ // var atlas = new TextureAtlas(null, texture);
+
+ // atlas.CreateRegion("region0", 10, 20, 30, 40);
+ // Assert.Throws<InvalidOperationException>(() => atlas.CreateRegion("region0", 50, 60, 35, 45));
+ // }
+ // }
+
+ // [Fact]
+ // public void TextureAtlas_GetRegion_InvalidIndexThrowsException_Test()
+ // {
+ // using (var texture = new Texture2D(_game.GraphicsDevice, 100, 200))
+ // {
+ // var atlas = new TextureAtlas(null, texture);
+
+ // atlas.CreateRegion("region0", 10, 20, 30, 40);
+ // Assert.Throws<IndexOutOfRangeException>(() => atlas.GetRegion(-1));
+ // }
+ // }
+
+ // [Fact]
+ // public void TextureAtlas_GetRegion_InvalidNameThrowsException_Test()
+ // {
+ // using (var texture = new Texture2D(_game.GraphicsDevice, 100, 200))
+ // {
+ // var atlas = new TextureAtlas(null, texture);
+
+ // atlas.CreateRegion("region0", 10, 20, 30, 40);
+ // Assert.Throws<KeyNotFoundException>(() => atlas.GetRegion("region1"));
+ // }
+ // }
+
+ // [Fact]
+ // public void TextureAtlas_EnumerateRegions_Test()
+ // {
+ // using (var texture = new Texture2D(_game.GraphicsDevice, 100, 200))
+ // {
+ // var atlas = new TextureAtlas(null, texture);
+
+ // var regions = new TextureRegion2D[3];
+ // regions[0] = atlas.CreateRegion("region0", 10, 20, 30, 40);
+ // regions[1] = atlas.CreateRegion("region1", 50, 60, 35, 45);
+ // regions[2] = atlas.CreateRegion("region2", 32, 33, 34, 35);
+ // var index = 0;
+
+ // foreach (var region in atlas)
+ // {
+ // Assert.Same(region, regions[index]);
+ // index++;
+ // }
+ // }
+ // }
+
+ // [Fact]
+ // public void TextureAtlas_Create_WithDefaultParameters_Test()
+ // {
+ // using (var texture = new Texture2D(_game.GraphicsDevice, 50, 100) {Name = "testTexture"})
+ // {
+ // var atlas = TextureAtlas.Create(null, texture, 25, 50);
+
+ // Assert.Equal(4, atlas.RegionCount);
+ // Assert.True(atlas.Regions.All(i => i.Width == 25));
+ // Assert.True(atlas.Regions.All(i => i.Height == 50));
+ // Assert.True(atlas.Regions.All(i => ReferenceEquals(i.Texture, texture)));
+ // Assert.True(atlas.Regions.All(i => i.Name.StartsWith(texture.Name)));
+ // }
+ // }
+
+ // [Fact]
+ // public void TextureAtlas_Create_WithMaxRegionCount_Test()
+ // {
+ // using (var texture = new Texture2D(_game.GraphicsDevice, 64, 64))
+ // {
+ // var atlas = TextureAtlas.Create(null, texture, 32, 32, maxRegionCount: 3);
+
+ // Assert.Equal(3, atlas.RegionCount);
+ // }
+ // }
+
+ // [Fact]
+ // public void TextureAtlas_Create_WithMargin_Test()
+ // {
+ // using (var texture = new Texture2D(_game.GraphicsDevice, 24, 24))
+ // {
+ // var atlas = TextureAtlas.Create(null, texture, 10, 10, margin: 2);
+
+ // Assert.Equal(4, atlas.RegionCount);
+ // Assert.True(atlas.Regions.All(i => i.Width == 10 && i.Height == 10));
+ // Assert.Equal(2, atlas[0].X);
+ // Assert.Equal(2, atlas[0].Y);
+ // Assert.Equal(12, atlas[3].X);
+ // Assert.Equal(12, atlas[3].Y);
+ // }
+ // }
+
+ // [Fact]
+ // public void TextureAtlas_Create_WithSpacing_Test()
+ // {
+ // using (var texture = new Texture2D(_game.GraphicsDevice, 24, 24))
+ // {
+ // var atlas = TextureAtlas.Create(null, texture, 10, 10, spacing: 2);
+
+ // Assert.Equal(4, atlas.RegionCount);
+ // Assert.True(atlas.Regions.All(i => i.Width == 10 && i.Height == 10));
+ // Assert.Equal(0, atlas[0].X);
+ // Assert.Equal(0, atlas[0].Y);
+ // Assert.Equal(12, atlas[3].X);
+ // Assert.Equal(12, atlas[3].Y);
+ // }
+ // }
+
+ // [Fact]
+ // public void TextureAtlas_Create_WithMarginAndSpacing_Test()
+ // {
+ // using (var texture = new Texture2D(_game.GraphicsDevice, 28, 28))
+ // {
+ // var atlas = TextureAtlas.Create(null, texture, 10, 10, margin: 3, spacing: 2);
+
+ // Assert.Equal(4, atlas.RegionCount);
+ // Assert.True(atlas.Regions.All(i => i.Width == 10 && i.Height == 10));
+ // Assert.Equal(3, atlas[0].X);
+ // Assert.Equal(3, atlas[0].Y);
+ // Assert.Equal(15, atlas[3].X);
+ // Assert.Equal(15, atlas[3].Y);
+ // }
+ // }
+ //}
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/TextureAtlases/TextureRegion2DTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/TextureAtlases/TextureRegion2DTests.cs
new file mode 100644
index 0000000..1274b10
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/TextureAtlases/TextureRegion2DTests.cs
@@ -0,0 +1,40 @@
+//using Microsoft.Xna.Framework.Graphics;
+//using MonoGame.Extended.TextureAtlases;
+//using Xunit;
+
+//namespace MonoGame.Extended.Tests.TextureAtlases
+//{
+//
+// public class TextureRegion2DTests
+// {
+// [Fact]
+// public void TextureRegion2D_FromTexture_Test()
+// {
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var texture = new Texture2D(graphicsDevice, 100, 200);
+// var textureRegion = new TextureRegion2D(texture);
+
+// Assert.AreSame(texture, textureRegion.Texture);
+// Assert.Equal(0, textureRegion.X);
+// Assert.Equal(0, textureRegion.Y);
+// Assert.Equal(100, textureRegion.Width);
+// Assert.Equal(200, textureRegion.Height);
+// Assert.IsNull(textureRegion.Tag);
+// }
+
+// [Fact]
+// public void TextureRegion2D_Specified_Test()
+// {
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var texture = new Texture2D(graphicsDevice, 100, 200);
+// var textureRegion = new TextureRegion2D(texture, 10, 20, 30, 40);
+
+// Assert.AreSame(texture, textureRegion.Texture);
+// Assert.Equal(10, textureRegion.X);
+// Assert.Equal(20, textureRegion.Y);
+// Assert.Equal(30, textureRegion.Width);
+// Assert.Equal(40, textureRegion.Height);
+// Assert.IsNull(textureRegion.Tag);
+// }
+// }
+//} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Vector2ExtensionsTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Vector2ExtensionsTests.cs
new file mode 100644
index 0000000..aa1d889
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/Vector2ExtensionsTests.cs
@@ -0,0 +1,101 @@
+using Microsoft.Xna.Framework;
+using Xunit;
+
+namespace MonoGame.Extended.Tests
+{
+ public class Vector2ExtensionsTests
+ {
+ [Fact]
+ public void Vector2_EqualsWithTolerence_Test()
+ {
+ var a = new Vector2(1f, 1f);
+ var b = new Vector2(1.0000001f, 1.0000001f);
+
+ Assert.False(a.Equals(b));
+ Assert.True(a.EqualsWithTolerence(b));
+ }
+
+ [Fact]
+ public void Vector2_NormalizedCopy_Test()
+ {
+ var a = new Vector2(5, -10);
+ var b = a.NormalizedCopy();
+
+ Assert.True(new Vector2(0.4472136f, -0.8944272f).EqualsWithTolerence(b));
+ }
+
+ [Fact]
+ public void Vector2_Perpendicular_Test()
+ {
+ // http://mathworld.wolfram.com/PerpendicularVector.html
+ var a = new Vector2(5, -10);
+ var b = a.PerpendicularClockwise();
+ var c = a.PerpendicularCounterClockwise();
+
+ Assert.Equal(new Vector2(-10, -5), b);
+ Assert.Equal(new Vector2(10, 5), c);
+ }
+
+ [Fact]
+ public void Vector2_Rotate_90_Degrees_Test()
+ {
+ var a = new Vector2(0, -10);
+ var b = a.Rotate(MathHelper.ToRadians(90));
+
+ Assert.True(new Vector2(10, 0).EqualsWithTolerence(b));
+ }
+
+ [Fact]
+ public void Vector2_Rotate_360_Degrees_Test()
+ {
+ var a = new Vector2(0, 10);
+ var b = a.Rotate(MathHelper.ToRadians(360));
+
+ Assert.True(new Vector2(0, 10).EqualsWithTolerence(b));
+ }
+
+ [Fact]
+ public void Vector2_Rotate_45_Degrees_Test()
+ {
+ var a = new Vector2(0, -10);
+ var b = a.Rotate(MathHelper.ToRadians(45));
+
+ Assert.True(new Vector2(7.071068f, -7.071068f).EqualsWithTolerence(b));
+ }
+
+ [Fact]
+ public void Vector2_Truncate_Test()
+ {
+ var a = new Vector2(10, 10);
+ var b = a.Truncate(5);
+
+ Assert.Equal(5f, b.Length(), 3);
+ }
+
+ [Fact]
+ public void Vector2_IsNaN_Test()
+ {
+ var a = new Vector2(float.NaN, 10);
+ var b = new Vector2(10, float.NaN);
+ var c = new Vector2(float.NaN, float.NaN);
+ var d = new Vector2(10, 10);
+
+ Assert.True(a.IsNaN());
+ Assert.True(b.IsNaN());
+ Assert.True(c.IsNaN());
+ Assert.False(d.IsNaN());
+ }
+
+ [Fact]
+ public void Vector2_ToAngle_Test()
+ {
+ var a = new Vector2(0, -10);
+ var b = new Vector2(10, 0);
+ var c = -Vector2.UnitY.Rotate(MathHelper.ToRadians(45));
+
+ Assert.Equal(MathHelper.ToRadians(0), a.ToAngle());
+ Assert.Equal(MathHelper.ToRadians(90), b.ToAngle());
+ Assert.Equal(MathHelper.ToRadians(45), c.ToAngle());
+ }
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/ViewportAdapters/BoxingViewportAdapterTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/ViewportAdapters/BoxingViewportAdapterTests.cs
new file mode 100644
index 0000000..665fea4
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/ViewportAdapters/BoxingViewportAdapterTests.cs
@@ -0,0 +1,41 @@
+//using Microsoft.Xna.Framework.Graphics;
+//using MonoGame.Extended.ViewportAdapters;
+//using Xunit;
+
+//namespace MonoGame.Extended.Tests.ViewportAdapters
+//{
+//
+// public class BoxingViewportAdapterTests
+// {
+// [Fact]
+// public void BoxingViewportAdapter_Letterbox_Test()
+// {
+// var gameWindow = new MockGameWindow();
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var viewportAdapter = new BoxingViewportAdapter(gameWindow, graphicsDevice, 800, 480);
+
+// graphicsDevice.Viewport = new Viewport(0, 0, 1024, 768);
+// viewportAdapter.Reset();
+
+// Assert.Equal(1024, graphicsDevice.Viewport.Width);
+// Assert.Equal(614, graphicsDevice.Viewport.Height);
+// Assert.Equal(BoxingMode.Letterbox, viewportAdapter.BoxingMode);
+// }
+
+// [Fact]
+// public void BoxingViewportAdapter_Pillarbox_Test()
+// {
+// var gameWindow = new MockGameWindow();
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var viewportAdapter = new BoxingViewportAdapter(gameWindow, graphicsDevice, 800, 480);
+
+// graphicsDevice.Viewport = new Viewport(0, 0, 900, 500);
+// viewportAdapter.Reset();
+
+// Assert.Equal(833, graphicsDevice.Viewport.Width);
+// Assert.Equal(500, graphicsDevice.Viewport.Height);
+// Assert.Equal(BoxingMode.Pillarbox, viewportAdapter.BoxingMode);
+// }
+
+// }
+//} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/ViewportAdapters/DefaultViewportAdapterTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/ViewportAdapters/DefaultViewportAdapterTests.cs
new file mode 100644
index 0000000..1262119
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/ViewportAdapters/DefaultViewportAdapterTests.cs
@@ -0,0 +1,26 @@
+//using Microsoft.Xna.Framework;
+//using Microsoft.Xna.Framework.Graphics;
+//using MonoGame.Extended.ViewportAdapters;
+//using Xunit;
+
+//namespace MonoGame.Extended.Tests.ViewportAdapters
+//{
+//
+// public class DefaultViewportAdapterTests
+// {
+// [Fact]
+// public void DefaultViewportAdapter_Test()
+// {
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var viewportAdapter = new DefaultViewportAdapter(graphicsDevice);
+
+// graphicsDevice.Viewport = new Viewport(0, 0, 1024, 768);
+
+// Assert.Equal(1024, viewportAdapter.ViewportWidth);
+// Assert.Equal(768, viewportAdapter.ViewportHeight);
+// Assert.Equal(viewportAdapter.ViewportWidth, viewportAdapter.VirtualWidth);
+// Assert.Equal(viewportAdapter.ViewportHeight, viewportAdapter.VirtualHeight);
+// Assert.Equal(Matrix.Identity, viewportAdapter.GetScaleMatrix());
+// }
+// }
+//} \ No newline at end of file
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/WithinDeltaEqualityComparer.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/WithinDeltaEqualityComparer.cs
new file mode 100644
index 0000000..6ab17de
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tests/WithinDeltaEqualityComparer.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+
+namespace MonoGame.Extended.Tests;
+
+public class WithinDeltaEqualityComparer : IEqualityComparer<float>
+{
+ private readonly float _delta;
+
+ public WithinDeltaEqualityComparer(float delta)
+ {
+ _delta = delta;
+ }
+
+ public bool Equals(float x, float y)
+ {
+ return Math.Abs(x - y) < _delta;
+ }
+
+ public int GetHashCode(float obj)
+ {
+ return obj.GetHashCode();
+ }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tiled.Tests/FullMapRendererTest.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tiled.Tests/FullMapRendererTest.cs
new file mode 100644
index 0000000..3f8ae1d
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tiled.Tests/FullMapRendererTest.cs
@@ -0,0 +1,291 @@
+//using Microsoft.Xna.Framework;
+//using Microsoft.Xna.Framework.Graphics;
+//using MonoGame.Extended.Shapes;
+//using MonoGame.Extended.TextureAtlases;
+//using MonoGame.Extended.Tiled;
+//using MonoGame.Extended.Tiled.Graphics;
+//using NSubstitute;
+//using Xunit;
+
+//namespace MonoGame.Extended.Tests.Tiled.Renderers
+//{
+//
+// public class FullMapRendererTest
+// {
+// [Fact]
+// public void Draw_MapObjectLayer_MissingGID_NoGroups()
+// {
+// var gd = TestHelper.CreateGraphicsDevice();
+// var r = new MockRenderer(gd);
+// var texture = Substitute.For<Texture2D>(gd, 64, 64);
+
+// var m = new TiledMap("test", 2, 2, 32, 32);
+// m.CreateTileset(texture, 0, 32, 32, 4);
+
+// IShapeF shape = new RectangleF(1, 1, 1, 1);
+// TiledObject[] objs =
+// {
+// new TiledObject(TiledObjectType.Tile, 1, null, shape, 1, 1) { IsVisible = true },
+// };
+
+// var layer = new TiledObjectLayer("object", objs);
+// m.AddLayer(layer);
+
+// r.Map = m;
+
+// r.Draw(new Matrix());
+
+// Assert.IsNull(gd.Indices);
+// }
+
+// [Fact]
+// public void Draw_MapObjectLayer_ShapeObject_NoGroups()
+// {
+// var gd = TestHelper.CreateGraphicsDevice();
+// var r = new MockRenderer(gd);
+// var texture = Substitute.For<Texture2D>(gd, 64, 64);
+
+// var m = new TiledMap("test", 2, 2, 32, 32);
+// m.CreateTileset(texture, 0, 32, 32, 4);
+
+// IShapeF shape = new RectangleF(1, 1, 1, 1);
+// TiledObject[] objs =
+// {
+// new TiledObject(TiledObjectType.Rectangle, 1, 1, shape, 1, 1) { IsVisible = true },
+// };
+
+// var layer = new TiledObjectLayer("object", objs);
+// m.AddLayer(layer);
+
+// r.Map = m;
+
+// r.Draw(new Matrix());
+
+// Assert.IsNull(gd.Indices);
+// }
+
+// [Fact]
+// public void Draw_MapObjectLayer_TileObject_OneGroup()
+// {
+// var gd = TestHelper.CreateGraphicsDevice();
+// var r = new MockRenderer(gd);
+// var texture = Substitute.For<Texture2D>(gd, 64, 64);
+
+// var m = new TiledMap("test", 2, 2, 32, 32);
+// m.CreateTileset(texture, 0, 32, 32, 4);
+
+// IShapeF shape = new RectangleF(1, 1, 1, 1);
+// TiledObject[] objs =
+// {
+// new TiledObject(TiledObjectType.Tile, 1, 1, shape, 1, 1) { IsVisible = true },
+// };
+
+// var layer = new TiledObjectLayer("object", objs);
+// m.AddLayer(layer);
+
+// r.Map = m;
+
+// r.Draw(new Matrix());
+
+// Assert.NotNull(gd.Indices);
+// Assert.Equal(6, gd.Indices.IndexCount);
+// }
+
+// [Fact]
+// public void Draw_MapObjectLayer_NotVisible_NoGroups()
+// {
+// var gd = TestHelper.CreateGraphicsDevice();
+// var r = new MockRenderer(gd);
+// var texture = Substitute.For<Texture2D>(gd, 64, 64);
+
+// var m = new TiledMap("test", 2, 2, 32, 32);
+// m.CreateTileset(texture, 0, 32, 32, 4);
+
+// IShapeF shape = new RectangleF(1, 1, 1, 1);
+// TiledObject[] objs =
+// {
+// new TiledObject(TiledObjectType.Tile, 1, 1, shape, 1, 1) { IsVisible = false },
+// };
+
+// var layer = new TiledObjectLayer("object", objs);
+// m.AddLayer(layer);
+
+// r.Map = m;
+
+// r.Draw(new Matrix());
+
+// Assert.IsNull(gd.Indices);
+// }
+
+// [Fact]
+// public void Draw_MapObjectLayer_NoObjects_NoGroups()
+// {
+// var gd = TestHelper.CreateGraphicsDevice();
+// var r = new MockRenderer(gd);
+// var texture = Substitute.For<Texture2D>(gd, 64, 64);
+
+// var m = new TiledMap("test", 2, 2, 32, 32);
+// m.CreateTileset(texture, 0, 32, 32, 4);
+
+// TiledObject[] objs = {};
+
+// var layer = new TiledObjectLayer("object", objs);
+// m.AddLayer(layer);
+
+// r.Map = m;
+
+// r.Draw(new Matrix());
+
+// Assert.IsNull(gd.Indices);
+// }
+
+// [Fact]
+// public void Draw_MapTileLayer_TwoVisible_OneGroup()
+// {
+// var gd = TestHelper.CreateGraphicsDevice();
+// var r = new MockRenderer(gd);
+// var texture = Substitute.For<Texture2D>(gd, 64, 64);
+
+// var m = new TiledMap("test", 2, 2, 32, 32);
+// m.CreateTileset(texture, 0, 32, 32, 4);
+// m.CreateTileLayer("tile", 2, 2, new int[] { 1, 0, 1, 0 });
+
+// r.Map = m;
+
+// r.Draw(new Matrix());
+
+// Assert.NotNull(gd.Indices);
+// Assert.Equal(12, gd.Indices.IndexCount);
+// }
+
+// [Fact]
+// public void Draw_MapTileLayer_AllBlank_NoGroups()
+// {
+// var gd = TestHelper.CreateGraphicsDevice();
+// var r = new MockRenderer(gd);
+// var texture = Substitute.For<Texture2D>(gd, 64, 64);
+
+// var m = new TiledMap("test", 2, 2, 32, 32);
+// m.CreateTileset(texture, 0, 32, 32, 4);
+// m.CreateTileLayer("tile", 2, 2, new int[] { 0, 0, 0, 0 });
+
+// r.Map = m;
+
+// r.Draw(new Matrix());
+
+// Assert.IsNull(gd.Indices);
+// }
+
+// [Fact]
+// public void Draw_MapImageLayer_OneGroup()
+// {
+// var gd = TestHelper.CreateGraphicsDevice();
+// var r = new MockRenderer(gd);
+// var texture = Substitute.For<Texture2D>(gd, 64, 64);
+
+// var m = new TiledMap("test", 10, 10, 32, 32);
+// m.CreateImageLayer("img", texture, new Vector2(100, 100));
+
+// r.Map = m;
+
+// r.Draw(new Matrix());
+
+// Assert.NotNull(gd.Indices);
+// Assert.Equal(6, gd.Indices.IndexCount);
+// }
+
+// [Fact]
+// public void Draw_MapNoGroups()
+// {
+// var gd = TestHelper.CreateGraphicsDevice();
+// var r = new MockRenderer(gd);
+// r.Map = new TiledMap("test", 10, 10, 32, 32);
+
+// r.Draw(new Matrix());
+
+// Assert.IsNull(gd.Indices);
+// }
+
+// [Fact]
+// public void Draw_NoMap()
+// {
+// var gd = TestHelper.CreateGraphicsDevice();
+// var r = new MockRenderer(gd);
+
+// r.Draw(new Matrix());
+
+// Assert.IsNull(gd.Indices);
+// }
+
+// [Fact]
+// public void CreatePrimatives()
+// {
+// var gd = TestHelper.CreateGraphicsDevice();
+// var texture = Substitute.For<Texture2D>(gd, 64, 64);
+// var region = Substitute.For<TextureRegion2D>(texture, 1, 1, 32, 32);
+
+// VertexPositionTexture[] vertices;
+// ushort[] indexes;
+
+// var r = new MockRenderer(gd);
+// r.CreatePrimitives(new Point(0, 0), region, 0, 0.5f, out vertices, out indexes);
+
+// Assert.Equal(4, vertices.Length);
+// Assert.Equal(new Vector3(0, 0, .5f), vertices[0].Position);
+// Assert.Equal(new Vector2(0.0234375f, 0.0234375f), vertices[0].TextureCoordinate);
+// Assert.Equal(new Vector3(32, 0, .5f), vertices[1].Position);
+// Assert.Equal(new Vector2(0.515625f, 0.0234375f), vertices[1].TextureCoordinate);
+// Assert.Equal(new Vector3(0, 32, .5f), vertices[2].Position);
+// Assert.Equal(new Vector2(0.0234375f, 0.515625f), vertices[2].TextureCoordinate);
+// Assert.Equal(new Vector3(32, 32, .5f), vertices[3].Position);
+// Assert.Equal(new Vector2(0.515625f, 0.515625f), vertices[3].TextureCoordinate);
+
+// CollectionAssert.Equal(new[] { 0, 1, 2, 1, 3, 2 }, indexes);
+// }
+
+// [Fact]
+// public void CreatePrimatives_Offset10()
+// {
+// var gd = TestHelper.CreateGraphicsDevice();
+// var texture = Substitute.For<Texture2D>(gd, 64, 64);
+// var region = Substitute.For<TextureRegion2D>(texture, 1, 1, 32, 32);
+
+// VertexPositionTexture[] vertices;
+// ushort[] indexes;
+
+// var r = new MockRenderer(gd);
+// r.CreatePrimitives(new Point(0, 0), region, 10, 0.5f, out vertices, out indexes);
+
+// Assert.Equal(4, vertices.Length);
+// Assert.Equal(new Vector3(0, 0, .5f), vertices[0].Position);
+// Assert.Equal(new Vector2(0.0234375f, 0.0234375f), vertices[0].TextureCoordinate);
+// Assert.Equal(new Vector3(32, 0, .5f), vertices[1].Position);
+// Assert.Equal(new Vector2(0.515625f, 0.0234375f), vertices[1].TextureCoordinate);
+// Assert.Equal(new Vector3(0, 32, .5f), vertices[2].Position);
+// Assert.Equal(new Vector2(0.0234375f, 0.515625f), vertices[2].TextureCoordinate);
+// Assert.Equal(new Vector3(32, 32, .5f), vertices[3].Position);
+// Assert.Equal(new Vector2(0.515625f, 0.515625f), vertices[3].TextureCoordinate);
+
+// CollectionAssert.Equal(new[] { 40, 41, 42, 41, 43, 42 }, indexes);
+// }
+// }
+
+// internal class MockRenderer : TiledMapRenderer
+// {
+// public MockRenderer(GraphicsDevice graphicsDevice)
+// : base(graphicsDevice)
+// {
+// }
+
+// public void CreatePrimitives(Point point, TextureRegion2D region, int offset, float depth,
+// out VertexPositionTexture[] vertices, out ushort[] indexes)
+// {
+// base.CreatePrimitives(point, region, offset, depth, out vertices, out indexes);
+// }
+
+// public new void Draw(Matrix viewMatrix)
+// {
+// base.Draw(viewMatrix);
+// }
+// }
+//}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tiled.Tests/MonoGame.Extended.Tiled.Tests.csproj b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tiled.Tests/MonoGame.Extended.Tiled.Tests.csproj
new file mode 100644
index 0000000..32a97f2
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tiled.Tests/MonoGame.Extended.Tiled.Tests.csproj
@@ -0,0 +1,7 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\source\MonoGame.Extended.Tiled\MonoGame.Extended.Tiled.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tiled.Tests/TiledTilesetTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tiled.Tests/TiledTilesetTests.cs
new file mode 100644
index 0000000..e3e5084
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tiled.Tests/TiledTilesetTests.cs
@@ -0,0 +1,130 @@
+//#region
+
+//using Microsoft.Xna.Framework.Content.Pipeline;
+//using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
+//using Microsoft.Xna.Framework.Graphics;
+//using MonoGame.Extended.Tests;
+//using Xunit;
+
+//#endregion
+
+//namespace MonoGame.Extended.Tiled.Tests
+//{
+//
+// public class TiledTilesetTests
+// {
+// [Fact]
+// public void Constructor()
+// {
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var texture = new Texture2D(graphicsDevice, 64, 64);
+
+// var tiledTileset = new TiledMapTileset(texture, 10, 32, 32, 4, 0, 0);
+
+
+// //Assert.IsNull(tiledTileset.GetTileRegion(0));
+// }
+
+// [Fact]
+// public void GetTileRegion_BlankTile()
+// {
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var texture = new Texture2D(graphicsDevice, 64, 64);
+
+// var tiledTileset = new TiledTileset(texture, 10, 32, 32, 4, 0, 0);
+
+
+// //Assert.IsNull(tiledTileset.GetTileRegion(0));
+// }
+
+// [Fact]
+// [TestCase(9, Result = false, Description = "Too low")]
+// [TestCase(10, Result = true, Description = "Min tile")]
+// [TestCase(11, Result = true, Description = "Middle tile")]
+// [TestCase(13, Result = true, Description = "Last tile")]
+// [TestCase(14, Result = false, Description = "Too high")]
+// public bool ContainsTileId(int id)
+// {
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var texture = new Texture2D(graphicsDevice, 64, 64);
+
+// var tiledTileset = new TiledTileset(texture, 10, 32, 32, 4, 0, 0);
+
+// return tiledTileset.ContainsTileId(id);
+// }
+
+// [Fact]
+// public void Constructor_NoMargin([Values(0, 2)] int spacing)
+// {
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var texture = new Texture2D(graphicsDevice, 64, 64);
+
+// var tiledTileset = new TiledTileset(texture, 1, 32, 32, 4, spacing, 0);
+
+// var region = tiledTileset.GetTileRegion(1);
+// Assert.Equal(texture, region.Texture);
+// Assert.Equal(0, region.X);
+// Assert.Equal(0, region.Y);
+// Assert.Equal(32, region.Width);
+// Assert.Equal(32, region.Height);
+
+// region = tiledTileset.GetTileRegion(2);
+// Assert.Equal(texture, region.Texture);
+// Assert.Equal(spacing + 32, region.X);
+// Assert.Equal(0, region.Y);
+// Assert.Equal(32, region.Width);
+// Assert.Equal(32, region.Height);
+
+// region = tiledTileset.GetTileRegion(3);
+// Assert.Equal(texture, region.Texture);
+// Assert.Equal(0, region.X);
+// Assert.Equal(spacing + 32, region.Y);
+// Assert.Equal(32, region.Width);
+// Assert.Equal(32, region.Height);
+
+// region = tiledTileset.GetTileRegion(4);
+// Assert.Equal(texture, region.Texture);
+// Assert.Equal(spacing + 32, region.X);
+// Assert.Equal(spacing + 32, region.Y);
+// Assert.Equal(32, region.Width);
+// Assert.Equal(32, region.Height);
+// }
+
+// [Fact]
+// public void Constructor_NoSpacing([Values(0, 2)] int margin)
+// {
+// var graphicsDevice = TestHelper.CreateGraphicsDevice();
+// var texture = new Texture2D(graphicsDevice, 64, 64);
+
+// var tileset = new TiledTileset(texture, 1, 32, 32, 4, 0, margin);
+
+// var region = tileset.GetTileRegion(1);
+// Assert.Equal(texture, region.Texture);
+// Assert.Equal(margin, region.X);
+// Assert.Equal(margin, region.Y);
+// Assert.Equal(32, region.Width);
+// Assert.Equal(32, region.Height);
+
+// region = tileset.GetTileRegion(2);
+// Assert.Equal(texture, region.Texture);
+// Assert.Equal(margin + 32, region.X);
+// Assert.Equal(margin, region.Y);
+// Assert.Equal(32, region.Width);
+// Assert.Equal(32, region.Height);
+
+// region = tileset.GetTileRegion(3);
+// Assert.Equal(texture, region.Texture);
+// Assert.Equal(margin, region.X);
+// Assert.Equal(margin + 32, region.Y);
+// Assert.Equal(32, region.Width);
+// Assert.Equal(32, region.Height);
+
+// region = tileset.GetTileRegion(4);
+// Assert.Equal(texture, region.Texture);
+// Assert.Equal(margin + 32, region.X);
+// Assert.Equal(margin + 32, region.Y);
+// Assert.Equal(32, region.Width);
+// Assert.Equal(32, region.Height);
+// }
+// }
+//}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tweening.Tests/ColorHandler.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tweening.Tests/ColorHandler.cs
new file mode 100644
index 0000000..a4cddc0
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tweening.Tests/ColorHandler.cs
@@ -0,0 +1,8 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGame.Extended.Tweening.Tests;
+
+public class ColorHandler
+{
+ public Color Color { get; set; }
+}
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tweening.Tests/MonoGame.Extended.Tweening.Tests.csproj b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tweening.Tests/MonoGame.Extended.Tweening.Tests.csproj
new file mode 100644
index 0000000..88ce44f
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tweening.Tests/MonoGame.Extended.Tweening.Tests.csproj
@@ -0,0 +1,7 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\source\MonoGame.Extended.Tweening\MonoGame.Extended.Tweening.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tweening.Tests/TweenerTests.cs b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tweening.Tests/TweenerTests.cs
new file mode 100644
index 0000000..0fbfded
--- /dev/null
+++ b/Plugins/MonoGame.Extended/tests/MonoGame.Extended.Tweening.Tests/TweenerTests.cs
@@ -0,0 +1,15 @@
+using Microsoft.Xna.Framework;
+using Xunit;
+
+namespace MonoGame.Extended.Tweening.Tests;
+
+public class TweenerTests
+{
+ [Fact]
+ public void TweenerTweenToSuccessTest()
+ {
+ var tweener = new Tweener();
+ var obj = new ColorHandler();
+ tweener.TweenTo(obj, x => x.Color, Color.Red, 2f);
+ }
+}
diff --git a/Plugins/MonoGame.ImGuiNet/.gitignore b/Plugins/MonoGame.ImGuiNet/.gitignore
new file mode 100644
index 0000000..ffca287
--- /dev/null
+++ b/Plugins/MonoGame.ImGuiNet/.gitignore
@@ -0,0 +1,404 @@
+# Created by https://www.toptal.com/developers/gitignore/api/visualstudio,csharp
+# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudio,csharp
+
+### Csharp ###
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
+.idea/
+.DS_Store
+
+# End of https://www.toptal.com/developers/gitignore/api/visualstudio,csharp
diff --git a/Plugins/MonoGame.ImGuiNet/Images/ReadMeBanner.png b/Plugins/MonoGame.ImGuiNet/Images/ReadMeBanner.png
new file mode 100644
index 0000000..b02c77c
--- /dev/null
+++ b/Plugins/MonoGame.ImGuiNet/Images/ReadMeBanner.png
Binary files differ
diff --git a/Plugins/MonoGame.ImGuiNet/LICENSE b/Plugins/MonoGame.ImGuiNet/LICENSE
new file mode 100644
index 0000000..0e01c1f
--- /dev/null
+++ b/Plugins/MonoGame.ImGuiNet/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Mezo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Plugins/MonoGame.ImGuiNet/MonoGame.ImGuiNet/DrawVertDeclaration.cs b/Plugins/MonoGame.ImGuiNet/MonoGame.ImGuiNet/DrawVertDeclaration.cs
new file mode 100644
index 0000000..5ad2f9e
--- /dev/null
+++ b/Plugins/MonoGame.ImGuiNet/MonoGame.ImGuiNet/DrawVertDeclaration.cs
@@ -0,0 +1,29 @@
+using Microsoft.Xna.Framework.Graphics;
+using ImGuiNET;
+
+namespace MonoGame.ImGuiNet;
+
+public static class DrawVertDeclaration
+{
+ public static readonly VertexDeclaration Declaration;
+
+ public static readonly int Size;
+
+ static DrawVertDeclaration()
+ {
+ unsafe { Size = sizeof(ImDrawVert); }
+
+ Declaration = new VertexDeclaration(
+ Size,
+
+ // Position
+ new VertexElement(0, VertexElementFormat.Vector2, VertexElementUsage.Position, 0),
+
+ // UV
+ new VertexElement(8, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0),
+
+ // Color
+ new VertexElement(16, VertexElementFormat.Color, VertexElementUsage.Color, 0)
+ );
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.ImGuiNet/MonoGame.ImGuiNet/ImGuiRenderer.cs b/Plugins/MonoGame.ImGuiNet/MonoGame.ImGuiNet/ImGuiRenderer.cs
new file mode 100644
index 0000000..a9a430f
--- /dev/null
+++ b/Plugins/MonoGame.ImGuiNet/MonoGame.ImGuiNet/ImGuiRenderer.cs
@@ -0,0 +1,426 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using ImGuiNET;
+using System.Threading;
+using System.Reflection;
+
+namespace MonoGame.ImGuiNet;
+
+public class ImGuiRenderer
+{
+ private Game _game;
+
+ // Graphics
+ private GraphicsDevice _graphicsDevice;
+
+ private BasicEffect _effect;
+ private RasterizerState _rasterizerState;
+
+ private byte[] _vertexData;
+ private VertexBuffer _vertexBuffer;
+ private int _vertexBufferSize;
+
+ private byte[] _indexData;
+ private IndexBuffer _indexBuffer;
+ private int _indexBufferSize;
+
+ // Textures
+ private Dictionary<IntPtr, Texture2D> _loadedTextures;
+
+ private int _textureId;
+ private IntPtr? _fontTextureId;
+
+ // Input
+ private int _scrollWheelValue;
+ private int _horizontalScrollWheelValue;
+ private readonly float WHEEL_DELTA = 120;
+ private Keys[] _allKeys = Enum.GetValues<Keys>();
+
+ public ImGuiRenderer(Game game)
+ {
+ var context = ImGui.CreateContext();
+ ImGui.SetCurrentContext(context);
+
+ _game = game ?? throw new ArgumentNullException(nameof(game));
+ _graphicsDevice = game.GraphicsDevice;
+
+ _loadedTextures = new Dictionary<IntPtr, Texture2D>();
+
+ _rasterizerState = new RasterizerState()
+ {
+ CullMode = CullMode.None,
+ DepthBias = 0,
+ FillMode = FillMode.Solid,
+ MultiSampleAntiAlias = false,
+ ScissorTestEnable = true,
+ SlopeScaleDepthBias = 0
+ };
+
+ SetupInput();
+ }
+
+ #region ImGuiRenderer
+
+ /// <summary>
+ /// Creates a texture and loads the font data from ImGui. Should be called when the <see cref="GraphicsDevice" /> is initialized but before any rendering is done
+ /// </summary>
+ public virtual unsafe void RebuildFontAtlas()
+ {
+ // Get font texture from ImGui
+ var io = ImGui.GetIO();
+ io.Fonts.GetTexDataAsRGBA32(out byte* pixelData, out int width, out int height, out int bytesPerPixel);
+
+ // Copy the data to a managed array
+ var pixels = new byte[width * height * bytesPerPixel];
+ unsafe { Marshal.Copy(new IntPtr(pixelData), pixels, 0, pixels.Length); }
+
+ // Create and register the texture as an XNA texture
+ var tex2d = new Texture2D(_graphicsDevice, width, height, false, SurfaceFormat.Color);
+ tex2d.SetData(pixels);
+
+ // Should a texture already have been build previously, unbind it first so it can be deallocated
+ if (_fontTextureId.HasValue) UnbindTexture(_fontTextureId.Value);
+
+ // Bind the new texture to an ImGui-friendly id
+ _fontTextureId = BindTexture(tex2d);
+
+ // Let ImGui know where to find the texture
+ io.Fonts.SetTexID(_fontTextureId.Value);
+ io.Fonts.ClearTexData(); // Clears CPU side texture data
+ }
+
+ /// <summary>
+ /// Creates a pointer to a texture, which can be passed through ImGui calls such as <see cref="ImGui.Image" />. That pointer is then used by ImGui to let us know what texture to draw
+ /// </summary>
+ public virtual IntPtr BindTexture(Texture2D texture)
+ {
+ var id = new IntPtr(_textureId++);
+
+ _loadedTextures.Add(id, texture);
+
+ return id;
+ }
+
+ /// <summary>
+ /// Removes a previously created texture pointer, releasing its reference and allowing it to be deallocated
+ /// </summary>
+ public virtual void UnbindTexture(IntPtr textureId)
+ {
+ _loadedTextures.Remove(textureId);
+ }
+
+ /// <summary>
+ /// Sets up ImGui for a new frame, should be called at frame start
+ /// </summary>
+ public virtual void BeginLayout(GameTime gameTime)
+ {
+ ImGui.GetIO().DeltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
+
+ UpdateInput();
+
+ ImGui.NewFrame();
+ }
+
+ /// <summary>
+ /// Asks ImGui for the generated geometry data and sends it to the graphics pipeline, should be called after the UI is drawn using ImGui.** calls
+ /// </summary>
+ public virtual void EndLayout()
+ {
+ ImGui.Render();
+
+ unsafe { RenderDrawData(ImGui.GetDrawData()); }
+ }
+
+ #endregion ImGuiRenderer
+
+ #region Setup & Update
+
+ /// <summary>
+ /// Setup key input event handler.
+ /// </summary>
+ protected virtual void SetupInput()
+ {
+ var io = ImGui.GetIO();
+
+ // MonoGame-specific //////////////////////
+ _game.Window.TextInput += (s, a) =>
+ {
+ if (a.Character == '\t') return;
+ io.AddInputCharacter(a.Character);
+ };
+
+ ///////////////////////////////////////////
+
+ // FNA-specific ///////////////////////////
+ //TextInputEXT.TextInput += c =>
+ //{
+ // if (c == '\t') return;
+
+ // ImGui.GetIO().AddInputCharacter(c);
+ //};
+ ///////////////////////////////////////////
+ }
+
+ /// <summary>
+ /// Updates the <see cref="Effect" /> to the current matrices and texture
+ /// </summary>
+ protected virtual Effect UpdateEffect(Texture2D texture)
+ {
+ _effect = _effect ?? new BasicEffect(_graphicsDevice);
+
+ var io = ImGui.GetIO();
+
+ _effect.World = Matrix.Identity;
+ _effect.View = Matrix.Identity;
+ _effect.Projection = Matrix.CreateOrthographicOffCenter(0f, io.DisplaySize.X, io.DisplaySize.Y, 0f, -1f, 1f);
+ _effect.TextureEnabled = true;
+ _effect.Texture = texture;
+ _effect.VertexColorEnabled = true;
+
+ return _effect;
+ }
+
+ /// <summary>
+ /// Sends XNA input state to ImGui
+ /// </summary>
+ protected virtual void UpdateInput()
+ {
+ if (!_game.IsActive) return;
+
+ var io = ImGui.GetIO();
+
+ var mouse = Mouse.GetState();
+ var keyboard = Keyboard.GetState();
+
+ io.AddMousePosEvent(mouse.X, mouse.Y);
+ io.AddMouseButtonEvent(0, mouse.LeftButton == ButtonState.Pressed);
+ io.AddMouseButtonEvent(1, mouse.RightButton == ButtonState.Pressed);
+ io.AddMouseButtonEvent(2, mouse.MiddleButton == ButtonState.Pressed);
+ io.AddMouseButtonEvent(3, mouse.XButton1 == ButtonState.Pressed);
+ io.AddMouseButtonEvent(4, mouse.XButton2 == ButtonState.Pressed);
+
+ io.AddMouseWheelEvent(
+ (mouse.HorizontalScrollWheelValue - _horizontalScrollWheelValue) / WHEEL_DELTA,
+ (mouse.ScrollWheelValue - _scrollWheelValue) / WHEEL_DELTA);
+ _scrollWheelValue = mouse.ScrollWheelValue;
+ _horizontalScrollWheelValue = mouse.HorizontalScrollWheelValue;
+
+ foreach (var key in _allKeys)
+ {
+ if (TryMapKeys(key, out ImGuiKey imguikey))
+ {
+ io.AddKeyEvent(imguikey, keyboard.IsKeyDown(key));
+ }
+ }
+
+ io.DisplaySize = new System.Numerics.Vector2(_graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight);
+ io.DisplayFramebufferScale = new System.Numerics.Vector2(1f, 1f);
+ }
+
+ private bool TryMapKeys(Keys key, out ImGuiKey imguikey)
+ {
+ //Special case not handed in the switch...
+ //If the actual key we put in is "None", return none and true.
+ //otherwise, return none and false.
+ if (key == Keys.None)
+ {
+ imguikey = ImGuiKey.None;
+ return true;
+ }
+
+ imguikey = key switch
+ {
+ Keys.Back => ImGuiKey.Backspace,
+ Keys.Tab => ImGuiKey.Tab,
+ Keys.Enter => ImGuiKey.Enter,
+ Keys.CapsLock => ImGuiKey.CapsLock,
+ Keys.Escape => ImGuiKey.Escape,
+ Keys.Space => ImGuiKey.Space,
+ Keys.PageUp => ImGuiKey.PageUp,
+ Keys.PageDown => ImGuiKey.PageDown,
+ Keys.End => ImGuiKey.End,
+ Keys.Home => ImGuiKey.Home,
+ Keys.Left => ImGuiKey.LeftArrow,
+ Keys.Right => ImGuiKey.RightArrow,
+ Keys.Up => ImGuiKey.UpArrow,
+ Keys.Down => ImGuiKey.DownArrow,
+ Keys.PrintScreen => ImGuiKey.PrintScreen,
+ Keys.Insert => ImGuiKey.Insert,
+ Keys.Delete => ImGuiKey.Delete,
+ >= Keys.D0 and <= Keys.D9 => ImGuiKey._0 + (key - Keys.D0),
+ >= Keys.A and <= Keys.Z => ImGuiKey.A + (key - Keys.A),
+ >= Keys.NumPad0 and <= Keys.NumPad9 => ImGuiKey.Keypad0 + (key - Keys.NumPad0),
+ Keys.Multiply => ImGuiKey.KeypadMultiply,
+ Keys.Add => ImGuiKey.KeypadAdd,
+ Keys.Subtract => ImGuiKey.KeypadSubtract,
+ Keys.Decimal => ImGuiKey.KeypadDecimal,
+ Keys.Divide => ImGuiKey.KeypadDivide,
+ >= Keys.F1 and <= Keys.F12 => ImGuiKey.F1 + (key - Keys.F1),
+ Keys.NumLock => ImGuiKey.NumLock,
+ Keys.Scroll => ImGuiKey.ScrollLock,
+ Keys.LeftShift => ImGuiKey.ModShift,
+ Keys.LeftControl => ImGuiKey.ModCtrl,
+ Keys.LeftAlt => ImGuiKey.ModAlt,
+ Keys.OemSemicolon => ImGuiKey.Semicolon,
+ Keys.OemPlus => ImGuiKey.Equal,
+ Keys.OemComma => ImGuiKey.Comma,
+ Keys.OemMinus => ImGuiKey.Minus,
+ Keys.OemPeriod => ImGuiKey.Period,
+ Keys.OemQuestion => ImGuiKey.Slash,
+ Keys.OemTilde => ImGuiKey.GraveAccent,
+ Keys.OemOpenBrackets => ImGuiKey.LeftBracket,
+ Keys.OemCloseBrackets => ImGuiKey.RightBracket,
+ Keys.OemPipe => ImGuiKey.Backslash,
+ Keys.OemQuotes => ImGuiKey.Apostrophe,
+ _ => ImGuiKey.None,
+ };
+
+ return imguikey != ImGuiKey.None;
+ }
+
+ #endregion Setup & Update
+
+ #region Internals
+
+ /// <summary>
+ /// Gets the geometry as set up by ImGui and sends it to the graphics device
+ /// </summary>
+ private void RenderDrawData(ImDrawDataPtr drawData)
+ {
+ // Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, vertex/texcoord/color pointers
+ var lastViewport = _graphicsDevice.Viewport;
+ var lastScissorBox = _graphicsDevice.ScissorRectangle;
+
+ _graphicsDevice.BlendFactor = Color.White;
+ _graphicsDevice.BlendState = BlendState.NonPremultiplied;
+ _graphicsDevice.RasterizerState = _rasterizerState;
+ _graphicsDevice.DepthStencilState = DepthStencilState.DepthRead;
+
+ // Handle cases of screen coordinates != from framebuffer coordinates (e.g. retina displays)
+ drawData.ScaleClipRects(ImGui.GetIO().DisplayFramebufferScale);
+
+ // Setup projection
+ _graphicsDevice.Viewport = new Viewport(0, 0, _graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight);
+
+ UpdateBuffers(drawData);
+
+ RenderCommandLists(drawData);
+
+ // Restore modified state
+ _graphicsDevice.Viewport = lastViewport;
+ _graphicsDevice.ScissorRectangle = lastScissorBox;
+ }
+
+ private unsafe void UpdateBuffers(ImDrawDataPtr drawData)
+ {
+ if (drawData.TotalVtxCount == 0)
+ {
+ return;
+ }
+
+ // Expand buffers if we need more room
+ if (drawData.TotalVtxCount > _vertexBufferSize)
+ {
+ _vertexBuffer?.Dispose();
+
+ _vertexBufferSize = (int)(drawData.TotalVtxCount * 1.5f);
+ _vertexBuffer = new VertexBuffer(_graphicsDevice, DrawVertDeclaration.Declaration, _vertexBufferSize, BufferUsage.None);
+ _vertexData = new byte[_vertexBufferSize * DrawVertDeclaration.Size];
+ }
+
+ if (drawData.TotalIdxCount > _indexBufferSize)
+ {
+ _indexBuffer?.Dispose();
+
+ _indexBufferSize = (int)(drawData.TotalIdxCount * 1.5f);
+ _indexBuffer = new IndexBuffer(_graphicsDevice, IndexElementSize.SixteenBits, _indexBufferSize, BufferUsage.None);
+ _indexData = new byte[_indexBufferSize * sizeof(ushort)];
+ }
+
+ // Copy ImGui's vertices and indices to a set of managed byte arrays
+ int vtxOffset = 0;
+ int idxOffset = 0;
+
+ for (int n = 0; n < drawData.CmdListsCount; n++)
+ {
+ ImDrawListPtr cmdList = drawData.CmdLists[n];
+
+ fixed (void* vtxDstPtr = &_vertexData[vtxOffset * DrawVertDeclaration.Size])
+ fixed (void* idxDstPtr = &_indexData[idxOffset * sizeof(ushort)])
+ {
+ Buffer.MemoryCopy((void*)cmdList.VtxBuffer.Data, vtxDstPtr, _vertexData.Length, cmdList.VtxBuffer.Size * DrawVertDeclaration.Size);
+ Buffer.MemoryCopy((void*)cmdList.IdxBuffer.Data, idxDstPtr, _indexData.Length, cmdList.IdxBuffer.Size * sizeof(ushort));
+ }
+
+ vtxOffset += cmdList.VtxBuffer.Size;
+ idxOffset += cmdList.IdxBuffer.Size;
+ }
+
+ // Copy the managed byte arrays to the gpu vertex- and index buffers
+ _vertexBuffer.SetData(_vertexData, 0, drawData.TotalVtxCount * DrawVertDeclaration.Size);
+ _indexBuffer.SetData(_indexData, 0, drawData.TotalIdxCount * sizeof(ushort));
+ }
+
+ private unsafe void RenderCommandLists(ImDrawDataPtr drawData)
+ {
+ _graphicsDevice.SetVertexBuffer(_vertexBuffer);
+ _graphicsDevice.Indices = _indexBuffer;
+
+ int vtxOffset = 0;
+ int idxOffset = 0;
+
+ for (int n = 0; n < drawData.CmdListsCount; n++)
+ {
+ ImDrawListPtr cmdList = drawData.CmdLists[n];
+
+ for (int cmdi = 0; cmdi < cmdList.CmdBuffer.Size; cmdi++)
+ {
+ ImDrawCmdPtr drawCmd = cmdList.CmdBuffer[cmdi];
+
+ if (drawCmd.ElemCount == 0)
+ {
+ continue;
+ }
+
+ if (!_loadedTextures.ContainsKey(drawCmd.TextureId))
+ {
+ throw new InvalidOperationException($"Could not find a texture with id '{drawCmd.TextureId}', please check your bindings");
+ }
+
+ _graphicsDevice.ScissorRectangle = new Rectangle(
+ (int)drawCmd.ClipRect.X,
+ (int)drawCmd.ClipRect.Y,
+ (int)(drawCmd.ClipRect.Z - drawCmd.ClipRect.X),
+ (int)(drawCmd.ClipRect.W - drawCmd.ClipRect.Y)
+ );
+
+ var effect = UpdateEffect(_loadedTextures[drawCmd.TextureId]);
+
+ foreach (var pass in effect.CurrentTechnique.Passes)
+ {
+ pass.Apply();
+
+#pragma warning disable CS0618 // // FNA does not expose an alternative method.
+ _graphicsDevice.DrawIndexedPrimitives(
+ primitiveType: PrimitiveType.TriangleList,
+ baseVertex: (int)drawCmd.VtxOffset + vtxOffset,
+ minVertexIndex: 0,
+ numVertices: cmdList.VtxBuffer.Size,
+ startIndex: (int)drawCmd.IdxOffset + idxOffset,
+ primitiveCount: (int)drawCmd.ElemCount / 3
+ );
+#pragma warning restore CS0618
+ }
+ }
+
+ vtxOffset += cmdList.VtxBuffer.Size;
+ idxOffset += cmdList.IdxBuffer.Size;
+ }
+ }
+
+ #endregion Internals
+}
diff --git a/Plugins/MonoGame.ImGuiNet/MonoGame.ImGuiNet/Monogame.ImGuiNet.csproj b/Plugins/MonoGame.ImGuiNet/MonoGame.ImGuiNet/Monogame.ImGuiNet.csproj
new file mode 100644
index 0000000..3205e14
--- /dev/null
+++ b/Plugins/MonoGame.ImGuiNet/MonoGame.ImGuiNet/Monogame.ImGuiNet.csproj
@@ -0,0 +1,57 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <OutputType>Library</OutputType>
+ <TargetFramework>net8.0</TargetFramework>
+ <RollForward>Major</RollForward>
+ <PublishReadyToRun>false</PublishReadyToRun>
+ <TieredCompilation>false</TieredCompilation>
+ <Version>1.1.0</Version>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <Authors>Mezo</Authors>
+ <Company>Abslute Chaos</Company>
+ <Copyright>Copyright (c) 2023 Mezo</Copyright>
+
+ <RepositoryUrl>https://github.com/Mezo-hx/MonoGame.ImGuiNet</RepositoryUrl>
+ <RepositoryType>git</RepositoryType>
+ <RepositoryBranch>main</RepositoryBranch>
+ <NeutralLanguage>en</NeutralLanguage>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <PackageId>MonoGame.ImGuiNet</PackageId>
+
+ <PackageIcon>NugetpkgIcon.png</PackageIcon>
+ <PackageTags>
+ MonoGame;ImGui;import;processes;read;write;gui;
+ </PackageTags>
+ <PackageReadmeFile>README.md</PackageReadmeFile>
+ <PackageReleaseNotes>
+ [General fixes]
+ </PackageReleaseNotes>
+ <Description>
+ A superset of ImGuiNet with additional components designed for Monogame
+ </Description>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <Title>MonoGame.ImGuiNet</Title>
+ <PackageLicenseExpression>MIT</PackageLicenseExpression>
+ <PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <None Include="../NugetpkgIcon.png" Pack="true" PackagePath="" />
+ <None Include="../README.MD" Pack="true" PackagePath="" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="ImGui.NET" Version="1.90.1.1" />
+ <PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.303" />
+ <PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.1.303" />
+ </ItemGroup>
+
+ <Target Name="RestoreDotnetTools" BeforeTargets="Restore">
+ <Message Text="Restoring dotnet tools" Importance="High" />
+ <Exec Command="dotnet tool restore" />
+ </Target>
+</Project> \ No newline at end of file
diff --git a/Plugins/MonoGame.ImGuiNet/MonoGame.ImGuiNet/Utils/FilePicker.cs b/Plugins/MonoGame.ImGuiNet/MonoGame.ImGuiNet/Utils/FilePicker.cs
new file mode 100644
index 0000000..298c527
--- /dev/null
+++ b/Plugins/MonoGame.ImGuiNet/MonoGame.ImGuiNet/Utils/FilePicker.cs
@@ -0,0 +1,26 @@
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Windows;
+
+namespace Monogame.ImGuiNet.Utils
+{
+ public class FilePicker
+ {
+ public static string DefaultPath_Windows = "C:\\"; //The default path for windows
+ public static string ImagePath; //The File path from the image
+ public static void BeginFilePicker()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ // TODO: Stop being lazy and release the FilePicker as quick as possible
+ }
+
+ // TODO: Add support for Linux and Mac
+ }
+
+ public static void BeginFolderPicker()
+ {
+ // TODO: Functionality for folder picking
+ }
+ }
+}
diff --git a/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNet.sln b/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNet.sln
new file mode 100644
index 0000000..0555f3c
--- /dev/null
+++ b/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNet.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.6.33717.318
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monogame.ImGuiNet", "MonoGame.ImGuiNet\Monogame.ImGuiNet.csproj", "{0A0C88C8-0E2C-4B1A-BF69-8D67F97F37C6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Monogame.ImGuiNetSamples", "Monogame.ImGuiNetSamples\Monogame.ImGuiNetSamples.csproj", "{36565BA0-242D-4DD1-B184-841E349CFFC8}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {0A0C88C8-0E2C-4B1A-BF69-8D67F97F37C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0A0C88C8-0E2C-4B1A-BF69-8D67F97F37C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0A0C88C8-0E2C-4B1A-BF69-8D67F97F37C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0A0C88C8-0E2C-4B1A-BF69-8D67F97F37C6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {36565BA0-242D-4DD1-B184-841E349CFFC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {36565BA0-242D-4DD1-B184-841E349CFFC8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {36565BA0-242D-4DD1-B184-841E349CFFC8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {36565BA0-242D-4DD1-B184-841E349CFFC8}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {04475A17-846B-466C-9D3D-A3AD35D40DA3}
+ EndGlobalSection
+EndGlobal
diff --git a/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/.config/dotnet-tools.json b/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/.config/dotnet-tools.json
new file mode 100644
index 0000000..efabe22
--- /dev/null
+++ b/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/.config/dotnet-tools.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb": {
+ "version": "3.8.1.303",
+ "commands": [
+ "mgcb"
+ ]
+ },
+ "dotnet-mgcb-editor": {
+ "version": "3.8.1.303",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.1.303",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.1.303",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.1.303",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Content/Content.mgcb b/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Content/Content.mgcb
new file mode 100644
index 0000000..07d49d8
--- /dev/null
+++ b/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Content/Content.mgcb
@@ -0,0 +1,34 @@
+
+#----------------------------- Global Properties ----------------------------#
+
+/outputDir:bin/$(Platform)
+/intermediateDir:obj/$(Platform)
+/platform:DesktopGL
+/config:
+/profile:Reach
+/compress:False
+
+#-------------------------------- References --------------------------------#
+
+
+#---------------------------------- Content ---------------------------------#
+
+#begin suzanne.fbx
+/importer:FbxImporter
+/processor:ModelProcessor
+/processorParam:ColorKeyColor=0,0,0,0
+/processorParam:ColorKeyEnabled=True
+/processorParam:DefaultEffect=BasicEffect
+/processorParam:GenerateMipmaps=True
+/processorParam:GenerateTangentFrames=False
+/processorParam:PremultiplyTextureAlpha=True
+/processorParam:PremultiplyVertexColors=True
+/processorParam:ResizeTexturesToPowerOfTwo=False
+/processorParam:RotationX=0
+/processorParam:RotationY=0
+/processorParam:RotationZ=0
+/processorParam:Scale=1
+/processorParam:SwapWindingOrder=False
+/processorParam:TextureFormat=Compressed
+/build:suzanne.fbx
+
diff --git a/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Content/suzanne.fbx b/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Content/suzanne.fbx
new file mode 100644
index 0000000..8f3faff
--- /dev/null
+++ b/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Content/suzanne.fbx
Binary files differ
diff --git a/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Game1.cs b/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Game1.cs
new file mode 100644
index 0000000..f498182
--- /dev/null
+++ b/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Game1.cs
@@ -0,0 +1,3035 @@
+/*
+ * Originally adapted from withoutaface/MonoGameImGuiNETexamples which is a port of the C++ Dear IMGUI sample code for the C++ version of Dear ImGui
+ */
+
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using System.Collections.Generic;
+using System;
+using MonoGame.ImGuiNet;
+using ImGuiNET;
+
+using Vec2 = System.Numerics.Vector2;
+using Vec3 = System.Numerics.Vector3;
+using Vec4 = System.Numerics.Vector4;
+
+namespace Monogame.ImGuiNetSamples
+{
+ /// <summary>
+ /// This is the main type for your game.
+ /// </summary>
+ public class Game1 : Game
+ {
+ GraphicsDeviceManager graphics;
+ SpriteBatch spriteBatch;
+
+ ImGuiRenderer GuiRenderer;
+
+ bool WasResized = false;
+ private Model suzanne;
+
+ private Matrix world = Matrix.CreateScale(1.5f, 1.5f, 1.5f) * Matrix.CreateRotationX(-1.5f);
+ private Matrix view = Matrix.CreateLookAt(new Vector3(0, 0, 10), new Vector3(0, 0, 0), Vector3.UnitY);
+ private Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), 800f / 480f, 0.1f, 100.0f);
+
+ public Game1()
+ {
+ graphics = new GraphicsDeviceManager(this);
+ Content.RootDirectory = "Content";
+ IsMouseVisible = true;
+
+ graphics.PreferredBackBufferWidth = 1024;
+ graphics.PreferredBackBufferHeight = 768;
+
+ Window.AllowUserResizing = true; // true;
+ Window.ClientSizeChanged += delegate { WasResized = true; };
+ }
+
+ /// <summary>
+ /// Allows the game to perform any initialization it needs to before starting to run.
+ /// This is where it can query for any required services and load any non-graphic
+ /// related content. Calling base.Initialize will enumerate through any components
+ /// and initialize them as well.
+ /// </summary>
+ protected override void Initialize()
+ {
+ // TODO: Add your initialization logic here
+ this.Window.Title = "MonoGame & ImGui.NET";
+
+ GuiRenderer = new ImGuiRenderer(this);
+ GuiRenderer.RebuildFontAtlas();
+
+ base.Initialize();
+ }
+
+ /// <summary>
+ /// LoadContent will be called once per game and is the place to load
+ /// all of your content.
+ /// </summary>
+ protected override void LoadContent()
+ {
+ // Create a new SpriteBatch, which can be used to draw textures.
+ spriteBatch = new SpriteBatch(GraphicsDevice);
+
+ // TODO: use this.Content to load your game content here
+ suzanne = Content.Load<Model>("suzanne");
+ }
+
+ /// <summary>
+ /// UnloadContent will be called once per game and is the place to unload
+ /// game-specific content.
+ /// </summary>
+ protected override void UnloadContent()
+ {
+ // TODO: Unload any non ContentManager content here
+ }
+
+ /// <summary>
+ /// Allows the game to run logic such as updating the world,
+ /// checking for collisions, gathering input, and playing audio.
+ /// </summary>
+ /// <param name="gameTime">Provides a snapshot of timing values.</param>
+ protected override void Update(GameTime gameTime)
+ {
+ if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
+ Exit();
+
+ //update logic
+ if (WasResized)
+ {
+ string new_resolution = resolution[select_res];
+
+ int res_width = int.Parse(new_resolution.Split('x')[0]);
+ int res_height = int.Parse(new_resolution.Split('x')[1]);
+
+ graphics.PreferredBackBufferWidth = Window.ClientBounds.Width; //1920;
+ graphics.PreferredBackBufferHeight = Window.ClientBounds.Height; //1080;
+
+ graphics.ApplyChanges();
+
+ WasResized = false;
+ current_res = select_res;
+
+ debug_log.Add("(" + DateTime.Now.ToShortTimeString() + ") [MonoGame] Changed Resolution to " + resolution[current_res]);
+ }
+
+ base.Update(gameTime);
+ }
+
+ private void DrawModel(Model model, Matrix world, Matrix view, Matrix projection)
+ {
+ foreach (ModelMesh mesh in model.Meshes)
+ {
+ foreach (BasicEffect effect in mesh.Effects)
+ {
+ effect.TextureEnabled = false;
+ effect.EnableDefaultLighting();
+ effect.World = world;
+ effect.View = view;
+ effect.Projection = projection;
+ }
+ mesh.Draw();
+ }
+ }
+
+ /// <summary>
+ /// This is called when the game should draw itself.
+ /// </summary>
+ /// <param name="gameTime">Provides a snapshot of timing values.</param>
+ protected override void Draw(GameTime gameTime)
+ {
+ GraphicsDevice.Clear(Color.CornflowerBlue);
+
+ //Framerate
+ float frameRate = 1 / (float)gameTime.ElapsedGameTime.TotalSeconds;
+
+ //Depth Buffer
+ DepthStencilState dss = new DepthStencilState();
+ dss.DepthBufferEnable = true;
+ GraphicsDevice.DepthStencilState = dss;
+ GraphicsDevice.BlendState = BlendState.Opaque;
+
+ //Draw 3D model
+ if (render_model == 1)
+ {
+ DrawModel(suzanne, world, view, projection);
+ }
+
+ base.Draw(gameTime);
+
+ //ImGui Begin
+ GuiRenderer.BeginLayout(gameTime);
+
+ #region ImGui
+ //Native Demos
+ if (show_native_examples)
+ {
+ DrawImGuiNativeDemos();
+ }
+
+ //Style Editor
+ if (show_app_style_editor)
+ {
+ ImGui.Begin("Dear ImGui Style Editor");
+ ImGui.ShowStyleEditor();
+ ImGui.End();
+ }
+
+ //Metrics window
+ if (show_app_metrics)
+ {
+ ImGui.ShowMetricsWindow();
+ }
+
+ //About window
+ if (show_app_about)
+ {
+ ImGui.ShowAboutWindow();
+ }
+
+ //Overlay
+ if (show_app_simple_overlay)
+ {
+ DrawImGuiOverlay(frameRate);
+ }
+
+ //Menu bar
+ if (show_app_main_menu_bar)
+ {
+ DrawImGuiMenuBar();
+ }
+
+ //Console
+ if (show_app_console)
+ {
+ DrawImGuiExampleAppConsole();
+ }
+
+ //Log
+ if (show_app_log)
+ {
+ DrawImGuiDebugLog();
+ }
+
+ //MonoGame
+ if (show_monogame_settings)
+ {
+ DrawMonoGameWindow();
+ }
+
+ //Main Demo window
+ if (show_main_window)
+ {
+ DrawDemoWindow();
+ }
+ #endregion
+
+ //ImGui End
+ GuiRenderer.EndLayout();
+ }
+
+ #region Demos
+ private void DrawImGuiNativeDemos()
+ {
+ ImGui.ShowDemoWindow();
+ }
+ #endregion
+
+ #region Overlay
+
+ //OverlayVariables
+ float distanceX = 10.0f;
+ float distanceY = 10.0f;
+ int corner = 0;
+
+ private void DrawImGuiOverlay(float frameRate)
+ {
+ ImGuiIOPtr io = ImGui.GetIO();
+ ImGuiWindowFlags windowFlags = ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoMove;
+
+ if (corner >= 0 && corner < 4)
+ {
+ //offset X
+ if (corner == 1 || corner == 3)
+ {
+ distanceX = (float)graphics.PreferredBackBufferWidth - 250.0f;
+ }
+ else
+ {
+ distanceX = 10.0f;
+ }
+ //offset Y
+ if (corner == 2 || corner == 3)
+ {
+ distanceY = (float)graphics.PreferredBackBufferHeight - 100.0f;
+ }
+ else
+ {
+ distanceY = 10.0f;
+ }
+ //offset menubar
+ if ((corner == 0 || corner == 1) && show_app_main_menu_bar)
+ {
+ distanceY += 20.0f;
+ }
+
+ Vec2 windowPosition = new Vec2(distanceX, distanceY);
+ ImGui.SetNextWindowPos(windowPosition);
+ }
+
+ ImGui.SetNextWindowBgAlpha(0.35f);
+ if (ImGui.Begin("Example: Simple overlay", windowFlags))
+ {
+ ImGui.Text("Simple overlay\nin the corner of the screen\n(right-click to change position)");
+ ImGui.Separator();
+ if (ImGui.IsMousePosValid())
+ {
+ ImGui.Text(string.Format("Mouse Position: ({0},{1})", io.MousePos.X, io.MousePos.Y));
+ }
+ else
+ {
+ ImGui.Text("Mouse Position: <invalid>");
+ }
+
+ ImGui.Text(string.Format("Frames per second: {0}", frameRate.ToString()));
+
+ if (ImGui.BeginPopupContextWindow())
+ {
+ //if (ImGui.MenuItem("Custom", null, corner == -1)) { corner = -1; }
+ if (ImGui.MenuItem("Top-left", null, corner == 0)) { corner = 0; }
+ if (ImGui.MenuItem("Top-right", null, corner == 1)) { corner = 1; }
+ if (ImGui.MenuItem("Bottom-left", null, corner == 2)) { corner = 2; }
+ if (ImGui.MenuItem("Bottom-right", null, corner == 3)) { corner = 3; }
+
+ ImGui.EndPopup();
+ }
+ }
+
+ ImGui.End();
+ }
+ #endregion
+
+ #region MenuBar
+ private void DrawImGuiMenuBar()
+ {
+ if (ImGui.BeginMainMenuBar())
+ {
+ if (ImGui.BeginMenu("File"))
+ {
+ ShowExampleMenuFile();
+ ImGui.EndMenu();
+ }
+ if (ImGui.BeginMenu("Edit"))
+ {
+ if (ImGui.MenuItem("Undo", "CTRL+Z")) { }
+ if (ImGui.MenuItem("Redo", "CTRL+Y", false, false)) { }
+ ImGui.Separator();
+ if (ImGui.MenuItem("Cut", "CTRL+X")) { }
+ if (ImGui.MenuItem("Copy", "CTRL+C")) { }
+ if (ImGui.MenuItem("Paste", "CTRL+V")) { }
+ ImGui.EndMenu();
+ }
+ ImGui.EndMainMenuBar();
+ }
+ }
+
+ private void ShowExampleMenuFile()
+ {
+ ImGui.MenuItem("(demo menu)", null, false, false);
+ if (ImGui.MenuItem("New")) { }
+ if (ImGui.MenuItem("Open", "Ctrl+O")) { }
+ if (ImGui.MenuItem("Open Recent"))
+ {
+ ImGui.MenuItem("fish_hat.c");
+ ImGui.MenuItem("fish_hat.inl");
+ ImGui.MenuItem("fish_hat.h");
+ if (ImGui.MenuItem("More.."))
+ {
+ ImGui.MenuItem("Hello");
+ ImGui.MenuItem("Sailor");
+ if (ImGui.BeginMenu("Recurse.."))
+ {
+ ShowExampleMenuFile();
+ ImGui.EndMenu();
+ }
+ ImGui.EndMenu();
+ }
+ ImGui.EndMenu();
+ }
+ if (ImGui.MenuItem("Save", "Ctrl+S")) { }
+ if (ImGui.MenuItem("Save As ..")) { }
+
+ ImGui.Separator();
+ if (ImGui.BeginMenu("Options"))
+ {
+ bool enabled = true;
+ ImGui.MenuItem("Enabled", "", enabled);
+ ImGui.BeginChild("child", new Vec2(0, 60), ImGuiChildFlags.Border);
+ for (int i = 0; i < 10; i++)
+ {
+ ImGui.Text(string.Format("Scrolling Text {0}", i));
+ }
+ ImGui.EndChild();
+ float f = 0.5f;
+ int n = 0;
+ ImGui.SliderFloat("Value", ref f, 0.0f, 1.0f);
+ ImGui.InputFloat("Input", ref f, 0.1f);
+ ImGui.Combo("Combo", ref n, "Yes\0No\0Maybe\0\0");
+ ImGui.EndMenu();
+ }
+
+ if (ImGui.BeginMenu("Colors"))
+ {
+ float sz = ImGui.GetTextLineHeight();
+ //ImGui.Text(((int)ImGuiCol.COUNT).ToString()); //Test
+ for (int i = 0; i < (int)ImGuiCol.COUNT; i++)
+ {
+ string name = ImGui.GetStyleColorName((ImGuiCol)i);
+ Vec2 p = ImGui.GetCursorScreenPos();
+ ImGui.GetWindowDrawList().AddRectFilled(p, new Vec2(p.X + sz, p.Y + sz), ImGui.GetColorU32((ImGuiCol)i));
+ ImGui.Dummy(new Vec2(sz, sz));
+ ImGui.SameLine();
+ ImGui.MenuItem(name);
+ }
+ ImGui.EndMenu();
+ }
+
+ if (ImGui.BeginMenu("Options")) //Append!
+ {
+ bool b = true;
+ ImGui.Checkbox("SomeOption", ref b);
+ ImGui.EndMenu();
+ }
+
+ if (ImGui.BeginMenu("Disabled", false)) { } //Disabled
+ if (ImGui.MenuItem("Checked", null, true)) { }
+ if (ImGui.MenuItem("Quit", "Alt+F4")) { }
+ }
+ #endregion
+
+ #region DebugLog
+ static List<string> debug_log = new List<string>();
+ static bool AutoScroll = true;
+ unsafe private struct ImGuiDebugLog
+ {
+ //ImGuiTextBufferPtr Buf;
+ ImGuiTextFilterPtr Filter;
+ //ImVector<int> LineOffsets;
+ //bool AutoScroll;
+
+ public void DebugLog()
+ {
+ //AutoScroll = true;
+ //Buf = ImGuiNative.ImGuiTextBuffer_ImGuiTextBuffer();
+ var FilterPointer = ImGuiNative.ImGuiTextFilter_ImGuiTextFilter(null);
+ Filter = new ImGuiTextFilterPtr(FilterPointer);
+ }
+
+ public void Clear()
+ {
+ debug_log.Clear();
+ }
+
+ public void AddLog(string text)
+ {
+ //int old_size = this.Buf.Buf.Size;
+
+ debug_log.Add(text);
+
+ }
+
+ public void Destroy()
+ {
+ ImGuiNative.ImGuiTextFilter_destroy(Filter.NativePtr);
+ }
+
+ public void Draw(string title)
+ {
+ if (!ImGui.Begin(title))
+ {
+ ImGui.End();
+ return;
+ }
+
+ //Options menu
+ if (ImGui.BeginPopup("Options"))
+ {
+ ImGui.Checkbox("Auto-scroll", ref AutoScroll);
+ ImGui.EndPopup();
+ }
+
+ //Main window
+ if (ImGui.Button("Options"))
+ {
+ ImGui.OpenPopup("Options");
+ }
+ ImGui.SameLine();
+ bool clear = ImGui.Button("Clear");
+ ImGui.SameLine();
+ bool copy = ImGui.Button("Copy");
+ ImGui.SameLine();
+ Filter.Draw("Filter", -100.0f);
+
+ ImGui.Separator();
+ ImGui.BeginChild("scrolling", new Vec2(0, 0), ImGuiChildFlags.None, ImGuiWindowFlags.HorizontalScrollbar);
+
+ if (clear)
+ {
+ Clear();
+ }
+ if (copy)
+ {
+ ImGui.LogToClipboard();
+ }
+
+ ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vec2(0, 0));
+ if (Filter.IsActive())
+ {
+ foreach (string str in debug_log)
+ {
+ if (Filter.PassFilter(str))
+ {
+ //ImGui.TextUnformatted(str);
+ ImGui.BulletText(str);
+ }
+
+ }
+
+ }
+ else
+ {
+ foreach (string str in debug_log)
+ {
+ ImGui.TextUnformatted(str);
+ }
+ }
+ ImGui.PopStyleVar();
+
+ if (AutoScroll && ImGui.GetScrollY() >= ImGui.GetScrollMaxY())
+ {
+ ImGui.SetScrollHereY(1.0f);
+ }
+
+ ImGui.EndChild();
+ ImGui.End();
+
+ }
+ }
+
+ private void DrawImGuiDebugLog()
+ {
+ ImGuiDebugLog log = new ImGuiDebugLog();
+
+ ImGui.SetNextWindowSize(new Vec2(500, 400), ImGuiCond.FirstUseEver);
+ ImGui.Begin("Example: Log");
+ if (ImGui.SmallButton("[Debug] Add 5 entries"))
+ {
+ string[] words = { "Bumfuzzled", "Cattywampus", "Snickersnee", "Abibliophobia", "Absquatulate" };
+ foreach (string str in words)
+ {
+ log.AddLog("Frame " + ImGui.GetFrameCount() + " [info] Hello, current time is " + ImGui.GetTime() + " here's a word: " + str);
+ }
+ }
+ ImGui.End();
+
+ log.DebugLog();
+ log.Draw("Example: Log");
+ log.Destroy();
+ }
+ #endregion
+
+ #region AppConsole
+ static List<string> console_log = new List<string>();
+ static List<string> console_history = new List<string>();
+ static bool AutoScroll_Console = true;
+ unsafe private struct ImGuiExampleAppConsole
+ {
+ ImGuiTextFilterPtr Filter;
+
+ public void ExampleAppConsole()
+ {
+ var FilterPointer = ImGuiNative.ImGuiTextFilter_ImGuiTextFilter(null);
+ Filter = new ImGuiTextFilterPtr(FilterPointer);
+ }
+
+ public void ClearLog()
+ {
+ console_log.Clear();
+ }
+
+ public void AddLog(string text)
+ {
+ console_log.Add(text);
+ }
+
+ public void Destroy()
+ {
+ ImGuiNative.ImGuiTextFilter_destroy(Filter.NativePtr);
+ }
+
+ public void Draw(string title)
+ {
+ ImGui.SetNextWindowSize(new Vec2(520, 600), ImGuiCond.FirstUseEver);
+ if (!ImGui.Begin(title))
+ {
+ ImGui.End();
+ return;
+ }
+
+ if (ImGui.BeginPopupContextItem())
+ {
+ if (ImGui.MenuItem("Close Console"))
+ {
+ show_app_console = false;
+ }
+ ImGui.EndPopup();
+ }
+
+ ImGui.TextWrapped(
+ "This example implements a console with basic coloring" + //, completion "+//(TAB key) and history (Up/Down keys)
+ ". A more elaborate " +
+ "implementation may want to store entries along with extra data such as timestamp, emitter, etc.");
+ ImGui.TextWrapped("Enter 'HELP' for help.");
+
+ if (ImGui.SmallButton("Add Debug Text"))
+ {
+ AddLog(console_log.Count + " some text");
+ AddLog("some more text");
+ AddLog("display very important message here!");
+ }
+ ImGui.SameLine();
+ if (ImGui.SmallButton("Add Debug Error"))
+ {
+ AddLog("[error] something went wrong");
+ }
+ ImGui.SameLine();
+ if (ImGui.SmallButton("Clear"))
+ {
+ ClearLog();
+ }
+ ImGui.SameLine();
+ bool copy_to_clipboard = ImGui.SmallButton("Copy");
+ ImGui.Separator();
+
+ //Options menu
+ if (ImGui.BeginPopup("Options"))
+ {
+ ImGui.Checkbox("Auto-scroll", ref AutoScroll_Console);
+ ImGui.EndPopup();
+ }
+
+ //Filter
+ if (ImGui.Button("Options"))
+ {
+ ImGui.OpenPopup("Options");
+ }
+ ImGui.SameLine();
+ Filter.Draw("Filter (\"incl,-excl\") (\"error\")", 180.0f);
+ ImGui.Separator();
+
+ float footer_height_to_reserve = ImGui.GetStyle().ItemSpacing.Y + ImGui.GetFrameHeightWithSpacing();
+ ImGui.BeginChild("ScrollingRegion", new Vec2(0, -footer_height_to_reserve), ImGuiChildFlags.None, ImGuiWindowFlags.HorizontalScrollbar);
+ if (ImGui.BeginPopupContextWindow())
+ {
+ if (ImGui.Selectable("Clear"))
+ {
+ ClearLog();
+ }
+ ImGui.EndPopup();
+ }
+
+ if (copy_to_clipboard)
+ {
+ ImGui.LogToClipboard();
+ }
+
+ ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vec2(4, 1));
+ if (Filter.IsActive())
+ {
+ foreach (string str in console_log)
+ {
+ if (Filter.PassFilter(str))
+ {
+ Vec4 color;
+ bool has_color = false;
+ if (str.IndexOf("[error]") != -1)
+ {
+ color = new Vec4(1.0f, 0.4f, 0.4f, 1.0f);
+ ImGui.PushStyleColor(ImGuiCol.Text, color);
+ has_color = true;
+ }
+ if (str.IndexOf("# ") == 0)
+ {
+ color = new Vec4(1.0f, 0.8f, 0.6f, 1.0f);
+ ImGui.PushStyleColor(ImGuiCol.Text, color);
+ has_color = true;
+ }
+ ImGui.TextUnformatted(str);
+ if (has_color)
+ {
+ ImGui.PopStyleColor();
+ }
+ }
+
+ }
+ }
+ else
+ {
+ foreach (string str in console_log)
+ {
+ Vec4 color;
+ bool has_color = false;
+ if (str.IndexOf("[error]") != -1)
+ {
+ color = new Vec4(1.0f, 0.4f, 0.4f, 1.0f);
+ ImGui.PushStyleColor(ImGuiCol.Text, color);
+ has_color = true;
+ }
+ if (str.IndexOf("# ") == 0)
+ {
+ color = new Vec4(1.0f, 0.8f, 0.6f, 1.0f);
+ ImGui.PushStyleColor(ImGuiCol.Text, color);
+ has_color = true;
+ }
+ ImGui.TextUnformatted(str);
+ if (has_color)
+ {
+ ImGui.PopStyleColor();
+ }
+ }
+ }
+ ImGui.PopStyleVar();
+
+ if (AutoScroll_Console && ImGui.GetScrollY() >= ImGui.GetScrollMaxY())
+ {
+ ImGui.SetScrollHereY(1.0f);
+ }
+
+ ImGui.EndChild();
+ ImGui.Separator();
+
+ string input_buf = "";
+ bool reclaim_focus = false;
+ ImGuiInputTextFlags input_text_flags = ImGuiInputTextFlags.EnterReturnsTrue; //| ImGuiInputTextFlags.CallbackCompletion | ImGuiInputTextFlags.CallbackHistory;
+ if (ImGui.InputText("Input", ref input_buf, 250, input_text_flags))
+ {
+ ExecCommand(input_buf);
+ reclaim_focus = true;
+ }
+
+ ImGui.SetItemDefaultFocus();
+ if (reclaim_focus)
+ {
+ ImGui.SetKeyboardFocusHere(-1);
+ }
+
+ ImGui.End();
+
+ }
+
+ void ExecCommand(string command)
+ {
+ //History
+ if (console_history.Count == 10)
+ {
+ console_history.RemoveAt(0);
+ }
+ console_history.Add(command);
+
+ //Show Input
+ AddLog("# " + command);
+
+ //Commands
+ if (command == "CLEAR")
+ {
+ ClearLog();
+ }
+ else if (command == "HELP")
+ {
+ AddLog("Commands:");
+ AddLog("- CLEAR");
+ AddLog("- HELP");
+ AddLog("- HISTORY");
+ }
+ else if (command == "HISTORY")
+ {
+ int history_pos = 0;
+ foreach (string item in console_history)
+ {
+ AddLog(" " + history_pos.ToString() + ": " + item);
+ history_pos++;
+ }
+ }
+ else
+ {
+ AddLog("Unknown command: '" + command + "'");
+ }
+
+ }
+
+ }
+
+ private void DrawImGuiExampleAppConsole()
+ {
+ string console_window_name = "Example: Console";
+ ImGuiExampleAppConsole console = new ImGuiExampleAppConsole();
+
+
+ //ImGui.Begin(console_window_name);
+ //if (ImGui.SmallButton("[Debug] Add 5 entries"))
+ //{
+ // string[] words = { "Bumfuzzled", "Cattywampus", "Snickersnee", "Abibliophobia", "Absquatulate" };
+ // foreach (string str in words)
+ // {
+ // console.AddLog("Frame " + ImGui.GetFrameCount() + " [info] Hello, current time is " + ImGui.GetTime() + " here's a word: " + str);
+ // }
+ //}
+ //ImGui.End();
+
+ console.ExampleAppConsole();
+ console.Draw(console_window_name);
+ console.Destroy();
+ }
+ #endregion
+
+ #region DrawDemoWindowVariables
+ // Window options
+ bool no_titlebar = false;
+ bool no_scrollbar = false;
+ bool no_menu = false;
+ bool no_move = false;
+ bool no_resize = false;
+ bool no_collapse = false;
+ //bool no_close = false;
+ bool no_nav = false;
+ bool no_background = false;
+ bool no_bring_to_front = false;
+ // Examples
+ bool show_app_main_menu_bar = false;
+ bool show_app_log = false;
+ bool show_app_simple_overlay = false;
+ bool show_native_examples = false;
+ static bool show_app_console = false;
+ bool show_monogame_settings = true;
+ // Tools
+ bool show_app_style_editor = false;
+ bool show_app_metrics = false;
+ bool show_app_about = false;
+ #endregion
+
+ private void DrawDemoWindow()
+ {
+
+ ImGuiWindowFlags windowFlags = 0;
+ if (no_titlebar) { windowFlags |= ImGuiWindowFlags.NoTitleBar; }
+ if (no_scrollbar) { windowFlags |= ImGuiWindowFlags.NoScrollbar; }
+ if (!no_menu) { windowFlags |= ImGuiWindowFlags.MenuBar; }
+ if (no_move) { windowFlags |= ImGuiWindowFlags.NoMove; }
+ if (no_resize) { windowFlags |= ImGuiWindowFlags.NoResize; }
+ if (no_collapse) { windowFlags |= ImGuiWindowFlags.NoCollapse; }
+ if (no_nav) { windowFlags |= ImGuiWindowFlags.NoNav; }
+ if (no_background) { windowFlags |= ImGuiWindowFlags.NoBackground; }
+ if (no_bring_to_front) { windowFlags |= ImGuiWindowFlags.NoBringToFrontOnFocus; }
+ //if (no_close) { } // p_open = null;
+ //bool p_open = true;
+
+ ImGui.SetNextWindowPos(new Vec2(450, 20), ImGuiCond.FirstUseEver);
+ ImGui.SetNextWindowSize(new Vec2(550, 680), ImGuiCond.FirstUseEver);
+
+ //Main body
+ if (!ImGui.Begin("Dear ImGui Demo / Monogame & ImGui.Net", windowFlags)) //ref p_open, windowFlags)) Close button is not changing bool!?
+ {
+ ImGui.End();
+ return;
+ }
+
+ ImGui.PushItemWidth(ImGui.GetFontSize() * -12);
+
+ //Menu bar
+ if (ImGui.BeginMenuBar())
+ {
+ if (ImGui.BeginMenu("Menu"))
+ {
+ ShowExampleMenuFile();
+ ImGui.EndMenu();
+ }
+ if (ImGui.BeginMenu("Examples"))
+ {
+ ImGui.MenuItem("Main menu bar", null, ref show_app_main_menu_bar);
+ ImGui.MenuItem("Console", null, ref show_app_console);
+ ImGui.MenuItem("Log", null, ref show_app_log);
+ //ImGui.MenuItem("Simple Layout", null, true);
+ //ImGui.MenuItem("Property editor", null, true);
+ //ImGui.MenuItem("Long text display", null, true);
+ //ImGui.MenuItem("Auto-resizing window", null, true);
+ //ImGui.MenuItem("Constrained-resizing window", null, true);
+ ImGui.MenuItem("Simple overlay", null, ref show_app_simple_overlay);
+ //ImGui.MenuItem("Fullscreen window", null, true);
+ //ImGui.MenuItem("Manipulating window titles", null, true);
+ //ImGui.MenuItem("Custom rendering", null, true);
+ //ImGui.MenuItem("Documents", null, true);
+ ImGui.MenuItem("Native ImGui Demos", null, ref show_native_examples);
+ ImGui.MenuItem("MonoGame Settings", null, ref show_monogame_settings);
+ ImGui.EndMenu();
+ }
+ if (ImGui.BeginMenu("Tools"))
+ {
+ ImGui.MenuItem("Metrics/Debugger", null, ref show_app_metrics);
+ ImGui.MenuItem("Style Editor", null, ref show_app_style_editor);
+ ImGui.MenuItem("About Dear ImGui", null, ref show_app_about);
+ ImGui.EndMenu();
+ }
+ ImGui.EndMenuBar();
+ }
+
+ ImGui.Text("dear imgui says hello. (" + ImGui.GetVersion() + ")");
+ ImGui.Spacing();
+
+ if (ImGui.CollapsingHeader("Help"))
+ {
+ ImGui.Text("ABOUT THIS DEMO:");
+ ImGui.BulletText("Sections below are demonstrating many aspects of the library.");
+ ImGui.BulletText("The \"Examples\" menu above leads to more demo contents.");
+ ImGui.BulletText("The \"Tools\" menu above gives access to: About Box, Style Editor,\nand Metrics/Debugger (general purpose Dear ImGui debugging tool).");
+ ImGui.Separator();
+
+ ImGui.Text("PROGRAMMER GUIDE:");
+ ImGui.BulletText("See the ShowDemoWindow() code in imgui_demo.cpp. <- you are here!");
+ ImGui.BulletText("See comments in imgui.cpp.");
+ ImGui.BulletText("See example applications in the examples/ folder.");
+ ImGui.BulletText("Read the FAQ at http://www.dearimgui.org/faq/");
+ ImGui.BulletText("Set 'io.ConfigFlags |= NavEnableKeyboard' for keyboard controls.");
+ ImGui.BulletText("Set 'io.ConfigFlags |= NavEnableGamepad' for gamepad controls.");
+ ImGui.Separator();
+
+ ImGui.Text("USER GUIDE:");
+ ImGui.ShowUserGuide();
+ }
+
+ if (ImGui.CollapsingHeader("Configuration"))
+ {
+ if (ImGui.TreeNode("Style"))
+ {
+ HelpMarker("The same contents can be accessed in 'Tools->Style Editor' or by calling the ShowStyleEditor() function.");
+ ImGui.ShowStyleEditor();
+ ImGui.TreePop();
+ ImGui.Separator();
+ }
+
+ if (ImGui.TreeNode("Capture/Logging"))
+ {
+ HelpMarker("The logging API redirects all text output so you can easily capture the content of a window or a block. Tree nodes can be automatically expanded.\nTry opening any of the contents below in this window and then click one of the \"Log To\" button.");
+ ImGui.LogButtons();
+
+ HelpMarker("You can also call ImGui.LogText() to output directly to the log without a visual output.");
+ if (ImGui.Button("Copy \"Hello, world!\" to clipboard"))
+ {
+ ImGui.LogToClipboard();
+ ImGui.LogText("Hello, world!");
+ ImGui.LogFinish();
+ }
+ ImGui.TreePop();
+ }
+ }
+
+ if (ImGui.CollapsingHeader("Window options"))
+ {
+ //if(ImGui.BeginTable // feature of imgui 1.75?
+ ImGui.Checkbox("No titlebar", ref no_titlebar);
+ ImGui.Checkbox("No scrollbar", ref no_scrollbar);
+ ImGui.Checkbox("No menu", ref no_menu);
+ ImGui.Checkbox("No move", ref no_move);
+ ImGui.Checkbox("No resize", ref no_resize);
+ ImGui.Checkbox("No collapse", ref no_collapse);
+ //ImGui.Checkbox("No close", ref no_close);
+ ImGui.Checkbox("No nav", ref no_nav);
+ ImGui.Checkbox("No background", ref no_background);
+ ImGui.Checkbox("No bring to front", ref no_bring_to_front);
+ }
+
+ DemoWindowWidgets();
+ DemoWindowLayout();
+ DemoWindowPopups();
+ //ShowDemoWindowTables();
+ DemoWindowMisc();
+
+ ImGui.PopItemWidth();
+ ImGui.End();
+ }
+
+ private void HelpMarker(string desc)
+ {
+ ImGui.TextDisabled("(?)");
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.BeginTooltip();
+ ImGui.PushTextWrapPos(ImGui.GetFontSize() * 35.0f);
+ ImGui.TextUnformatted(desc);
+ ImGui.PopTextWrapPos();
+ ImGui.EndTooltip();
+ }
+ }
+
+ #region DemoWindowWidgetsVariables
+ //Basic Input
+ int clicked = 0;
+ bool check = true;
+ int e = 0;
+ int counter = 0;
+ int item_current = 0;
+ string str0 = "Hello, world!";
+ string str1 = "";
+ int i0 = 123;
+ float f0 = 0.001f;
+ double d0 = 999999.00000001;
+ float f1 = 1.42f;
+ Vec3 vec3 = new Vec3(0.10f, 0.20f, 0.30f);
+ //Basic Drag & Slider
+ int i1 = 50, i2 = 42;
+ float f2 = 1.00f, f3 = 0.0067f;
+ int i3 = 0;
+ float f4 = 0.123f;
+ float angle = 0.0f;
+ Vec3 col1 = new Vec3(1.0f, 0.0f, 0.2f);
+ Vec4 col2 = new Vec4(0.4f, 0.7f, 0.0f, 0.5f);
+ int current_fruit = 1;
+ //Trees
+ bool base_flags_first_run = true;
+ uint base_flags = 0;
+ bool align_label_with_current_x_position = false;
+ int index_selected = 0;
+ //bool test_drag_and_drop = false;
+ //Collapsing headers
+ bool closable_group = true;
+ //Word wrapping
+ float wrap_width = 200.0f;
+ //Images
+ int pressed_count = 0;
+ //Combo
+ uint flags = 0; //ImGuiComboFlags
+ int item_current_idx = 0;
+ int item_current_2 = 0;
+ int item_current_3 = -1;
+ //List boxes
+ int item_current_idx_lb = 0;
+ //Selectables
+ bool[] selection = { false, true, false, false, false };
+ int selected = -1;
+ bool[] selection_ms = { false, false, false, false, false };
+ bool[] selected_rend = { false, false, false };
+ bool[] selected_align = { true, false, true, false, true, false, true, false, true };
+ //Text input
+ bool flags_ti_first_run = true;
+ uint flags_ti = 0;
+ string buf1 = "";
+ string buf2 = "";
+ string buf3 = "";
+ string buf4 = "";
+ string buf5 = "";
+ string password = "password123";
+ //Tabs
+ bool flags_tabs_first_run = true;
+ uint tab_bar_flags = 0;
+ bool[] opened = { true, true, true, true };
+ //Plots Widgets
+ bool animate = true;
+ float[] values = new float[90];
+ int values_offset = 0;
+ double refresh_time = 0.0;
+ float phase = 0.0f;
+ float progress = 0.0f, progress_dir = 1.0f;
+ //Color Widgets
+ Vec3 color_vec3 = new Vec3(114.0f / 255.0f, 144.0f / 255.0f, 154 / 255.0f);
+ Vec4 color_vec4 = new Vec4(114.0f / 255.0f, 144.0f / 255.0f, 154 / 255.0f, 200.0f / 255.0f);
+ bool alpha_preview = true;
+ bool alpha_half_preview = false;
+ bool drag_and_drop = true;
+ bool options_menu = true;
+ bool hdr = false;
+ bool alpha = true;
+ bool alpha_bar = true;
+ bool side_preview = true;
+ bool ref_color = false;
+ Vec4 ref_color_v = new Vec4(1.0f, 0.0f, 1.0f, 0.5f);
+ int display_mode = 0;
+ int picker_mode = 0;
+ Vec4 color_hsv = new Vec4(0.23f, 1.0f, 1.0f, 1.0f);
+ //Range Widgets
+ float begin = 10, end = 90;
+ int begin_i = 100, end_i = 1000;
+ //Multi component Widgets
+ Vec2 vec2f = new Vec2(0.10f, 0.20f);
+ Vec3 vec3f = new Vec3(0.10f, 0.20f, 0.30f);
+ Vec4 vec4f = new Vec4(0.10f, 0.20f, 0.30f, 0.44f);
+ int[] vec4i = { 1, 5, 100, 255 };
+ //Vertical Sliders
+ float spacing = 4;
+ int int_value = 0;
+ float[] values_vert = { 0.0f, 0.60f, 0.35f, 0.9f, 0.70f, 0.20f, 0.0f };
+ float col_red = 1.0f;
+ float col_green = 1.0f;
+ float col_blue = 1.0f;
+ float[] values2 = { 0.20f, 0.80f, 0.40f, 0.25f };
+ #endregion
+
+ private void DemoWindowWidgets()
+ {
+ if (!ImGui.CollapsingHeader("Widgets"))
+ {
+ return;
+ }
+
+ //Basic
+ #region Basic
+ if (ImGui.TreeNode("Basic"))
+ {
+
+ if (ImGui.Button("Button"))
+ {
+ clicked++;
+ if (clicked == 2)
+ {
+ clicked = 0;
+ }
+ }
+ if (clicked == 1)
+ {
+ ImGui.SameLine();
+ ImGui.Text("Thanks for clicking me!");
+ }
+
+ ImGui.Checkbox("checkbox", ref check);
+
+ ImGui.RadioButton("radio a", ref e, 0);
+ ImGui.SameLine();
+ ImGui.RadioButton("radio b", ref e, 1);
+ ImGui.SameLine();
+ ImGui.RadioButton("radio c", ref e, 2);
+
+ for (int i = 0; i < 7; i++)
+ {
+ if (i > 0)
+ {
+ ImGui.SameLine();
+ }
+ ImGui.PushID(i);
+ //ImColorPtr color = new ImColorPtr();
+ //ImGuiNative.ImColor_HSV ?
+ ImGui.PushStyleColor(ImGuiCol.Button, new Vec4(i / 7.0f, 0.6f, 0.6f, 0.6f));
+ ImGui.PushStyleColor(ImGuiCol.ButtonHovered, new Vec4(i / 7.0f, 0.7f, 0.7f, 0.7f));
+ ImGui.PushStyleColor(ImGuiCol.ButtonActive, new Vec4(i / 7.0f, 0.8f, 0.8f, 0.8f));
+ ImGui.Button("Click");
+ ImGui.PopStyleColor(3);
+ ImGui.PopID();
+ }
+
+ ImGui.AlignTextToFramePadding();
+ ImGui.Text("Hold to repeat:");
+ ImGui.SameLine();
+
+ float spacing = ImGui.GetStyle().ItemInnerSpacing.X;
+ ImGui.PushButtonRepeat(true);
+ if (ImGui.ArrowButton("##left", ImGuiDir.Left)) { counter--; }
+ ImGui.SameLine(0.0f, spacing);
+ if (ImGui.ArrowButton("##right", ImGuiDir.Right)) { counter++; }
+ ImGui.PopButtonRepeat();
+ ImGui.SameLine();
+ ImGui.Text(counter.ToString());
+
+ ImGui.Text("Hover over me");
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("I am a tooltip");
+ }
+
+ ImGui.SameLine();
+ ImGui.Text("- or me");
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.BeginTooltip();
+ ImGui.Text("I am a fancy tooltip");
+ float[] arr = { 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f };
+ ImGui.PlotLines("Curve", ref arr[0], arr.Length);
+ ImGui.EndTooltip();
+ }
+
+ ImGui.Separator();
+ ImGui.LabelText("label", "Value");
+
+ //combo box
+ string items = "AAAA\0BBBB\0CCCC\0DDDD\0EEEE\0FFFF\0GGGG\0HHHH\0IIIIIII\0JJJJ\0KKKKKKK";
+ ImGui.Combo("combo", ref item_current, items, 11);
+
+ //input
+ ImGui.InputText("input text", ref str0, 128);
+ ImGui.SameLine();
+ HelpMarker("USER:\n" +
+ "Hold SHIFT or use mouse to select text.\n" +
+ "CTRL+Left/Right to word jump.\n" +
+ "CTRL+A or double-click to select all.\n" +
+ "CTRL+X,CTRL+C,CTRL+V clipboard.\n" +
+ "CTRL+Z,CTRL+Y undo/redo.\n" +
+ "ESCAPE to revert.\n\n" +
+ "PROGRAMMER:\n" +
+ "You can use the ImGuiInputTextFlags_CallbackResize facility if you need to wire InputText() " +
+ "to a dynamic string type. See misc/cpp/imgui_stdlib.h for an example (this is not demonstrated " +
+ "in imgui_demo.cpp).");
+
+ ImGui.InputTextWithHint("input text (w/ hint", "enter text here", ref str1, 10);
+
+ ImGui.InputInt("input int", ref i0);
+ ImGui.SameLine();
+ HelpMarker("You can apply arithmetic operators +,*,/ on numerical values.\n" +
+ " e.g. [ 100 ], input \'*2\', result becomes [ 200 ]\n" +
+ "Use +- to subtract.");
+
+ ImGui.InputFloat("input float", ref f0, 0.01f, 1.0f, "%.3f");
+
+ ImGui.InputDouble("input double", ref d0, 0.01f, 1.0f, "%.8f");
+
+ ImGui.InputFloat("input scientific", ref f1, 0.0f, 0.0f, "%e");
+ ImGui.SameLine();
+ HelpMarker("You can input value using the scientific notation,\n" +
+ " e.g. \"1e+8\" becomes \"100000000\".");
+
+ ImGui.InputFloat3("input float 3", ref vec3);
+
+ //drag
+ ImGui.DragInt("drag int", ref i1, 1);
+ ImGui.SameLine();
+ HelpMarker("Click and drag to edit value.\n" +
+ "Hold SHIFT/ALT for faster/slower edit.\n" +
+ "Double-click or CTRL+click to input value.");
+
+ ImGui.DragInt("drag int 0..100", ref i2, 1, 0, 100, "%d%%");
+
+ ImGui.DragFloat("drag float", ref f2, 0.005f);
+ ImGui.DragFloat("drag small float", ref f3, 0.0001f, 0.0f, 0.0f, "%.06f ns");
+
+ ImGui.SliderInt("slider int", ref i3, -1, 3);
+ ImGui.SameLine();
+ HelpMarker("CTRL+click to input value.");
+
+ ImGui.SliderFloat("slider float", ref f4, 0.0f, 1.0f, "ratio = %.3f");
+
+ ImGui.SliderAngle("slider angle", ref angle);
+
+ //color
+ ImGui.ColorEdit3("color 1", ref col1);
+ ImGui.SameLine();
+ HelpMarker("Click on the color square to open a color picker.\n" +
+ "Click and hold to use drag and drop.\n" +
+ "Right-click on the color square to show options.\n" +
+ "CTRL+click on individual component to input value.\n");
+
+ ImGui.ColorEdit4("color 2", ref col2);
+
+ string[] fruits = { "Apple", "Banana", "Cherry", "Kiwi", "Mango", "Orange", "Pineapple", "Strawberry", "Watermelon" };
+ ImGui.ListBox("listbox", ref current_fruit, fruits, fruits.Length);
+ ImGui.SameLine();
+ HelpMarker("Using the simplified one-liner ListBox API here.\nRefer to the \"List boxes\" section below for an explanation of how to use the more flexible and general BeginListBox/EndListBox API.");
+
+ ImGui.TreePop();
+ }
+ #endregion
+
+ //Trees
+ #region Trees
+ if (ImGui.TreeNode("Trees"))
+ {
+ if (ImGui.TreeNode("Basic trees"))
+ {
+ for (int i = 0; i < 5; i++)
+ {
+ if (i == 0)
+ {
+ ImGui.SetNextItemOpen(true, ImGuiCond.Once);
+ }
+
+ if (ImGui.TreeNode(i.ToString(), "Child " + i.ToString()))
+ {
+ ImGui.Text("blah blah");
+ ImGui.SameLine();
+ if (ImGui.SmallButton("button")) { }
+ ImGui.TreePop();
+ }
+ }
+ ImGui.TreePop();
+ }
+
+ if (ImGui.TreeNode("Advanced, with Selectable nodes"))
+ {
+ HelpMarker(
+ "This is a more typical looking tree with selectable nodes.\n" +
+ "Click to select, CTRL+Click to toggle, click on arrows or double-click to open.");
+
+ if (base_flags_first_run)
+ {
+ ImGuiTreeNodeFlags _base_flags = ImGuiTreeNodeFlags.OpenOnArrow | ImGuiTreeNodeFlags.OpenOnDoubleClick;
+ base_flags = (uint)_base_flags;
+ base_flags_first_run = false;
+ }
+
+ ImGui.CheckboxFlags("ImGuiTreeNodeFlags_OpenOnArrow", ref base_flags, (uint)ImGuiTreeNodeFlags.OpenOnArrow);
+ ImGui.CheckboxFlags("ImGuiTreeNodeFlags_OpenOnDoubleClick", ref base_flags, (uint)ImGuiTreeNodeFlags.OpenOnDoubleClick);
+ ImGui.Checkbox("Align label with current X position", ref align_label_with_current_x_position);
+ //ImGui.Checkbox("Test tree node as drag source", ref test_drag_and_drop);
+ ImGui.Text("Hello!");
+ if (align_label_with_current_x_position)
+ {
+ ImGui.Unindent(ImGui.GetTreeNodeToLabelSpacing());
+ }
+
+ for (int i = 0; i < 6; i++)
+ {
+ ImGuiTreeNodeFlags node_flags = (ImGuiTreeNodeFlags)base_flags;
+ if (i == index_selected)
+ {
+ node_flags |= ImGuiTreeNodeFlags.Selected;
+ }
+
+ if (i < 3)
+ {
+ if (ImGui.TreeNodeEx(i.ToString(), node_flags, "Selectable Node " + i.ToString()))
+ {
+ ImGui.BulletText("Blah blah\nBlah Blah");
+ ImGui.TreePop();
+ }
+ }
+ else
+ {
+ node_flags |= ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen;
+ ImGui.TreeNodeEx(i.ToString(), node_flags, "Selectable Leaf " + i.ToString());
+ }
+
+ if (ImGui.IsItemClicked())
+ {
+ index_selected = i;
+ }
+ }
+
+
+ ImGui.TreePop();
+
+ }
+ ImGui.TreePop();
+ }
+ #endregion
+
+ //Collapsing Headers
+ #region Collapsing Headers & Bullets
+ if (ImGui.TreeNode("Collapsing Headers"))
+ {
+ ImGui.Checkbox("Show 2nd header", ref closable_group);
+ if (ImGui.CollapsingHeader("Header", ImGuiTreeNodeFlags.None))
+ {
+ ImGui.Text("IsItemHovered: " + ImGui.IsItemHovered());
+ for (int i = 0; i < 5; i++)
+ {
+ ImGui.Text("Some content " + i);
+ }
+ }
+ if (ImGui.CollapsingHeader("Header with a close button", ref closable_group))
+ {
+ ImGui.Text("IsItemHovered: " + ImGui.IsItemHovered());
+ for (int i = 0; i < 5; i++)
+ {
+ ImGui.Text("More content " + i);
+ }
+ }
+ ImGui.TreePop();
+ }
+
+ //Bullets
+ if (ImGui.TreeNode("Bullets"))
+ {
+ ImGui.BulletText("Bullet point 1");
+ ImGui.BulletText("Bullet point 2\nOn multiple lines");
+ if (ImGui.TreeNode("Tree node"))
+ {
+ ImGui.BulletText("Another bullet point");
+ ImGui.TreePop();
+ }
+ ImGui.Bullet();
+ ImGui.Text("Bullet point 3 (two calls)");
+ ImGui.Bullet();
+ ImGui.SmallButton("Button");
+ ImGui.TreePop();
+ }
+ #endregion
+
+ //Text
+ #region Text
+ if (ImGui.TreeNode("Text"))
+ {
+ if (ImGui.TreeNode("Colorful Text"))
+ {
+ ImGui.TextColored(new Vec4(1.0f, 0.0f, 1.0f, 1.0f), "Pink");
+ ImGui.TextColored(new Vec4(1.0f, 1.0f, 0.0f, 1.0f), "Yellow");
+ ImGui.TextDisabled("Disabled");
+ ImGui.SameLine();
+ HelpMarker("The TextDisabled color is stored in ImGuiStyle.");
+ ImGui.TreePop();
+ }
+
+
+ if (ImGui.TreeNode("Word Wrapping"))
+ {
+ ImGui.TextWrapped("This text should automatically wrap on the edge of the window. The current implementation " +
+ "for text wrapping follows simple rules suitable for English and possibly other languages.");
+ ImGui.Spacing();
+
+ ImGui.SliderFloat("Wrap width", ref wrap_width, -20, 600, "%.0f");
+
+ ImDrawListPtr draw_list = ImGui.GetWindowDrawList();
+ for (int n = 0; n < 2; n++)
+ {
+ ImGui.Text("Test paragraph " + n);
+ Vec2 pos = ImGui.GetCursorPos();
+ Vec2 marker_min = new Vec2(pos.X + wrap_width, pos.Y);
+ Vec2 marker_max = new Vec2(pos.X + wrap_width + 10, pos.Y + ImGui.GetTextLineHeight());
+ ImGui.PushTextWrapPos(ImGui.GetCursorPos().X + wrap_width);
+ if (n == 0)
+ {
+ ImGui.Text(string.Format("The lazy dog is a good dog. This paragraph should fit within {0} pixels. Testing a 1 character word. The quick brown fox jumps over the lazy dog.", wrap_width));
+ }
+ else
+ {
+ ImGui.Text("aaaaaaaa bbbbbbbb, c cccccccc,dddddddd. d eeeeeeee ffffffff. gggggggg!hhhhhhhh");
+ }
+
+ Vec4 colf = new Vec4(255.0f, 255.0f, 0.0f, 255.0f);
+ uint col = ImGui.ColorConvertFloat4ToU32(colf); //ImGuiNative.igColorConvertFloat4ToU32(colf);
+
+ draw_list.AddRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), col);
+
+ Vec4 colf2 = new Vec4(255.0f, 0.0f, 255.0f, 255.0f);
+ uint col2 = ImGui.ColorConvertFloat4ToU32(colf2); //ImGuiNative.igColorConvertFloat4ToU32(colf2);
+
+ draw_list.AddRectFilled(marker_min, marker_max, col2);
+
+ ImGui.PopTextWrapPos();
+
+ }
+ ImGui.TreePop();
+ }
+ ImGui.TreePop();
+ }
+ #endregion
+
+ //Images
+ #region Images
+ if (ImGui.TreeNode("Images"))
+ {
+ ImGuiIOPtr io = ImGui.GetIO();
+ ImGui.TextWrapped("Below we are displaying the font texture (which is the only texture we have access to in this demo). " +
+ "Use the 'ImTextureID' type as storage to pass pointers or identifier to your own texture data. " +
+ "Hover the texture for a zoomed view!");
+
+ System.IntPtr my_tex_id = io.Fonts.TexID;
+ float my_tex_w = (float)io.Fonts.TexWidth;
+ float my_tex_h = (float)io.Fonts.TexHeight;
+
+ ImGui.Text(my_tex_w + "x" + my_tex_h);
+ Vec2 pos = ImGui.GetCursorScreenPos();
+ Vec2 uv_min = new Vec2(0.0f, 0.0f); // top left
+ Vec2 uv_max = new Vec2(1.0f, 1.0f); // lower right
+ Vec4 tint_col = new Vec4(1.0f, 1.0f, 1.0f, 1.0f); // no tint
+ Vec4 border_col = new Vec4(1.0f, 1.0f, 1.0f, 0.5f); // 50% opaque white
+
+ ImGui.Image(my_tex_id, new Vec2(my_tex_w, my_tex_h), uv_min, uv_max, tint_col, border_col);
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.BeginTooltip();
+ float region_sz = 32.0f;
+ float region_x = io.MousePos.X - pos.X - region_sz * 0.5f;
+ float region_y = io.MousePos.Y - pos.Y - region_sz * 0.5f;
+ float zoom = 4.0f;
+ if (region_x < 0.0f) { region_x = 0.0f; }
+ else if (region_x > my_tex_w - region_sz) { region_x = my_tex_w - region_sz; }
+ if (region_y < 0.0f) { region_y = 0.0f; }
+ else if (region_y > my_tex_h - region_sz) { region_y = my_tex_h - region_sz; }
+ ImGui.Text(string.Format("Min: ({0},{1})", region_x, region_y));
+ ImGui.Text(string.Format("Max: ({0},{1})", region_x + region_sz, region_y + region_sz));
+ Vec2 uv0 = new Vec2((region_x) / my_tex_w, (region_y) / my_tex_h);
+ Vec2 uv1 = new Vec2((region_x + region_sz) / my_tex_w, (region_y + region_sz) / my_tex_h);
+ ImGui.Image(my_tex_id, new Vec2(region_sz * zoom, region_sz * zoom), uv0, uv1, tint_col, border_col);
+ ImGui.EndTooltip();
+ }
+
+ ImGui.TextWrapped("And now some textured buttons..");
+ for (int i = 0; i < 8; i++)
+ {
+ ImGui.PushID(i);
+ int frame_padding = -1 + i;
+ Vec2 size = new Vec2(32.0f, 32.0f);
+ Vec2 uv0 = new Vec2(0.0f, 0.0f);
+ Vec2 uv1 = new Vec2(32.0f / my_tex_w, 32.0f / my_tex_h);
+ Vec4 bg_col = new Vec4(0.0f, 0.0f, 0.0f, 1.0f); // black background
+ Vec4 tint_col2 = new Vec4(1.0f, 1.0f, 1.0f, 1.0f); // no tint
+ if (ImGui.ImageButton($"textured_button_id_{i}",my_tex_id, size, uv0, uv1, bg_col, tint_col2))
+ {
+ pressed_count += 1;
+ }
+ ImGui.PopID();
+ ImGui.SameLine();
+ }
+ ImGui.NewLine();
+ ImGui.Text(string.Format("Pressed {0} times.", pressed_count));
+ ImGui.TreePop();
+ }
+ #endregion
+
+ //Combo
+ #region Combo & List boxes
+ if (ImGui.TreeNode("Combo"))
+ {
+ ImGui.CheckboxFlags("ImGuiComboFlags_PopupAlignLeft", ref flags, (uint)ImGuiComboFlags.PopupAlignLeft);
+ ImGui.SameLine();
+ HelpMarker("Only makes a difference if the popup is larger than the combo");
+ if (ImGui.CheckboxFlags("ImGuiComboFlags_NoArrowButton", ref flags, (uint)ImGuiComboFlags.NoArrowButton))
+ {
+ flags &= ~(uint)ImGuiComboFlags.NoPreview; // clear the other flag
+ }
+ if (ImGui.CheckboxFlags("ImGuiComboFlags_NoPreview", ref flags, (uint)ImGuiComboFlags.NoPreview))
+ {
+ flags &= ~(uint)ImGuiComboFlags.NoArrowButton; // clear the other flag
+ }
+
+ string[] items = { "AAAA", "BBBB", "CCCC", "DDDD", "EEEE", "FFFF", "GGGG", "HHHH", "IIII", "JJJJ", "KKKK", "LLLLLLL", "MMMM", "OOOOOOO" };
+ string combo_label = items[item_current_idx];
+ ImGuiComboFlags flags_cf = (ImGuiComboFlags)flags;
+ if (ImGui.BeginCombo("combo 1", combo_label, flags_cf))
+ {
+ for (int n = 0; n < items.Length; n++)
+ {
+ bool is_selected = (item_current_idx == n);
+ if (ImGui.Selectable(items[n], is_selected))
+ {
+ item_current_idx = n;
+ }
+
+ if (is_selected)
+ {
+ ImGui.SetItemDefaultFocus();
+ }
+ }
+ ImGui.EndCombo();
+ }
+
+ ImGui.Combo("combo 2 (one-liner)", ref item_current_2, "aaaa\0bbbb\0cccc\0dddd\0eeee\0\0");
+
+ ImGui.Combo("combo 3 (array)", ref item_current_3, items, items.Length);
+
+ ImGui.TreePop();
+ }
+
+ //List boxes
+ if (ImGui.TreeNode("List boxes"))
+ {
+ string[] items = { "AAAA", "BBBB", "CCCC", "DDDD", "EEEE", "FFFF", "GGGG", "HHHH", "IIII", "JJJJ", "KKKK", "LLLLLLL", "MMMM", "OOOOOOO" };
+ if (ImGui.ListBox("listbox 1", ref item_current_idx_lb, items, items.Length)) { }
+
+ ImGui.TreePop();
+ }
+ #endregion
+
+ //Selectables
+ #region Selectables
+ if (ImGui.TreeNode("Selectables"))
+ {
+ if (ImGui.TreeNode("Basic"))
+ {
+ ImGui.Selectable("1. I am selectable", ref selection[0]);
+ ImGui.Selectable("2. I am selectable", ref selection[1]);
+ ImGui.Text("3. I am not selectable");
+ ImGui.Selectable("4. I am selectable", ref selection[3]);
+ if (ImGui.Selectable("5. I am double clickable", ref selection[4], ImGuiSelectableFlags.AllowDoubleClick))
+ {
+ if (ImGui.IsMouseDoubleClicked(0))
+ {
+ selection[4] = !selection[4];
+ }
+ }
+ ImGui.TreePop();
+ }
+ if (ImGui.TreeNode("Selection State: Single Selection"))
+ {
+ for (int n = 0; n < 5; n++)
+ {
+ string buf = string.Format("Object {0}", n);
+ if (ImGui.Selectable(buf, selected == n))
+ {
+ selected = n;
+ }
+ }
+ ImGui.TreePop();
+ }
+ if (ImGui.TreeNode("Selection State: Multiple Selection"))
+ {
+ HelpMarker("Hold CTRL and click to select multiple items.");
+
+ for (int n = 0; n < 5; n++)
+ {
+ string buf = string.Format("Object {0}", n);
+ if (ImGui.Selectable(buf, selection_ms[n]))
+ {
+ if (!ImGui.GetIO().KeyCtrl)
+ {
+ for (int r = 0; r < selection_ms.Length; r++)
+ {
+ selection_ms[r] = false;
+ }
+ }
+ selection_ms[n] ^= true;
+ }
+ }
+ ImGui.TreePop();
+ }
+ if (ImGui.TreeNode("Rendering more text into the same line"))
+ {
+ ImGui.Selectable("main.c", ref selected_rend[0]); ImGui.SameLine(300); ImGui.Text(" 2,345 bytes");
+ ImGui.Selectable("Hello.cpp", ref selected_rend[1]); ImGui.SameLine(300); ImGui.Text("12,345 bytes");
+ ImGui.Selectable("Hello.h", ref selected_rend[2]); ImGui.SameLine(300); ImGui.Text(" 2,345 bytes");
+ ImGui.TreePop();
+ }
+ if (ImGui.TreeNode("Alignment"))
+ {
+ HelpMarker("By default, Selectables uses style.SelectableTextAlign but it can be overridden on a per-item " +
+ "basis using PushStyleVar(). You'll probably want to always keep your default situation to " +
+ "left-align otherwise it becomes difficult to layout multiple items on a same line");
+
+ for (int y = 0; y < 3; y++)
+ {
+ for (int x = 0; x < 3; x++)
+ {
+ Vec2 alignment = new Vec2((float)x / 2.0f, (float)y / 2.0f);
+ string name = string.Format("({0},{1})", alignment.X, alignment.Y);
+ if (x > 0) { ImGui.SameLine(); }
+ ImGui.PushStyleVar(ImGuiStyleVar.SelectableTextAlign, alignment);
+ ImGui.Selectable(name, ref selected_align[3 * y + x], ImGuiSelectableFlags.None, new Vec2(80, 80));
+ ImGui.PopStyleVar();
+ }
+ }
+ ImGui.TreePop();
+ }
+ ImGui.TreePop();
+ }
+ #endregion
+
+ //Text Input
+ #region Text Input
+ if (ImGui.TreeNode("Text Input"))
+ {
+ if (ImGui.TreeNode("Multi-line Text Input"))
+ {
+ string text = "/*\n" +
+ " The Pentium F00F bug, shorthand for F0 0F C7 C8,\n" +
+ " the hexadecimal encoding of one offending instruction,\n" +
+ " more formally, the invalid operand with locked CMPXCHG8B\n" +
+ " instruction bug, is a design flaw in the majority of\n" +
+ " Intel Pentium, Pentium MMX, and Pentium OverDrive\n" +
+ " processors (all in the P5 microarchitecture).\n" +
+ "*/\n\n" +
+ "label:\n" +
+ "\tlock cmpxchg8b eax\n";
+
+ if (flags_ti_first_run)
+ {
+ ImGuiInputTextFlags _flags = ImGuiInputTextFlags.AllowTabInput;
+ flags_ti = (uint)_flags;
+ flags_ti_first_run = false;
+ }
+
+ HelpMarker("You can use the ImGuiInputTextFlags_CallbackResize facility if you need to wire InputTextMultiline() to a dynamic string type. See misc/cpp/imgui_stdlib.h for an example. (This is not demonstrated in imgui_demo.cpp because we don't want to include <string> in here)");
+ ImGui.CheckboxFlags("ImGuiInputTextFlags_ReadOnly", ref flags_ti, (uint)ImGuiInputTextFlags.ReadOnly);
+ ImGui.CheckboxFlags("ImGuiInputTextFlags_AllowTabInput", ref flags_ti, (uint)ImGuiInputTextFlags.AllowTabInput);
+ ImGui.CheckboxFlags("ImGuiInputTextFlags_CtrlEnterForNewLine", ref flags_ti, (uint)ImGuiInputTextFlags.CtrlEnterForNewLine);
+
+ ImGuiInputTextFlags flags_ml = (ImGuiInputTextFlags)flags_ti;
+ ImGui.InputTextMultiline("##source", ref text, (uint)text.Length, new Vec2(0, ImGui.GetTextLineHeight() * 16), flags_ml);
+ ImGui.TreePop();
+ }
+
+ if (ImGui.TreeNode("Filtered Text Input"))
+ {
+ ImGui.InputText("default", ref buf1, (uint)64);
+ ImGui.InputText("decimal", ref buf2, (uint)64, ImGuiInputTextFlags.CharsDecimal);
+ ImGui.InputText("hexadecimal", ref buf3, (uint)64, ImGuiInputTextFlags.CharsHexadecimal);
+ ImGui.InputText("uppercase", ref buf4, (uint)64, ImGuiInputTextFlags.CharsUppercase);
+ ImGui.InputText("no blank", ref buf5, (uint)64, ImGuiInputTextFlags.CharsNoBlank);
+ ImGui.TreePop();
+ }
+
+ if (ImGui.TreeNode("Password Input"))
+ {
+ ImGui.InputText("password", ref password, (uint)64, ImGuiInputTextFlags.Password);
+ ImGui.SameLine();
+ HelpMarker("Display all characters as '*'.\nDisable clipboard cut and copy.\nDisable logging.\n");
+ ImGui.InputTextWithHint("password (w/ hint)", "<password>", ref password, (uint)64, ImGuiInputTextFlags.Password);
+ ImGui.InputText("password (clear)", ref password, (uint)64);
+ ImGui.TreePop();
+ }
+
+ ImGui.TreePop();
+ }
+ #endregion
+
+ //Tabs
+ #region Tabs
+ if (ImGui.TreeNode("Tabs"))
+ {
+ if (ImGui.TreeNode("Basic"))
+ {
+ ImGuiTabBarFlags tab_bar_flags = ImGuiTabBarFlags.None;
+ if (ImGui.BeginTabBar("MyTabBar", tab_bar_flags))
+ {
+ if (ImGui.BeginTabItem("Avocado"))
+ {
+ ImGui.Text("This is the Avocado tab!\nblah blah blah blah blah");
+ ImGui.EndTabItem();
+ }
+ if (ImGui.BeginTabItem("Broccoli"))
+ {
+ ImGui.Text("This is the Broccoli tab!\nblah blah blah blah blah");
+ ImGui.EndTabItem();
+ }
+ if (ImGui.BeginTabItem("Cucumber"))
+ {
+ ImGui.Text("This is the Cucumber tab!\nblah blah blah blah blah");
+ ImGui.EndTabItem();
+ }
+ ImGui.EndTabBar();
+ }
+ ImGui.Separator();
+ ImGui.TreePop();
+ }
+
+ if (ImGui.TreeNode("Advanced & Close Button"))
+ {
+ if (flags_tabs_first_run)
+ {
+ ImGuiTabBarFlags _tab_bar_flags = ImGuiTabBarFlags.Reorderable;
+ tab_bar_flags = (uint)_tab_bar_flags;
+ flags_tabs_first_run = false;
+ }
+
+ ImGui.CheckboxFlags("ImGuiTabBarFlags_Reorderable", ref tab_bar_flags, (uint)ImGuiTabBarFlags.Reorderable);
+ ImGui.CheckboxFlags("ImGuiTabBarFlags_AutoSelectNewTabs", ref tab_bar_flags, (uint)ImGuiTabBarFlags.AutoSelectNewTabs);
+ ImGui.CheckboxFlags("ImGuiTabBarFlags_TabListPopupButton", ref tab_bar_flags, (uint)ImGuiTabBarFlags.TabListPopupButton);
+ ImGui.CheckboxFlags("ImGuiTabBarFlags_NoCloseWithMiddleMouseButton", ref tab_bar_flags, (uint)ImGuiTabBarFlags.NoCloseWithMiddleMouseButton);
+ if ((tab_bar_flags & (uint)ImGuiTabBarFlags.FittingPolicyMask) == 0)
+ {
+ tab_bar_flags |= (uint)ImGuiTabBarFlags.FittingPolicyDefault;
+ }
+ if (ImGui.CheckboxFlags("ImGuiTabBarFlags_FittingPolicyResizeDown", ref tab_bar_flags, (uint)ImGuiTabBarFlags.FittingPolicyResizeDown))
+ {
+ tab_bar_flags &= ~((uint)ImGuiTabBarFlags.FittingPolicyMask ^ (uint)ImGuiTabBarFlags.FittingPolicyResizeDown);
+ }
+ if (ImGui.CheckboxFlags("ImGuiTabBarFlags_FittingPolicyScroll", ref tab_bar_flags, (uint)ImGuiTabBarFlags.FittingPolicyScroll))
+ {
+ tab_bar_flags &= ~((uint)ImGuiTabBarFlags.FittingPolicyMask ^ (uint)ImGuiTabBarFlags.FittingPolicyScroll);
+ }
+
+ string[] names = { "Artichoke", "Beetroot", "Celery", "Daikon" };
+ for (int n = 0; n < opened.Length; n++)
+ {
+ if (n > 0) { ImGui.SameLine(); }
+ ImGui.Checkbox(names[n], ref opened[n]);
+ }
+
+ ImGuiTabBarFlags tab_bar_flags_tb = (ImGuiTabBarFlags)tab_bar_flags;
+ if (ImGui.BeginTabBar("MyTabBar", tab_bar_flags_tb))
+ {
+ for (int n = 0; n < opened.Length; n++)
+ {
+ if (opened[n] && ImGui.BeginTabItem(names[n], ref opened[n], ImGuiTabItemFlags.None))
+ {
+ ImGui.Text(string.Format("This is the {0} tab!", names[n]));
+ if (n == 1 || n == 3)
+ {
+ ImGui.Text("I am an odd tab.");
+ }
+ ImGui.EndTabItem();
+ }
+ }
+ ImGui.EndTabBar();
+ }
+ ImGui.Separator();
+ ImGui.TreePop();
+ }
+
+ ImGui.TreePop();
+
+ }
+ #endregion
+
+ //Plots Widgets
+ #region Plot Widgets
+ if (ImGui.TreeNode("Plots Widgets"))
+ {
+ ImGui.Checkbox("Animate", ref animate);
+
+ float[] arr = { 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f };
+ ImGui.PlotLines("Frame Times", ref arr[0], arr.Length);
+
+ if (!animate || refresh_time == 0.0)
+ {
+ refresh_time = ImGui.GetTime();
+ }
+ while (refresh_time < ImGui.GetTime())
+ {
+ values[values_offset] = (float)Math.Cos(phase);
+ values_offset = (values_offset + 1) % values.Length;
+ phase += 0.10f * values_offset;
+ refresh_time += 1.0f / 60.0f;
+ }
+
+ float average = 0.0f;
+ for (int n = 0; n < values.Length; n++)
+ {
+ average += values[n];
+ }
+ average /= (float)values.Length;
+ string overlay = string.Format("avg {0}", average);
+ ImGui.PlotLines("Lines", ref values[0], values.Length, values_offset, overlay, -1.0f, 1.0f, new Vec2(0, 80.0f));
+ ImGui.PlotHistogram("Histogram", ref arr[0], arr.Length, 0, null, 0.0f, 1.0f, new Vec2(0, 80.0f));
+ ImGui.Separator();
+
+ if (animate)
+ {
+ progress += progress_dir * 0.4f * ImGui.GetIO().DeltaTime;
+ if (progress >= +1.1f) { progress = +1.1f; progress_dir *= -1.0f; }
+ if (progress <= -0.1f) { progress = -0.1f; progress_dir *= -1.0f; }
+ }
+
+ ImGui.ProgressBar(progress, new Vec2(0.0f, 0.0f));
+ ImGui.SameLine(0.0f, ImGui.GetStyle().ItemInnerSpacing.X);
+ ImGui.Text("Progress Bar");
+
+ float progress_saturated = progress; //IM_CLAMP(progress, 0.0f, 1.0f) ?
+ string buf = string.Format("{0}/{1}", (int)(progress_saturated * 1753), 1753);
+ ImGui.ProgressBar(progress, new Vec2(0.0f, 0.0f), buf);
+ ImGui.TreePop();
+ }
+ #endregion
+
+ //Color Widgets
+ #region Color Widgets
+ if (ImGui.TreeNode("Color/Picker Widgets"))
+ {
+ ImGui.Checkbox("With Alpha Preview", ref alpha_preview);
+ ImGui.Checkbox("With Half Alpha Preview", ref alpha_half_preview);
+ ImGui.Checkbox("With Drag and Drop", ref drag_and_drop);
+ ImGui.Checkbox("With Options Menu", ref options_menu); ImGui.SameLine(); HelpMarker("Right-click on the individual color widget to show options.");
+ ImGui.Checkbox("With HDR", ref hdr); ImGui.SameLine(); HelpMarker("Currently all this does is to lift the 0..1 limits on dragging widgets.");
+ ImGuiColorEditFlags misc_flags = (hdr ? ImGuiColorEditFlags.HDR : 0) | (drag_and_drop ? 0 : ImGuiColorEditFlags.NoDragDrop) | (alpha_half_preview ? ImGuiColorEditFlags.AlphaPreviewHalf : (alpha_preview ? ImGuiColorEditFlags.AlphaPreview : 0)) | (options_menu ? 0 : ImGuiColorEditFlags.NoOptions);
+
+ ImGui.Text("Color widget:");
+ ImGui.SameLine(); HelpMarker(
+ "Click on the color square to open a color picker.\n" +
+ "CTRL+click on individual component to input value.\n");
+ ImGui.ColorEdit3("MyColor##1", ref color_vec3, misc_flags);
+
+ ImGui.Text("Color widget HSV with Alpha:");
+ ImGui.ColorEdit4("MyColor##2", ref color_vec4, ImGuiColorEditFlags.DisplayHSV | misc_flags);
+
+ ImGui.Text("Color widget with Float Display:");
+ ImGui.ColorEdit4("MyColor##2f", ref color_vec4, ImGuiColorEditFlags.Float | misc_flags);
+
+ ImGui.Text("Color button with Picker:");
+ ImGui.SameLine();
+ HelpMarker("With the ImGuiColorEditFlags_NoInputs flag you can hide all the slider/text inputs.\n" +
+ "With the ImGuiColorEditFlags_NoLabel flag you can pass a non-empty label which will only " +
+ "be used for the tooltip and picker popup.");
+ ImGui.ColorEdit4("MyColor##3", ref color_vec4, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel | misc_flags);
+
+ ImGui.Text("Color button only:");
+ ImGui.ColorButton("MyColor##3c", color_vec4, misc_flags, new Vec2(80, 80));
+
+ ImGui.Text("Color picker:");
+
+ ImGui.Checkbox("With Alpha", ref alpha);
+ ImGui.Checkbox("With Alpha Bar", ref alpha_bar);
+ ImGui.Checkbox("With Side Preview", ref side_preview);
+ if (side_preview)
+ {
+ ImGui.SameLine();
+ ImGui.Checkbox("With Ref Color", ref ref_color);
+ if (ref_color)
+ {
+ ImGui.SameLine();
+ ImGui.ColorEdit4("##RefColor", ref ref_color_v, ImGuiColorEditFlags.NoInputs | misc_flags);
+ }
+ }
+ ImGui.Combo("Display Mode", ref display_mode, "Auto/Current\0None\0RGB Only\0HSV Only\0Hex Only\0");
+ ImGui.SameLine();
+ HelpMarker(
+ "ColorEdit defaults to displaying RGB inputs if you don't specify a display mode, " +
+ "but the user can change it with a right-click.\n\nColorPicker defaults to displaying RGB+HSV+Hex " +
+ "if you don't specify a display mode.\n\nYou can change the defaults using SetColorEditOptions().");
+ ImGui.Combo("Picker Mode", ref picker_mode, "Auto/Current\0Hue bar + SV rect\0Hue wheel + SV triangle\0");
+ ImGui.SameLine(); HelpMarker("User can right-click the picker to change mode.");
+ ImGuiColorEditFlags flags = misc_flags;
+ if (!alpha) { flags |= ImGuiColorEditFlags.NoAlpha; }
+ if (alpha_bar) { flags |= ImGuiColorEditFlags.AlphaBar; }
+ if (!side_preview) { flags |= ImGuiColorEditFlags.NoSidePreview; }
+ if (picker_mode == 1) { flags |= ImGuiColorEditFlags.PickerHueBar; }
+ if (picker_mode == 2) { flags |= ImGuiColorEditFlags.PickerHueWheel; }
+ if (display_mode == 1) { flags |= ImGuiColorEditFlags.NoInputs; }
+ if (display_mode == 2) { flags |= ImGuiColorEditFlags.DisplayRGB; }
+ if (display_mode == 3) { flags |= ImGuiColorEditFlags.DisplayHSV; }
+ if (display_mode == 4) { flags |= ImGuiColorEditFlags.DisplayHex; }
+ ImGui.ColorPicker4("MyColor##4", ref color_vec4, flags, ref ref_color_v.X); // ref_color ? ref_color_v.X : null);
+
+ ImGui.Text("Set defaults in code:");
+ ImGui.SameLine(); HelpMarker("SetColorEditOptions() is designed to allow you to set boot-time default.\n" +
+ "We don't have Push/Pop functions because you can force options on a per-widget basis if needed," +
+ "and the user can change non-forced ones with the options menu.\nWe don't have a getter to avoid" +
+ "encouraging you to persistently save values that aren't forward-compatible.");
+ if (ImGui.Button("Default: Uint8 + HSV + Hue Bar"))
+ {
+ ImGui.SetColorEditOptions(ImGuiColorEditFlags.Uint8 | ImGuiColorEditFlags.DisplayHSV | ImGuiColorEditFlags.PickerHueBar);
+ }
+ if (ImGui.Button("Default: Float + HDR + Hue Wheel"))
+ {
+ ImGui.SetColorEditOptions(ImGuiColorEditFlags.Float | ImGuiColorEditFlags.HDR | ImGuiColorEditFlags.PickerHueWheel);
+ }
+ // HSV
+ ImGui.Spacing();
+ ImGui.Text("HSV encoded colors");
+ ImGui.SameLine(); HelpMarker("By default, colors are given to ColorEdit and ColorPicker in RGB, but ImGuiColorEditFlags_InputHSV" +
+ "allows you to store colors as HSV and pass them to ColorEdit and ColorPicker as HSV. This comes with the" +
+ "added benefit that you can manipulate hue values with the picker even when saturation or value are zero.");
+ ImGui.Text("Color widget with InputHSV:");
+ ImGui.ColorEdit4("HSV shown as RGB##1", ref color_hsv, ImGuiColorEditFlags.DisplayRGB | ImGuiColorEditFlags.InputHSV | ImGuiColorEditFlags.Float);
+ ImGui.ColorEdit4("HSV shown as HSV##1", ref color_hsv, ImGuiColorEditFlags.DisplayHSV | ImGuiColorEditFlags.InputHSV | ImGuiColorEditFlags.Float);
+ ImGui.DragFloat4("Raw HSV values", ref color_hsv, 0.01f, 0.0f, 1.0f);
+
+ ImGui.TreePop();
+ }
+ #endregion
+
+ //Range & Multi Widgets
+ # region Range & Multi Widgets
+ if (ImGui.TreeNode("Range Widgets"))
+ {
+ ImGui.DragFloatRange2("range float", ref begin, ref end, 0.25f, 0.0f, 100.0f, "Min: %.1f %%", "Max: %.1f %%");
+ ImGui.DragIntRange2("range int", ref begin_i, ref end_i, 5, 0, 1000, "Min: %d units", "Max: %d units");
+ ImGui.DragIntRange2("range int (no bounds)", ref begin_i, ref end_i, 5, 0, 0, "Min: %d units", "Max: %d units");
+ ImGui.TreePop();
+ }
+
+ if (ImGui.TreeNode("Multi-component Widgets"))
+ {
+ ImGui.InputFloat2("input float2", ref vec2f);
+ ImGui.DragFloat2("drag float2", ref vec2f, 0.01f, 0.0f, 1.0f);
+ ImGui.SliderFloat2("slider float2", ref vec2f, 0.0f, 1.0f);
+ ImGui.InputInt2("input int2", ref vec4i[0]);
+ ImGui.DragInt2("drag int2", ref vec4i[0], 1, 0, 255);
+ ImGui.SliderInt2("slider int2", ref vec4i[0], 0, 255);
+ ImGui.Spacing();
+
+ ImGui.InputFloat3("input float3", ref vec3f);
+ ImGui.DragFloat3("drag float3", ref vec3f, 0.01f, 0.0f, 1.0f);
+ ImGui.SliderFloat3("slider float3", ref vec3f, 0.0f, 1.0f);
+ ImGui.InputInt3("input int3", ref vec4i[0]);
+ ImGui.DragInt3("drag int3", ref vec4i[0], 1, 0, 255);
+ ImGui.SliderInt3("slider int3", ref vec4i[0], 0, 255);
+ ImGui.Spacing();
+
+ ImGui.InputFloat4("input float4", ref vec4f);
+ ImGui.DragFloat4("drag float4", ref vec4f);
+ ImGui.SliderFloat4("slider float4", ref vec4f, 0.0f, 1.0f);
+ ImGui.InputInt4("input int4", ref vec4i[0]);
+ ImGui.DragInt4("drag int4", ref vec4i[0], 1, 0, 255);
+ ImGui.SliderInt4("slider int4", ref vec4i[0], 0, 255);
+
+ ImGui.TreePop();
+ }
+ #endregion
+
+ //Vertical Sliders
+ #region Vertical Sliders
+ if (ImGui.TreeNode("Vertical Sliders"))
+ {
+ ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vec2(spacing, spacing));
+
+ ImGui.VSliderInt("##int", new Vec2(18, 160), ref int_value, 0, 5);
+ ImGui.SameLine();
+
+ ImGui.PushID("set1");
+ for (int i = 0; i < 7; i++)
+ {
+ if (i > 0)
+ {
+ ImGui.SameLine();
+ }
+ ImGui.PushID(i);
+
+ ImGui.ColorConvertHSVtoRGB(i / 7.0f, 0.5f, 0.5f, out col_red, out col_green, out col_blue);
+ Vec4 col_slider = new Vec4(col_red, col_green, col_blue, 1.0f);
+ ImGui.ColorConvertHSVtoRGB(i / 7.0f, 0.6f, 0.5f, out col_red, out col_green, out col_blue);
+ Vec4 col_slider_hov = new Vec4(col_red, col_green, col_blue, 1.0f);
+ ImGui.ColorConvertHSVtoRGB(i / 7.0f, 0.7f, 0.5f, out col_red, out col_green, out col_blue);
+ Vec4 col_slider_act = new Vec4(col_red, col_green, col_blue, 1.0f);
+ ImGui.ColorConvertHSVtoRGB(i / 7.0f, 0.9f, 0.9f, out col_red, out col_green, out col_blue);
+ Vec4 col_slider_grab = new Vec4(col_red, col_green, col_blue, 1.0f);
+
+ ImGui.PushStyleColor(ImGuiCol.FrameBg, col_slider);
+ ImGui.PushStyleColor(ImGuiCol.FrameBgHovered, col_slider_hov);
+ ImGui.PushStyleColor(ImGuiCol.FrameBgActive, col_slider_act);
+ ImGui.PushStyleColor(ImGuiCol.SliderGrab, col_slider_grab);
+ ImGui.VSliderFloat("##v", new Vec2(18, 160), ref values_vert[i], 0.0f, 1.0f, "");
+ if (ImGui.IsAnyItemActive() || ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip(values_vert[i].ToString());
+ }
+ ImGui.PopStyleColor(4);
+ ImGui.PopID();
+ }
+ ImGui.PopID();
+
+ ImGui.SameLine();
+ ImGui.PushID("set2");
+
+ int rows = 3;
+ Vec2 small_slider_size = new Vec2(18, 50);//(float)(int)((160.0f - (rows - 1) * spacing / rows)));
+ for (int nx = 0; nx < 4; nx++)
+ {
+ if (nx > 0) { ImGui.SameLine(); }
+ ImGui.BeginGroup();
+ for (int ny = 0; ny < rows; ny++)
+ {
+ ImGui.PushID(nx * rows + ny);
+ ImGui.VSliderFloat("##v", small_slider_size, ref values2[nx], 0.0f, 1.0f, "");
+ if (ImGui.IsItemActive() || ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip(values2[nx].ToString());
+ }
+ ImGui.PopID();
+ }
+ ImGui.EndGroup();
+ }
+ ImGui.PopID();
+
+ ImGui.SameLine();
+ ImGui.PushID("set3");
+ for (int i = 0; i < 4; i++)
+ {
+ if (i > 0)
+ {
+ ImGui.SameLine();
+ }
+ ImGui.PushID(i);
+ ImGui.PushStyleVar(ImGuiStyleVar.GrabMinSize, 40);
+ ImGui.VSliderFloat("##v", new Vec2(40, 160), ref values_vert[i], 0.0f, 1.0f, "%.2f\nsec");
+ ImGui.PopStyleVar();
+ ImGui.PopID();
+ }
+ ImGui.PopID();
+ ImGui.PopStyleVar();
+ ImGui.TreePop();
+ }
+ #endregion
+ }
+
+ #region DemoWindowLayoutVariables
+ //Child windows
+ bool disable_mouse_wheel = false;
+ bool disable_menu = false;
+ int offset_x = 0;
+ //Widgets Width
+ float f = 0.0f;
+ bool show_indented_items = true;
+ //Basic Horizontal Layout
+ bool c1 = false;
+ bool c2 = false;
+ bool c3 = false;
+ bool c4 = false;
+ float bf0 = 1.0f;
+ float bf1 = 2.0f;
+ float bf2 = 3.0f;
+ string[] items = { "AAAA", "BBBB", "CCCC", "DDDD" };
+ int item = -1;
+ int[] bselection = { 0, 1, 2, 3 };
+ //Scrolling
+ int track_item = 50;
+ bool enable_track = true;
+ bool enable_extra_decorations = false;
+ float scroll_to_off_px = 0.0f;
+ float scroll_to_pos_px = 200.0f;
+ #endregion
+
+ private void DemoWindowLayout()
+ {
+ if (!ImGui.CollapsingHeader("Layout & Scrolling"))
+ {
+ return;
+ }
+
+ //Child windows
+ #region Child windows
+ if (ImGui.TreeNode("Child windows"))
+ {
+ HelpMarker("Use child windows to begin into a self-contained independent scrolling/clipping regions within a host window.");
+
+ ImGui.Checkbox("Disable Mouse Wheel", ref disable_mouse_wheel);
+ ImGui.Checkbox("Disable Menu", ref disable_menu);
+
+ //child 1
+ ImGuiWindowFlags window_flags = ImGuiWindowFlags.HorizontalScrollbar;
+ if (disable_mouse_wheel)
+ {
+ window_flags |= ImGuiWindowFlags.NoScrollWithMouse;
+ }
+ Vec2 content_region_max;
+ ImGui.BeginChild("ChildL", new Vec2(ImGui.GetWindowContentRegionMax().X * 0.5f, 260), ImGuiChildFlags.Border, window_flags);
+ for (int i = 0; i < 100; i++)
+ {
+ ImGui.Text(string.Format("{0}: scrollable region", i.ToString("D4")));
+ }
+ ImGui.EndChild();
+ ImGui.SameLine();
+
+ //child 2
+ ImGuiWindowFlags window_flags_child2 = ImGuiWindowFlags.None;
+ if (disable_mouse_wheel)
+ {
+ window_flags_child2 |= ImGuiWindowFlags.NoScrollWithMouse;
+ }
+ if (!disable_menu)
+ {
+ window_flags_child2 |= ImGuiWindowFlags.MenuBar;
+ }
+ ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 5.0f);
+ ImGui.BeginChild("ChildR", new Vec2(0, 260), ImGuiChildFlags.Border, window_flags_child2);
+ if (!disable_menu && ImGui.BeginMenuBar())
+ {
+ if (ImGui.BeginMenu("Menu"))
+ {
+ ShowExampleMenuFile();
+ ImGui.EndMenu();
+ }
+ ImGui.EndMenuBar();
+ }
+ for (int i = 0; i < 100; i++)
+ {
+ ImGui.Button(i.ToString("D3"));
+ if ((i % 2) == 0)
+ {
+ ImGui.SameLine();
+ }
+ }
+ ImGui.EndChild();
+ ImGui.PopStyleVar();
+
+ ImGui.Separator();
+
+ ImGui.SetNextItemWidth(100);
+ ImGui.DragInt("Offset X", ref offset_x, 1.0f, -1000, 1000);
+
+ ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (float)offset_x);
+ Vec4 colv4 = new Vec4(255.0f, 0.0f, 0.0f, 100.0f);
+ ImGui.PushStyleColor(ImGuiCol.ChildBg, ImGui.ColorConvertFloat4ToU32(colv4));
+ ImGui.BeginChild("Red", new Vec2(200, 100), ImGuiChildFlags.Border, ImGuiWindowFlags.None);
+ for (int n = 0; n < 50; n++)
+ {
+ ImGui.Text("Some test " + n.ToString());
+ }
+ ImGui.EndChild();
+ bool child_is_hovered = ImGui.IsItemHovered();
+ Vec2 child_rect_min = ImGui.GetItemRectMin();
+ Vec2 child_rect_max = ImGui.GetItemRectMax();
+ ImGui.PopStyleColor();
+ ImGui.Text("Hovered: " + child_is_hovered.ToString());
+ ImGui.Text(string.Format("Rect of child window is: ({0},{1}) ({2},{3})", child_rect_min.X, child_rect_min.Y, child_rect_max.X, child_rect_max.Y));
+
+ ImGui.TreePop();
+ }
+ #endregion
+
+ //Widgets Width
+ #region Widgets Width
+ if (ImGui.TreeNode("Widgets Width"))
+ {
+ ImGui.Checkbox("Show intended items", ref show_indented_items);
+
+ ImGui.Text("SetNextItemWidth/PushItemWidth(100)");
+ ImGui.SameLine(); HelpMarker("Fixed width.");
+ ImGui.PushItemWidth(100);
+ ImGui.DragFloat("float##1b", ref f);
+ if (show_indented_items)
+ {
+ ImGui.Indent();
+ ImGui.DragFloat("float (intended)##1b", ref f);
+ ImGui.Unindent();
+ }
+ ImGui.PopItemWidth();
+
+ ImGui.Text("SetNextItemWidth/PushItemWidth(-100)");
+ ImGui.SameLine(); HelpMarker("Align to right edge minus 100");
+ ImGui.PushItemWidth(-100);
+ ImGui.DragFloat("float##2a", ref f);
+ if (show_indented_items)
+ {
+ ImGui.Indent();
+ ImGui.DragFloat("float (indented)##2b", ref f);
+ ImGui.Unindent();
+ }
+ ImGui.PopItemWidth();
+
+ ImGui.Text("SetNextItemWidth/PushItemWidth(GetContentRegionAvail().x * 0.5f)");
+ ImGui.SameLine(); HelpMarker("Half of available width.\n(~ right-cursor_pos)\n(works within a column set)");
+ ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X * 0.5f);
+ ImGui.DragFloat("float##3a", ref f);
+ if (show_indented_items)
+ {
+ ImGui.Indent();
+ ImGui.DragFloat("float (indented)##3b", ref f);
+ ImGui.Unindent();
+ }
+ ImGui.PopItemWidth();
+
+ ImGui.Text("SetNextItemWidth/PushItemWidth(-GetContentRegionAvail().x * 0.5f)");
+ ImGui.SameLine(); HelpMarker("Align to right edge minus half");
+ ImGui.PushItemWidth(-ImGui.GetContentRegionAvail().X * 0.5f);
+ ImGui.DragFloat("float##4a", ref f);
+ if (show_indented_items)
+ {
+ ImGui.Indent();
+ ImGui.DragFloat("float (indented)##4b", ref f);
+ ImGui.Unindent();
+ }
+ ImGui.PopItemWidth();
+
+ ImGui.TreePop();
+ }
+ #endregion
+
+ //Basic Horizontal Layout
+ #region Basic Horizontal Layout
+ if (ImGui.TreeNode("Basic Horizontal Layout"))
+ {
+ ImGui.TextWrapped("(Use ImGui.SameLine() to keep adding items to the right of the preceding item)");
+
+ // Text
+ ImGui.Text("Two items: Hello"); ImGui.SameLine();
+ ImGui.TextColored(new Vec4(1, 1, 0, 1), "Sailor");
+
+ // Adjust spacing
+ ImGui.Text("More spacing: Hello"); ImGui.SameLine(0, 20);
+ ImGui.TextColored(new Vec4(1, 1, 0, 1), "Sailor");
+
+ // Button
+ ImGui.AlignTextToFramePadding();
+ ImGui.Text("Normal buttons"); ImGui.SameLine();
+ ImGui.Button("Banana"); ImGui.SameLine();
+ ImGui.Button("Apple"); ImGui.SameLine();
+ ImGui.Button("Corniflower");
+
+ // Button
+ ImGui.Text("Small buttons"); ImGui.SameLine();
+ ImGui.SmallButton("Like this one"); ImGui.SameLine();
+ ImGui.Text("can fit within a text block.");
+
+ // Aligned to arbitrary position. Easy/cheap column.
+ ImGui.Text("Aligned");
+ ImGui.SameLine(150); ImGui.Text("x=150");
+ ImGui.SameLine(300); ImGui.Text("x=300");
+ ImGui.Text("Aligned");
+ ImGui.SameLine(150); ImGui.SmallButton("x=150");
+ ImGui.SameLine(300); ImGui.SmallButton("x=300");
+
+ // Checkbox
+ ImGui.Checkbox("My", ref c1); ImGui.SameLine();
+ ImGui.Checkbox("Tailor", ref c2); ImGui.SameLine();
+ ImGui.Checkbox("Is", ref c3); ImGui.SameLine();
+ ImGui.Checkbox("Rich", ref c4);
+
+ // Various
+ ImGui.PushItemWidth(80);
+ ImGui.Combo("Combo", ref item, items, items.Length); ImGui.SameLine();
+ ImGui.SliderFloat("X", ref bf0, 0.0f, 5.0f); ImGui.SameLine();
+ ImGui.SliderFloat("Y", ref bf1, 0.0f, 5.0f); ImGui.SameLine();
+ ImGui.SliderFloat("Z", ref bf2, 0.0f, 5.0f);
+ ImGui.PopItemWidth();
+
+ ImGui.PushItemWidth(80);
+ ImGui.Text("Lists:");
+ for (int i = 0; i < 4; i++)
+ {
+ if (i > 0) ImGui.SameLine();
+ ImGui.PushID(i);
+ ImGui.ListBox("", ref bselection[i], items, items.Length);
+ ImGui.PopID();
+ }
+ ImGui.PopItemWidth();
+
+ // Dummy
+ Vec2 button_sz = new Vec2(40, 40);
+ ImGui.Button("A", button_sz); ImGui.SameLine();
+ ImGui.Dummy(button_sz); ImGui.SameLine();
+ ImGui.Button("B", button_sz);
+
+ // Manually wrapping
+ // (we should eventually provide this as an automatic layout feature, but for now you can do it manually)
+ ImGui.Text("Manually wrapping:");
+ int buttons_count = 20;
+ float window_visible_x2 = ImGui.GetWindowPos().X + ImGui.GetWindowContentRegionMax().X;
+ for (int n = 0; n < buttons_count; n++)
+ {
+ ImGui.PushID(n);
+ ImGui.Button("Box", button_sz);
+ float last_button_x2 = ImGui.GetItemRectMax().X;
+ float next_button_x2 = last_button_x2 + 1.0f + button_sz.X; // Expected position if next button was on same line
+ if (n + 1 < buttons_count && next_button_x2 < window_visible_x2)
+ {
+ ImGui.SameLine();
+ }
+ ImGui.PopID();
+ }
+
+ ImGui.TreePop();
+ }
+ #endregion
+
+ //Groups
+ #region Groups
+ if (ImGui.TreeNode("Groups"))
+ {
+ HelpMarker("BeginGroup() basically locks the horizontal position for new line. " +
+ "EndGroup() bundles the whole group so that you can use \"item\" functions such as " +
+ "IsItemHovered()/IsItemActive() or SameLine() etc. on the whole group.");
+ ImGui.BeginGroup();
+ {
+ ImGui.BeginGroup();
+ ImGui.Button("AAA");
+ ImGui.SameLine();
+ ImGui.Button("BBB");
+ ImGui.SameLine();
+ ImGui.BeginGroup();
+ ImGui.Button("CCC");
+ ImGui.Button("DDD");
+ ImGui.EndGroup();
+ ImGui.SameLine();
+ ImGui.Button("EEE");
+ ImGui.EndGroup();
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("First group hovered");
+ }
+ }
+ // Capture the group size and create widgets using the same size
+ Vec2 size = ImGui.GetItemRectSize();
+ float[] values = { 0.5f, 0.20f, 0.80f, 0.60f, 0.25f };
+ ImGui.PlotHistogram("##values", ref values[0], values.Length, 0, null, 0.0f, 1.0f, size);
+
+ ImGui.Button("ACTION", new Vec2((size.X - ImGui.GetStyle().ItemSpacing.X) * 0.5f, size.Y));
+ ImGui.SameLine();
+ ImGui.Button("REACTION", new Vec2((size.X - ImGui.GetStyle().ItemSpacing.X) * 0.5f, size.Y));
+ ImGui.EndGroup();
+
+ // This breaks tree node
+ //ImGui.SameLine();
+ //ImGui.Button("LEVERAGE\nBUZZWORD", size);
+ //ImGui.SameLine();
+
+ ImGui.TreePop();
+ }
+ #endregion
+
+ //Text Baseline Alignment
+ #region Text Baseline Alignment
+ if (ImGui.TreeNode("Text Baseline Alignment"))
+ {
+ ImGui.BulletText("Text baseline:");
+ ImGui.SameLine();
+ HelpMarker("This is testing the vertical alignment that gets applied on text to keep it aligned with widgets. " +
+ "Lines only composed of text or \"small\" widgets use less vertical space than lines with framed widgets.");
+ ImGui.Indent();
+
+ ImGui.Text("KO Blahblah"); ImGui.SameLine();
+ ImGui.Button("Some framed item"); ImGui.SameLine();
+ HelpMarker("Baseline of button will look misaligned with text..");
+
+ ImGui.AlignTextToFramePadding();
+ ImGui.Text("OK Blahblah"); ImGui.SameLine();
+ ImGui.Button("Some framed item"); ImGui.SameLine();
+ HelpMarker("We call AlignTextToFramePadding() to vertically align the text baseline by +FramePadding.y");
+
+ // SmallButton() uses the same vertical padding as Text
+ ImGui.Button("TEST##1"); ImGui.SameLine();
+ ImGui.Text("TEST"); ImGui.SameLine();
+ ImGui.SmallButton("TEST##2");
+
+ // If your line starts with text, call AlignTextToFramePadding() to align text to upcoming widgets.
+ ImGui.AlignTextToFramePadding();
+ ImGui.Text("Text aligned to framed item"); ImGui.SameLine();
+ ImGui.Button("Item##1"); ImGui.SameLine();
+ ImGui.Text("Item"); ImGui.SameLine();
+ ImGui.SmallButton("Item##2"); ImGui.SameLine();
+ ImGui.Button("Item##3");
+
+ ImGui.Unindent();
+
+ ImGui.Spacing();
+
+ ImGui.BulletText("Multi-line text:");
+ ImGui.Indent();
+ ImGui.Text("One\nTwo\nThree"); ImGui.SameLine();
+ ImGui.Text("Hello\nWorld"); ImGui.SameLine();
+ ImGui.Text("Banana");
+
+ ImGui.Text("Banana"); ImGui.SameLine();
+ ImGui.Text("Hello\nWorld"); ImGui.SameLine();
+ ImGui.Text("One\nTwo\nThree");
+
+ ImGui.Button("HOP##1"); ImGui.SameLine();
+ ImGui.Text("Banana"); ImGui.SameLine();
+ ImGui.Text("Hello\nWorld"); ImGui.SameLine();
+ ImGui.Text("Banana");
+
+ ImGui.Button("HOP##2"); ImGui.SameLine();
+ ImGui.Text("Hello\nWorld"); ImGui.SameLine();
+ ImGui.Text("Banana");
+ ImGui.Unindent();
+
+ ImGui.Spacing();
+
+ ImGui.BulletText("Misc items:");
+ ImGui.Indent();
+
+ // SmallButton() sets FramePadding to zero. Text baseline is aligned to match baseline of previous Button.
+ ImGui.Button("80x80", new Vec2(80, 80));
+ ImGui.SameLine();
+ ImGui.Button("50x50", new Vec2(50, 50));
+ ImGui.SameLine();
+ ImGui.Button("Button()");
+ ImGui.SameLine();
+ ImGui.SmallButton("SmallButton()");
+
+ // Tree
+ float spacing = ImGui.GetStyle().ItemInnerSpacing.X;
+ ImGui.Button("Button##1");
+ ImGui.SameLine(0.0f, spacing);
+ if (ImGui.TreeNode("Node##1"))
+ {
+ // Placeholder tree data
+ for (int i = 0; i < 6; i++)
+ ImGui.BulletText(string.Format("Item {0}..", i));
+ ImGui.TreePop();
+ }
+
+ // Vertically align text node a bit lower so it'll be vertically centered with upcoming widget.
+ // Otherwise you can use SmallButton() (smaller fit).
+ ImGui.AlignTextToFramePadding();
+
+ // Common mistake to avoid: if we want to SameLine after TreeNode we need to do it before we add
+ // other contents below the node.
+ bool node_open = ImGui.TreeNode("Node##2");
+ ImGui.SameLine(0.0f, spacing); ImGui.Button("Button##2");
+ if (node_open)
+ {
+ // Placeholder tree data
+ for (int i = 0; i < 6; i++)
+ ImGui.BulletText(string.Format("Item {0}..", i));
+ ImGui.TreePop();
+ }
+
+ // Bullet
+ ImGui.Button("Button##3");
+ ImGui.SameLine(0.0f, spacing);
+ ImGui.BulletText("Bullet text");
+
+ ImGui.AlignTextToFramePadding();
+ ImGui.BulletText("Node");
+ ImGui.SameLine(0.0f, spacing); ImGui.Button("Button##4");
+ ImGui.Unindent();
+
+ ImGui.TreePop();
+ }
+ #endregion
+
+ //Scrolling
+ #region Scrolling
+ if (ImGui.TreeNode("Scrolling"))
+ {
+ //vertical
+ HelpMarker("Use SetScrollHereY() or SetScrollFromPosY() to scroll to a given vertical position.");
+
+ ImGui.Checkbox("Decoration", ref enable_extra_decorations);
+
+ ImGui.Checkbox("Track", ref enable_track);
+ ImGui.PushItemWidth(100);
+
+ ImGui.SameLine(140); enable_track |= ImGui.DragInt("##item", ref track_item, 0.25f, 0, 99, "Item = %d");
+
+ bool scroll_to_off = ImGui.Button("Scroll Offset");
+ ImGui.SameLine(140); scroll_to_off |= ImGui.DragFloat("##off", ref scroll_to_off_px, 1.00f, 0, 100.0f, "+%.0f px");
+
+ bool scroll_to_pos = ImGui.Button("Scroll To Pos");
+ ImGui.SameLine(140); scroll_to_pos |= ImGui.DragFloat("##pos", ref scroll_to_pos_px, 1.00f, -10, 100.0f, "X/Y = %.0f px");
+ ImGui.PopItemWidth();
+
+ if (scroll_to_off || scroll_to_pos)
+ {
+ enable_track = false;
+ }
+
+ float child_w = (ImGui.GetContentRegionAvail().X - 4 * 1.0f) / 5;
+ if (child_w < 1.0f)
+ {
+ child_w = 1.0f;
+ }
+ ImGui.PushID("##VerticalScrolling");
+ for (int i = 0; i < 5; i++)
+ {
+ if (i > 0) { ImGui.SameLine(); }
+ ImGui.BeginGroup();
+ string[] names = { "Top", "25%", "Center", "75%", "Bottom" };
+ ImGui.TextUnformatted(names[i]);
+
+ ImGuiWindowFlags child_flags = enable_extra_decorations ? ImGuiWindowFlags.MenuBar : 0;
+ string child_id = names[i];
+ bool child_is_visible = ImGui.BeginChild(child_id, new Vec2(child_w, 200.0f), ImGuiChildFlags.Border, child_flags);
+ if (ImGui.BeginMenuBar())
+ {
+ ImGui.TextUnformatted("abc");
+ ImGui.EndMenuBar();
+ }
+ if (scroll_to_off)
+ {
+ ImGui.SetScrollY(scroll_to_off_px);
+ }
+ if (scroll_to_pos)
+ {
+ ImGui.SetScrollFromPosY(ImGui.GetCursorStartPos().Y + scroll_to_pos_px, i * 0.25f);
+ }
+ if (child_is_visible)
+ {
+ for (int item = 0; item < 100; item++)
+ {
+ if (enable_track && item == track_item)
+ {
+ ImGui.TextColored(new Vec4(1, 1, 0, 1), "Item " + item);
+ ImGui.SetScrollHereY(i * 0.25f);
+ }
+ else
+ {
+ ImGui.Text("Item " + item);
+ }
+ }
+ }
+ float scroll_y = ImGui.GetScrollY();
+ float scroll_max_y = ImGui.GetScrollMaxY();
+ ImGui.EndChild();
+ ImGui.Text(scroll_y + "/" + scroll_max_y);
+ ImGui.EndGroup();
+
+ }
+ ImGui.PopID();
+
+ //horizontal
+ ImGui.Spacing();
+ HelpMarker("Use SetScrollHereX() or SetScrollFromPosX() to scroll to a given horizontal position.\n\n" +
+ "Because the clipping rectangle of most window hides half worth of WindowPadding on the " +
+ "left/right, using SetScrollFromPosX(+1) will usually result in clipped text whereas the " +
+ "equivalent SetScrollFromPosY(+1) wouldn't.");
+ ImGui.PushID("##HorizontalScrolling");
+ for (int i = 0; i < 5; i++)
+ {
+ float child_height = ImGui.GetTextLineHeight() + 30.0f;
+ ImGuiWindowFlags child_flags = ImGuiWindowFlags.HorizontalScrollbar | (enable_extra_decorations ? ImGuiWindowFlags.AlwaysVerticalScrollbar : 0);
+ string[] names = { "Left", "25%", "Center", "75%", "Right" };
+ string child_id = names[i];
+ bool child_is_visible = ImGui.BeginChild(child_id, new Vec2(-100, child_height), ImGuiChildFlags.Border, child_flags);
+ if (scroll_to_off)
+ {
+ ImGui.SetScrollX(scroll_to_off_px);
+ }
+ if (scroll_to_pos)
+ {
+ ImGui.SetScrollFromPosX(ImGui.GetCursorStartPos().X + scroll_to_pos_px, i * 0.25f);
+ }
+ if (child_is_visible)
+ {
+ for (int item = 0; item < 100; item++)
+ {
+ if (enable_track && item == track_item)
+ {
+ ImGui.TextColored(new Vec4(1, 1, 0, 1), "Item " + item);
+ ImGui.SetScrollHereX(i * 0.25f);
+ }
+ else
+ {
+ ImGui.Text("Item " + item);
+ }
+ ImGui.SameLine();
+ }
+ }
+ float scroll_x = ImGui.GetScrollX();
+ float scroll_max_x = ImGui.GetScrollMaxX();
+ ImGui.EndChild();
+ ImGui.SameLine();
+ ImGui.Text(string.Format("{0}\n{1}/{2}", names[i], scroll_x, scroll_max_x));
+ ImGui.Spacing();
+ }
+
+ ImGui.TreePop();
+ }
+ #endregion
+
+ }
+
+ #region DemoWindowPopupsVariables
+ //Popups
+ int selected_fish = -1;
+ bool[] toggles = { true, false, false, false, false };
+ //Context menus
+ float value = 0.5f;
+ string name = "Label1";
+ //Modals
+ bool show = false;
+ bool show_stacked = false;
+ bool dont_ask_me_next_time = false;
+ int item_mod = 1;
+ Vec4 color = new Vec4(0.4f, 0.7f, 0.0f, 0.5f);
+ #endregion
+
+ private void DemoWindowPopups()
+ {
+ if (!ImGui.CollapsingHeader("Popups & Modal windows"))
+ {
+ return;
+ }
+
+ //Popups
+ #region Popups
+ if (ImGui.TreeNode("Popups"))
+ {
+ ImGui.TextWrapped("When a popup is active, it inhibits interacting with windows that are behind the popup. " +
+ "Clicking outside the popup closes it.");
+
+ string[] names = { "Bream", "Haddock", "Mackerel", "Pollock", "Tilefish" };
+
+ if (ImGui.Button("Select.."))
+ {
+ ImGui.OpenPopup("my_select_popup");
+ }
+ ImGui.SameLine();
+ ImGui.TextUnformatted(selected_fish == -1 ? "<None>" : names[selected_fish]);
+ if (ImGui.BeginPopup("my_select_popup"))
+ {
+ ImGui.Text("Aquarium");
+ ImGui.Separator();
+ for (int i = 0; i < names.Length; i++)
+ {
+ if (ImGui.Selectable(names[i]))
+ {
+ selected_fish = i;
+ }
+ }
+ ImGui.EndPopup();
+ }
+
+ //menu with toggles
+ if (ImGui.Button("Toggle.."))
+ {
+ ImGui.OpenPopup("my_toggle_popup");
+ }
+ if (ImGui.BeginPopup("my_toggle_popup"))
+ {
+ for (int i = 0; i < names.Length; i++)
+ {
+ ImGui.MenuItem(names[i], "", ref toggles[i]);
+ }
+ if (ImGui.BeginMenu("Sub-menu"))
+ {
+ ImGui.MenuItem("Click me");
+ ImGui.EndMenu();
+ }
+
+ ImGui.Separator();
+ ImGui.Text("Tooltip here");
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("I am a tooltip over a popup");
+ }
+
+ if (ImGui.Button("Stacked Popup"))
+ {
+ ImGui.OpenPopup("another popup");
+ }
+ if (ImGui.BeginPopup("another popup"))
+ {
+ for (int i = 0; i < names.Length; i++)
+ {
+ ImGui.MenuItem(names[i], "", ref toggles[i]);
+ }
+ if (ImGui.BeginMenu("Sub-menu"))
+ {
+ ImGui.MenuItem("Click me");
+ if (ImGui.Button("Stacked Popup"))
+ {
+ ImGui.OpenPopup("another popup");
+ }
+ if (ImGui.BeginPopup("another popup"))
+ {
+ ImGui.Text("I am the last one here.");
+ ImGui.EndPopup();
+ }
+ ImGui.EndMenu();
+ }
+ ImGui.EndPopup();
+ }
+ ImGui.EndPopup();
+ }
+
+ if (ImGui.Button("File Menu.."))
+ {
+ ImGui.OpenPopup("my_file_popup");
+ }
+ if (ImGui.BeginPopup("my_file_popup"))
+ {
+ ShowExampleMenuFile();
+ ImGui.EndPopup();
+ }
+
+ ImGui.TreePop();
+ }
+ #endregion
+
+ //Context menus
+ #region Context menus
+ if (ImGui.TreeNode("Context menus"))
+ {
+ ImGui.Text(string.Format("Value = {0} (<-- right-click here)", value));
+ if (ImGui.BeginPopupContextItem("item context menu"))
+ {
+ if (ImGui.Selectable("Set to zero")) { value = 0.0f; }
+ if (ImGui.Selectable("Set to PI")) { value = 3.1415f; }
+ ImGui.SetNextItemWidth(-1.0f);
+ ImGui.DragFloat("##Value", ref value, 0.1f, 0.0f, 0.0f);
+ ImGui.EndPopup();
+ }
+
+ ImGui.Text("(You can also right-click me to open the same popup as above.)");
+ ImGui.OpenPopupOnItemClick("item context menu", ImGuiPopupFlags.MouseButtonRight);
+
+ ImGui.Button(string.Format("Button: {0}###Button", name));
+ if (ImGui.BeginPopupContextItem())
+ {
+ ImGui.Text("Edit name:");
+ ImGui.InputText("##edit", ref name, 100);
+ if (ImGui.Button("Close"))
+ {
+ ImGui.CloseCurrentPopup();
+ }
+ ImGui.EndPopup();
+ }
+ ImGui.SameLine(); ImGui.Text("(<-- right-click here)");
+
+ ImGui.TreePop();
+ }
+ #endregion
+
+ //Modals
+ #region Modals
+ if (ImGui.TreeNode("Modals"))
+ {
+ ImGui.TextWrapped("Modal windows are like popups but the user cannot close them by clicking outside.");
+
+ if (ImGui.Button("Delete.."))
+ {
+ ImGui.OpenPopup("Delete?");
+ show = true;
+ }
+
+ Vec2 center = new Vec2(400, 400);
+ ImGui.SetNextWindowPos(center, ImGuiCond.Appearing, new Vec2(0.5f, 0.5f));
+
+ if (ImGui.BeginPopupModal("Delete?", ref show, ImGuiWindowFlags.AlwaysAutoResize))
+ {
+ ImGui.Text("All those beautiful files will be deleted.\nThis operation cannot be undone!\n\n");
+ ImGui.Separator();
+
+ ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vec2(0, 0));
+ ImGui.Checkbox("Don't ask me next time", ref dont_ask_me_next_time);
+ ImGui.PopStyleVar();
+
+ if (ImGui.Button("OK", new Vec2(120, 0))) { ImGui.CloseCurrentPopup(); }
+ ImGui.SetItemDefaultFocus();
+ ImGui.SameLine();
+ if (ImGui.Button("Cancel", new Vec2(120, 0))) { ImGui.CloseCurrentPopup(); }
+ ImGui.EndPopup();
+ }
+
+ if (ImGui.Button("Stacked modals.."))
+ {
+ ImGui.OpenPopup("Stacked 1");
+ show_stacked = true;
+ }
+ if (ImGui.BeginPopupModal("Stacked 1", ref show_stacked, ImGuiWindowFlags.MenuBar))
+ {
+ if (ImGui.BeginMenuBar())
+ {
+ if (ImGui.BeginMenu("File"))
+ {
+ if (ImGui.MenuItem("Some menu item")) { }
+ ImGui.EndMenu();
+ }
+ ImGui.EndMenuBar();
+ }
+ ImGui.Text("Hello from Stacked The First\nUsing style.Colors[ImGuiCol_ModalWindowDimBg] behind it.");
+
+ ImGui.Combo("Combo", ref item_mod, "aaaa\0bbbb\0cccc\0dddd\0eeee\0\0");
+ ImGui.ColorEdit4("color", ref color);
+
+ if (ImGui.Button("Add another modal.."))
+ {
+ ImGui.OpenPopup("Stacked 2");
+ }
+
+ bool unused_open = true;
+ if (ImGui.BeginPopupModal("Stacked 2", ref unused_open))
+ {
+ ImGui.Text("Hello from Stacked The Second!");
+ if (ImGui.Button("Close"))
+ {
+ ImGui.CloseCurrentPopup();
+ }
+ ImGui.EndPopup();
+ }
+
+ if (ImGui.Button("Close"))
+ {
+ ImGui.CloseCurrentPopup();
+ }
+ ImGui.EndPopup();
+ }
+ ImGui.TreePop();
+ }
+ #endregion
+
+ }
+
+ #region DemoWindowMiscVariables
+ string buf = "hello";
+ #endregion
+
+ private void DemoWindowMisc()
+ {
+ if (ImGui.CollapsingHeader("Inputs, Navigation & Focus"))
+ {
+ ImGuiIOPtr io = ImGui.GetIO();
+
+ ImGui.Text(string.Format("WantCaptureMouse: {0}", io.WantCaptureMouse));
+ ImGui.Text(string.Format("WantCaptureKeyboard: {0}", io.WantCaptureKeyboard));
+ ImGui.Text(string.Format("WantTextInput: {0}", io.WantTextInput));
+ ImGui.Text(string.Format("WantSetMousePos: {0}", io.WantSetMousePos));
+ ImGui.Text(string.Format("NavActive: {0}, NavVisible: {1}", io.NavActive, io.NavVisible));
+
+ //keyboard mouse state
+ if (ImGui.TreeNode("Keyboard, Mouse & Navigation State"))
+ {
+ if (ImGui.IsMousePosValid())
+ {
+ ImGui.Text(string.Format("Mouse pos: ({0}, {1})", io.MousePos.X, io.MousePos.Y));
+ }
+ else
+ {
+ ImGui.Text("Mouse pos: <INVALID>");
+ }
+ ImGui.Text(string.Format("Mouse delta: ({0}, {1})", io.MouseDelta.X, io.MouseDelta.Y));
+ ImGui.Text("Mouse down:"); for (int i = 0; i < io.MouseDown.Count; i++) if (io.MouseDownDuration[i] >= 0.0f) { ImGui.SameLine(); ImGui.Text(string.Format("{0} ({1} secs)", i, io.MouseDownDuration[i])); }
+ ImGui.Text("Mouse clicked:"); for (int i = 0; i < io.MouseDown.Count; i++) if (ImGui.IsMouseClicked((ImGuiNET.ImGuiMouseButton)i)) { ImGui.SameLine(); ImGui.Text(i.ToString()); }
+ ImGui.Text("Mouse dblclick:"); for (int i = 0; i < io.MouseDown.Count; i++) if (ImGui.IsMouseDoubleClicked((ImGuiNET.ImGuiMouseButton)i)) { ImGui.SameLine(); ImGui.Text(i.ToString()); }
+ ImGui.Text("Mouse released:"); for (int i = 0; i < io.MouseDown.Count; i++) if (ImGui.IsMouseReleased((ImGuiNET.ImGuiMouseButton)i)) { ImGui.SameLine(); ImGui.Text(i.ToString()); }
+ ImGui.Text(string.Format("Mouse wheel: {0}", io.MouseWheel));
+
+// TODO: Not Supported
+ /*
+ ImGui.Text("Keys down:"); for (int i = 0; i < io.KeysDown.Count; i++) if (io.KeysDownDuration[i] >= 0.0f) { ImGui.SameLine(); ImGui.Text(string.Format("{0} ({1}) ({2} secs)", i, i, io.KeysDownDuration[i])); }
+ ImGui.Text("Keys pressed:"); for (int i = 0; i < io.KeysDown.Count; i++) if (ImGui.IsKeyPressed((ImGuiNET.ImGuiKey)i)) { ImGui.SameLine(); ImGui.Text(string.Format("{0} ({1})", i, i)); }
+ ImGui.Text("Keys release:"); for (int i = 0; i < io.KeysDown.Count; i++) if (ImGui.IsKeyReleased((ImGuiNET.ImGuiKey)i)) { ImGui.SameLine(); ImGui.Text(string.Format("{0} ({1})", i, i)); }
+ ImGui.Text(string.Format("Keys mods: {0}{1}{2}{3}", io.KeyCtrl ? "CTRL " : "", io.KeyShift ? "SHIFT " : "", io.KeyAlt ? "ALT " : "", io.KeySuper ? "SUPER " : ""));
+ ImGui.Text("Chars queue:"); for (int i = 0; i < io.InputQueueCharacters.Size; i++) { ushort c = io.InputQueueCharacters[i]; ImGui.SameLine(); ImGui.Text(string.Format("{0} {1}", (c > ' ' && c <= 255) ? (char)c : '?', c)); } // FIXME: Does not show chars as in example
+
+ ImGui.Text("NavInputs down:"); for (int i = 0; i < io.NavInputs.Count; i++) if (io.NavInputs[i] > 0.0f) { ImGui.SameLine(); ImGui.Text(string.Format("[{0}] {1}", i, io.NavInputs[i])); }
+ ImGui.Text("NavInputs pressed:"); for (int i = 0; i < io.NavInputs.Count; i++) if (io.NavInputsDownDuration[i] == 0.0f) { ImGui.SameLine(); ImGui.Text(string.Format("[{0}]", i)); }
+ ImGui.Text("NavInputs duration:"); for (int i = 0; i < io.NavInputs.Count; i++) if (io.NavInputsDownDuration[i] >= 0.0f) { ImGui.SameLine(); ImGui.Text(string.Format("[{0}] {1}", i, io.NavInputsDownDuration[i])); }
+ */
+
+
+ ImGui.Button("Hovering me sets the\nkeyboard capture flag");
+ if (ImGui.IsItemHovered())
+ {
+// TODO: Not Supported
+ }
+ ImGui.SameLine();
+ ImGui.Button("Holding me clears the\nthe keyboard capture flag");
+ if (ImGui.IsItemActive())
+ { }
+// TODO: Not Supported
+// ImGui.CaptureKeyboardFromApp(true);
+ }
+ ImGui.SameLine();
+ ImGui.Button("Holding me clears the\nthe keyboard capture flag");
+ if (ImGui.IsItemActive())
+ {
+ // ImGui.CaptureKeyboardFromApp(false);
+ }
+ ImGui.TreePop();
+ }
+
+ if (ImGui.TreeNode("Tabbing"))
+ {
+ ImGui.Text("Use TAB/SHIFT+TAB to cycle through keyboard editable fields.");
+ ImGui.InputText("1", ref buf, 100);
+ ImGui.InputText("2", ref buf, 100);
+ ImGui.InputText("3", ref buf, 100);
+// TODO: Not Supported
+// ImGui.PushAllowKeyboardFocus(false);
+ ImGui.InputText("4 (tab skip)", ref buf, 100);
+// TODO: Not Supported
+// ImGui.PopAllowKeyboardFocus();
+ ImGui.InputText("5", ref buf, 100);
+ ImGui.TreePop();
+ }
+ }
+
+
+ #region DrawMonoGameWindowVariables
+ bool show_main_window = true;
+ bool exit_app = false;
+ int current_res = 0;
+ int select_res = 0;
+ int render_model = 1;
+ string[] resolution = { "1024x768", "1280x720", "1280x960", "1366x768", "1440x1080", "1680x1050", "1600x1200", "1920x1080" };
+ Vec4 monogame_color = new Vec4(231.0f / 255.0f, 60.0f / 255.0f, 0.0f / 255.0f, 200.0f / 255.0f);
+ Vec4 monogame_framebg = new Vec4(227.0f / 255.0f, 227.0f / 255.0f, 227.0f / 255.0f, 255.0f / 255.0f);
+ Vec4 color_black = new Vec4(0.0f / 255.0f, 0.0f / 0.0f, 0.0f / 255.0f, 200.0f / 255.0f);
+ Vec4 color_white = new Vec4(255.0f / 255.0f, 255.0f / 255.0f, 255.0f / 255.0f, 242.0f / 255.0f);
+ #endregion
+
+ private void DrawMonoGameWindow()
+ {
+ ImGui.SetNextWindowSize(new Vec2(300, 220), ImGuiCond.FirstUseEver);
+ ImGui.SetNextWindowPos(new Vec2(10, 140), ImGuiCond.FirstUseEver);
+
+ ImGui.PushStyleColor(ImGuiCol.WindowBg, color_white);
+ ImGui.PushStyleColor(ImGuiCol.FrameBg, monogame_framebg);
+ ImGui.PushStyleColor(ImGuiCol.PopupBg, monogame_framebg);
+ ImGui.PushStyleColor(ImGuiCol.MenuBarBg, monogame_framebg);
+ ImGui.PushStyleColor(ImGuiCol.TitleBg, monogame_color);
+ ImGui.PushStyleColor(ImGuiCol.TitleBgActive, monogame_color);
+ ImGui.PushStyleColor(ImGuiCol.Button, monogame_color);
+
+ if (!ImGui.Begin("MonoGame Settings", ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.MenuBar))
+ {
+ ImGui.End();
+ return;
+ }
+
+ ImGui.PushStyleColor(ImGuiCol.Text, color_black);
+
+ if (ImGui.BeginMenuBar())
+ {
+ if (ImGui.BeginMenu("Options"))
+ {
+ ImGui.MenuItem("Exit App", null, ref exit_app);
+
+ ImGui.EndMenu();
+ }
+
+ if (ImGui.BeginMenu("ImGui"))
+ {
+ ImGui.MenuItem("Show Demo Window", null, ref show_main_window);
+
+ ImGui.EndMenu();
+ }
+
+ ImGui.EndMenuBar();
+ }
+
+ if (ImGui.SmallButton("Close"))
+ {
+ show_monogame_settings = false;
+
+ if (show_main_window == false)
+ {
+ show_main_window = true;
+ }
+ }
+
+ ImGui.Separator();
+
+ //ImGui.ColorEdit4("MG", ref monogame_color, ImGuiColorEditFlags.DisplayHSV);
+ //ImGui.Text(monogame_color.ToString());
+
+ ImGui.Combo("Window Size", ref select_res, resolution, resolution.Length);
+
+ if (current_res != select_res && WasResized == false)
+ {
+ WasResized = true;
+ }
+
+ ImGui.PushStyleColor(ImGuiCol.CheckMark, monogame_color);
+
+ ImGui.RadioButton("No render", ref render_model, 0);
+ ImGui.SameLine();
+ ImGui.RadioButton("Render model", ref render_model, 1);
+
+ ImGui.Separator();
+
+ ImGui.Text("Camera");
+ ImGui.PushButtonRepeat(true);
+
+ ImGui.Indent();
+ if (ImGui.ArrowButton("up", ImGuiDir.Up)) //ImGui.Button("Up"))
+ {
+ world = world * Matrix.CreateRotationX(0.1f);
+ }
+ ImGui.Unindent();
+ if (ImGui.ArrowButton("left", ImGuiDir.Left)) //ImGui.Button("Left"))
+ {
+ world = world * Matrix.CreateRotationY(-0.1f);
+ }
+ ImGui.SameLine();
+ ImGui.Text(" ");
+ ImGui.SameLine();
+ if (ImGui.ArrowButton("right", ImGuiDir.Right)) //ImGui.Button("Right"))
+ {
+ world = world * Matrix.CreateRotationY(0.1f);
+ }
+
+ ImGui.Indent();
+ if (ImGui.ArrowButton("down", ImGuiDir.Down)) //ImGui.Button("Down"))
+ {
+ world = world * Matrix.CreateRotationX(-0.1f);
+ }
+ ImGui.Unindent();
+ ImGui.PopButtonRepeat();
+
+ ImGui.PopStyleColor();
+ ImGui.PopStyleColor();
+ ImGui.PopStyleColor();
+ ImGui.PopStyleColor();
+ ImGui.PopStyleColor();
+ ImGui.PopStyleColor();
+ ImGui.PopStyleColor();
+ ImGui.PopStyleColor();
+ ImGui.PopStyleColor();
+
+ ImGui.End();
+
+ if (exit_app)
+ {
+ this.Exit();
+ }
+ }
+ }
+}
diff --git a/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Icon.bmp b/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Icon.bmp
new file mode 100644
index 0000000..2b48165
--- /dev/null
+++ b/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Icon.bmp
Binary files differ
diff --git a/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Icon.ico b/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Icon.ico
new file mode 100644
index 0000000..7d9dec1
--- /dev/null
+++ b/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Icon.ico
Binary files differ
diff --git a/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Monogame.ImGuiNetSamples.csproj b/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Monogame.ImGuiNetSamples.csproj
new file mode 100644
index 0000000..cc75871
--- /dev/null
+++ b/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Monogame.ImGuiNetSamples.csproj
@@ -0,0 +1,33 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <OutputType>WinExe</OutputType>
+ <TargetFramework>net8.0</TargetFramework>
+ <RollForward>Major</RollForward>
+ <PublishReadyToRun>false</PublishReadyToRun>
+ <TieredCompilation>false</TieredCompilation>
+ </PropertyGroup>
+ <PropertyGroup>
+ <ApplicationManifest>app.manifest</ApplicationManifest>
+ <ApplicationIcon>Icon.ico</ApplicationIcon>
+ <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
+ </PropertyGroup>
+ <ItemGroup>
+ <None Remove="Icon.ico" />
+ <None Remove="Icon.bmp" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="Icon.ico" />
+ <EmbeddedResource Include="Icon.bmp" />
+ </ItemGroup>
+ <ItemGroup>
+ <PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.303" />
+ <PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.1.303" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\MonoGame.ImGuiNet\Monogame.ImGuiNet.csproj" />
+ </ItemGroup>
+ <Target Name="RestoreDotnetTools" BeforeTargets="Restore">
+ <Message Text="Restoring dotnet tools" Importance="High" />
+ <Exec Command="dotnet tool restore" />
+ </Target>
+</Project> \ No newline at end of file
diff --git a/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Program.cs b/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Program.cs
new file mode 100644
index 0000000..83d14f7
--- /dev/null
+++ b/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/Program.cs
@@ -0,0 +1,3 @@
+
+using var game = new Monogame.ImGuiNetSamples.Game1();
+game.Run();
diff --git a/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/app.manifest b/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/app.manifest
new file mode 100644
index 0000000..42e5dba
--- /dev/null
+++ b/Plugins/MonoGame.ImGuiNet/Monogame.ImGuiNetSamples/app.manifest
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
+ <assemblyIdentity version="1.0.0.0" name="Monogame.ImGuiNetSamples"/>
+ <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
+ <security>
+ <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
+ <requestedExecutionLevel level="asInvoker" uiAccess="false" />
+ </requestedPrivileges>
+ </security>
+ </trustInfo>
+
+ <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+ <application>
+ <!-- A list of the Windows versions that this application has been tested on and is
+ is designed to work with. Uncomment the appropriate elements and Windows will
+ automatically selected the most compatible environment. -->
+
+ <!-- Windows Vista -->
+ <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
+
+ <!-- Windows 7 -->
+ <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
+
+ <!-- Windows 8 -->
+ <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
+
+ <!-- Windows 8.1 -->
+ <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
+
+ <!-- Windows 10 -->
+ <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
+
+ </application>
+ </compatibility>
+
+ <application xmlns="urn:schemas-microsoft-com:asm.v3">
+ <windowsSettings>
+ <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
+ <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness>
+ </windowsSettings>
+ </application>
+
+</assembly>
diff --git a/Plugins/MonoGame.ImGuiNet/NugetpkgIcon.png b/Plugins/MonoGame.ImGuiNet/NugetpkgIcon.png
new file mode 100644
index 0000000..e886023
--- /dev/null
+++ b/Plugins/MonoGame.ImGuiNet/NugetpkgIcon.png
Binary files differ
diff --git a/Plugins/MonoGame.ImGuiNet/README.md b/Plugins/MonoGame.ImGuiNet/README.md
new file mode 100644
index 0000000..322b598
--- /dev/null
+++ b/Plugins/MonoGame.ImGuiNet/README.md
@@ -0,0 +1,39 @@
+# MonoGame.ImGuiNet
+
+![Monogame.ImGuiNet Logo](Images/ReadMeBanner.png)
+
+**MonoGame.ImGuiNet** is a feature-rich extension of ImGuiNet, enhanced with a set of additional components designed exclusively for MonoGame (Which should work for XNA/FNA as well!). We give full credit to the creators of ImGuiNet for their excellent work.
+
+## Getting Started
+
+- **Installation**: Detailed installation instructions can be found in [this issue](https://github.com/Mezo-hx/MonoGame.ImGuiNet/issues/1).
+
+- **Wiki**: Explore the [Wiki](https://github.com/Mezo-hx/MonoGame.ImGuiNet/wiki) for documentation and guides to get started.
+
+## Notable Mentions
+
+We would like to acknowledge and thank [Dovker](https://github.com/dovker) for the inspiration provided by their original Monogame.imgui project. Their work served as a foundation for our efforts, as the original project was unmaintained.
+
+# Changelog
+
+**Update 0.1:**
+- Changed `BeforeLayout` and `AfterLayout` to `BeginLayout` and `EndLayout`
+- Fixed the lag issue when holding Shift or CTRL on the ImGui Window
+
+# Roadmap
+
+- **Version 1.0** (Future Release)
+ - Live Debugging Tools: Add an easy to use Live Debugging Tool or Profiler.
+ - . . .
+
+> Please note, that we will not be making any promises! so if we didn't add what will be said in our Roadmap, it's either still being worked on, or we were unable to do it!
+
+---
+
+# Contributors
+
+[![Contributors](https://contrib.rocks/image?repo=Mezo-hx/MonoGame.ImGuiNet)](https://github.com/Mezo-hx/MonoGame.ImGuiNet/graphs/contributors)
+
+---
+
+**Feel free to contribute, report issues, or provide feedback! We appreciate your support in making MonoGame.ImGuiNet even better.**