Pre Release

This commit is contained in:
2026-04-15 19:00:46 +03:00
commit 4034c7ce94
25 changed files with 4756 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
+34
View File
@@ -0,0 +1,34 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GridCasting", "GridCasting\GridCasting.csproj", "{D26523E3-132B-42EB-891C-45EBF8B98C65}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IgdrasilMath", "..\IgdrasilEngine\IgdrasilMath\IgdrasilMath.csproj", "{10E6C5FC-990E-429E-9635-E4F639FF6B9D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GuidingTurtle", "..\IgdrasilEngine\GuidingTurtle\GuidingTurtle.csproj", "{2575A7D5-2936-4823-A0B4-A466790D70B5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IgdrasilEngine", "IgdrasilEngine", "{41458A8F-715B-405E-84E2-140989D50977}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D26523E3-132B-42EB-891C-45EBF8B98C65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D26523E3-132B-42EB-891C-45EBF8B98C65}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D26523E3-132B-42EB-891C-45EBF8B98C65}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D26523E3-132B-42EB-891C-45EBF8B98C65}.Release|Any CPU.Build.0 = Release|Any CPU
{10E6C5FC-990E-429E-9635-E4F639FF6B9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{10E6C5FC-990E-429E-9635-E4F639FF6B9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{10E6C5FC-990E-429E-9635-E4F639FF6B9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{10E6C5FC-990E-429E-9635-E4F639FF6B9D}.Release|Any CPU.Build.0 = Release|Any CPU
{2575A7D5-2936-4823-A0B4-A466790D70B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2575A7D5-2936-4823-A0B4-A466790D70B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2575A7D5-2936-4823-A0B4-A466790D70B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2575A7D5-2936-4823-A0B4-A466790D70B5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{2575A7D5-2936-4823-A0B4-A466790D70B5} = {41458A8F-715B-405E-84E2-140989D50977}
{10E6C5FC-990E-429E-9635-E4F639FF6B9D} = {41458A8F-715B-405E-84E2-140989D50977}
EndGlobalSection
EndGlobal
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
using Path = GridCasting.Models.Path;
namespace GridCasting.Executor;
public class CommandContext
{
public Path Command { get; }
public IDictionary<string, object> Environment { get; }
public Stack<object> Stack { get; }
internal CommandContext(Path command, IDictionary<string, object> environment, Stack<object> stack)
{
Command = command;
Environment = environment;
Stack = stack;
}
}
+8
View File
@@ -0,0 +1,8 @@
using Path = GridCasting.Models.Path;
namespace GridCasting.Executor;
public interface ICommand
{
void Execute(CommandContext context);
}
@@ -0,0 +1,49 @@
namespace GridCasting.Executor;
public interface IEnvironmentResolver
{
/// <summary>
/// Loads and returns a collection of key-value pairs representing environment variables.
/// </summary>
/// <returns>
/// A collection of key-value pairs where the key is a string representing the variable name
/// and the value is the associated object.
/// </returns>
public IEnumerable<KeyValuePair<string, object>> OnLoad();
/// <summary>
/// Unloads and performs necessary cleanup operations for the current environment,
/// ensuring resources are released and any outstanding tasks are handled appropriately.
/// </summary>
public void OnUnload();
/// <summary>
/// Resets the environment variable identified by the specified key to its default value.
/// </summary>
/// <param name="key">The string key of the environment variable to reset.</param>
/// <returns>
/// The default value of the environment variable as an object if a reset value is defined;
/// otherwise, null.
/// </returns>
public object? OnReset(string key);
/// <summary>
/// Updates the value of the specified environment variable to a new value.
/// </summary>
/// <param name="key">The string key representing the name of the environment variable to update.</param>
/// <param name="value">The new value to assign to the specified environment variable.</param>
public void OnChange(string key, object value);
/// <summary>
/// An event that is triggered whenever an environment variable is updated.
/// </summary>
/// <remarks>
/// The event provides the key of the updated variable and its new value. This is useful for monitoring
/// changes to environment variables in real-time and ensuring the associated logic reacts accordingly.
/// </remarks>
/// <event>
/// Subscribing to this event allows external components to respond dynamically to updates
/// in the environment configuration.
/// </event>
public event Action<string, object> OnUpdate;
}
+232
View File
@@ -0,0 +1,232 @@
using GridCasting.Utils;
using Path = GridCasting.Models.Path;
namespace GridCasting.Executor;
/// <summary>
/// Represents a class that handles the execution of commands associated with specific path patterns.
/// Manages environment variables through a listenable dictionary and integrates capabilities for
/// reacting to changes within these variables.
/// </summary>
/// <remarks>
/// The <see cref="PathExecutor"/> class is designed to facilitate the execution of path-related commands
/// while allowing dynamic management of environment-specific variables. It supports the registration of
/// environment resolvers, maintains a collection of commands mapped to path patterns, and provides methods
/// for interacting with and executing commands.
/// </remarks>
public class PathExecutor
{
/// <summary>
/// Stores the internal mapping of commands to their corresponding path patterns.
/// This data structure associates commands, which implement the <see cref="ICommand"/> interface,
/// with specific path patterns and optionally supports pattern families for extended matching capabilities.
/// </summary>
/// <remarks>
/// The field leverages a <see cref="Trie{TNode, TValue}"/> to efficiently manage
/// hierarchical patterns and improve lookup performance during command execution.
/// This is an internal implementation detail and is used to match and retrieve
/// relevant commands based on the incoming path during execution.
/// </remarks>
private readonly Trie<int, ICommand> _commands = new();
/// <summary>
/// Maintains a collection of key-value pairs representing environment-specific variables and their associated values.
/// This dictionary acts as the primary storage for environment-related data used during path execution and command processing.
/// </summary>
/// <remarks>
/// The dictionary is updated and accessed through various methods in the <see cref="PathExecutor"/> class,
/// often in conjunction with environment resolvers that implement the <see cref="IEnvironmentResolver"/> interface.
/// Additionally, changes to this dictionary are synchronized with the listenable wrapper <see cref="ListenableDictionary{TKey, TValue}"/>
/// to trigger appropriate events such as updates, removals, and clear operations.
/// </remarks>
private readonly Dictionary<string, object> _environmentVariables = new();
/// <summary>
/// Represents a listenable dictionary that wraps environment variables
/// with the ability to react to updates, removals, and clearing of its contents.
/// </summary>
/// <remarks>
/// This field is a <see cref="ListenableDictionary{TKey, TValue}"/> object
/// that allows event-driven responses when changes occur in the underlying
/// environment variables. It builds upon a base dictionary to manage key-value pairs,
/// while invoking relevant event handlers for observed modifications.
/// </remarks>
private readonly ListenableDictionary<string, object> _environmentVariablesListenable;
/// <summary>
/// Maintains a mapping between environment variable keys and their corresponding environment resolvers.
/// This dictionary enables association of specific environment variable keys with implementations
/// of the <see cref="IEnvironmentResolver"/> interface to facilitate dynamic updates, load, and unload operations.
/// </summary>
/// <remarks>
/// This mapping is used to handle the assignment and resolution of environment variables within the
/// execution context. Each environment variable key is associated with an instance of an
/// <see cref="IEnvironmentResolver"/>, which provides the logic for loading, updating, and
/// unloading environment variables. Changes to the variable keys trigger relevant resolver events for
/// synchronized state management.
/// </remarks>
private readonly Dictionary<string, IEnvironmentResolver> _environmentResolverMap = new();
/// <summary>
/// Represents the execution stack used internally by the <see cref="PathExecutor"/>.
/// This stack manages objects relevant to the execution context of commands,
/// facilitating state tracking and enabling command chaining during execution flows.
/// </summary>
/// <remarks>
/// The stack operates as a first-in-last-out (FILO) data structure, where elements
/// added during command execution are processed in reverse order of addition.
/// It is primarily used to maintain the execution context and intermediate
/// results during command processing within the PathExecutor.
/// </remarks>
private readonly Stack<object> _stack = new();
/// <summary>
/// Handles the execution of commands associated with specific path patterns and manages environment variables
/// with the ability to listen and react to changes.
/// </summary>
/// <remarks>
/// This class integrates a listenable dictionary for environment variables, enabling event-driven responses to updates,
/// removals, or full-clears of its contents. It acts as a mediator for resolving context and executing commands
/// based on the given path patterns.
/// </remarks>
public PathExecutor()
{
_environmentVariablesListenable = new ListenableDictionary<string, object>(_environmentVariables);
_environmentVariablesListenable.OnUpdate += OnUpdate;
_environmentVariablesListenable.OnRemove += OnRemove;
_environmentVariablesListenable.OnClear += OnClear;
}
/// <summary>
/// Gets the collection of context resolvers added to the PathExecutor.
/// These context resolvers are responsible for handling specific execution contexts
/// during the execution of commands.
/// </summary>
/// <remarks>
/// The property returns a read-only list of <see cref="IEnvironmentResolver"/> instances
/// that have been registered using the <see cref="AddEnvironmentResolver"/>.
/// </remarks>
public IEnumerable<IEnvironmentResolver> Resolvers => _environmentResolverMap.Values;
/// <summary>
/// Adds an environment resolver and integrates its associated variables and update events into the PathExecutor.
/// </summary>
/// <param name="resolver">
/// The environment resolver to be added. This resolver provides a collection of environment variables
/// on load and triggers update events when its state changes.
/// </param>
public void AddEnvironmentResolver(IEnvironmentResolver resolver)
{
foreach (var (key, value) in resolver.OnLoad())
{
_environmentVariables.Add(key, value);
_environmentResolverMap.Add(key, resolver);
}
resolver.OnUpdate += OnResolverUpdate;
}
/// <summary>
/// Removes an environment resolver and clears all associated environment variables
/// from the resolver's mapped context.
/// </summary>
/// <param name="resolver">The environment resolver to be removed. It will be unregistered
/// from its update events, and its context will be cleaned up.</param>
public void RemoveContextResolver(IEnvironmentResolver resolver)
{
// Unmap all environment variables
resolver.OnUpdate -= OnResolverUpdate;
_environmentResolverMap.Where(x => x.Value == resolver).ToList()
.ForEach(x =>
{
_environmentResolverMap.Remove(x.Key);
_environmentVariables.Remove(x.Key);
});
resolver.OnUnload();
}
/// <summary>
/// Adds a command to be executed with an associated path pattern and optional pattern family configuration.
/// </summary>
/// <param name="command">The command to add. Must implement the <see cref="ICommand"/> interface.</param>
/// <param name="pattern">The path object used to define the execution pattern for the command.</param>
/// <param name="patternFamily">A boolean flag indicating whether the path pattern should be treated as a family of patterns. Defaults to false.</param>
public void AddCommand(ICommand command, Path pattern, bool patternFamily = false) =>
_commands.Set(pattern.ToArray(), patternFamily, command);
/// <summary>
/// Clears all objects from the execution stack of the PathExecutor.
/// This operation resets the stack to its initial state, removing any previously added elements.
/// </summary>
public void ResetStack() => _stack.Clear();
/// <summary>
/// Executes the command associated with the provided path.
/// If a matching command is found, it is executed within the provided execution context.
/// </summary>
/// <param name="path">The path object defining the start node and directions for the execution.</param>
/// <returns>
/// Returns <c>true</c> if a matching command for the specified path is found and executed successfully;
/// otherwise, returns <c>false</c>.
/// </returns>
public bool Execute(Path path)
{
if (!_commands.TryGetValue(path.ToArray(), out var command)) return false;
command.Execute(new CommandContext(
path,
_environmentVariablesListenable,
_stack
));
return true;
}
/// <summary>
/// Handles updates to environment variables by notifying the relevant environment resolver about the change.
/// </summary>
/// <param name="key">The key of the environment variable that has been updated.</param>
/// <param name="value">The new value associated with the updated key.</param>
private void OnUpdate(string key, object value)
{
if (!_environmentResolverMap.TryGetValue(key, out var resolver)) return;
resolver.OnChange(key, value);
}
/// <summary>
/// Handles the removal of a specific environment variable from the listenable dictionary and restores it to its default value
/// if defined by the associated resolver.
/// </summary>
/// <param name="key">The key of the environment variable to be removed and potentially reset.</param>
private void OnRemove(string key)
{
if (!_environmentResolverMap.TryGetValue(key, out var resolver)) return;
var defaultValue = resolver.OnReset(key);
if (defaultValue != null) _environmentVariables[key] = defaultValue;
}
/// <summary>
/// Resets environment variables by invoking the reset logic for each associated environment resolver
/// and re-populating the variables with their default values if provided.
/// </summary>
/// <remarks>
/// This method is triggered when the `OnClear` event is raised from the `ListenableDictionary`.
/// It iterates through the configured environment resolvers, calls their reset method for each key,
/// and updates the environment variables with reset defaults if applicable.
/// </remarks>
private void OnClear()
{
foreach (var (key, resolver) in _environmentResolverMap)
{
var defaultValue = resolver.OnReset(key);
if (defaultValue != null) _environmentVariables[key] = defaultValue;
}
}
/// <summary>
/// Updates the value of an environment variable in the internal storage when a corresponding change is triggered
/// by an environment resolver.
/// </summary>
/// <param name="key">The key of the environment variable being updated.</param>
/// <param name="value">The new value for the specified environment variable.</param>
private void OnResolverUpdate(string key, object value) => _environmentVariables[key] = value;
}
+23
View File
@@ -0,0 +1,23 @@
using GridCasting.Executor;
using GridCasting.Models.GridGraph;
using GridCasting.Transform;
using IgdrasilEngine.Engine.Math.Vectors;
namespace GridCasting;
public class GridCasting(
PathExecutor executor,
GridGraph graph,
float sensitivity,
params IEnumerable<IPathTransform> transforms)
{
private readonly GridResolver _resolver = new(graph, sensitivity);
private readonly PathExecutor _executor = executor;
private readonly IEnumerable<IPathTransform> _transforms = transforms;
public void Execute(FVector2[] positions)
{
var path = _resolver.GetPath(positions);
}
}
+18
View File
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\IgdrasilEngine\IgdrasilMath\IgdrasilMath.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Transform\PathTransforms\" />
</ItemGroup>
</Project>
+41
View File
@@ -0,0 +1,41 @@
using System.Collections;
namespace GridCasting.Models.Grid;
/// <summary>
/// Represents a grid structure that serves as a core abstraction for spatially organizing and managing elements
/// in a grid-based layout or system. Provides foundational functionality to integrate with and support operations
/// like grid graph generation, position resolution, and pathfinding using other related classes.
/// </summary>
public class Grid : IEnumerable<GridNode>
{
/// <summary>
/// Represents a collection of <see cref="GridNode"/> instances within a grid structure.
/// The <c>Nodes</c> variable holds the individual grid elements that collectively
/// form the grid, allowing for operations such as traversal, manipulation, or querying
/// of the underlying grid layout.
/// </summary>
public List<GridNode> Nodes { get; } = [];
/// <summary>
/// Provides indexed access to the grid nodes within the grid structure
/// by returning the element at the specified position in the node collection.
/// </summary>
/// <param name="index">The zero-based index of the grid node in the collection.</param>
/// <returns>Returns the <see cref="GridNode"/> instance located at the specified index.</returns>
public GridNode this[int index] => Nodes[index];
/// <summary>
/// Returns an enumerator that iterates through the collection of grid nodes
/// within the current grid structure.
/// </summary>
/// <returns>An enumerator for iterating through the <see cref="GridNode"/> elements in the grid.</returns>
public IEnumerator<GridNode> GetEnumerator() => Nodes.GetEnumerator();
/// <summary>
/// Returns an enumerator that iterates through the collection of grid nodes
/// within the current grid structure.
/// </summary>
/// <returns>An enumerator for iterating through the <see cref="GridNode"/> elements in the grid.</returns>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
+19
View File
@@ -0,0 +1,19 @@
using IgdrasilEngine.Engine.Math.Vectors;
namespace GridCasting.Models.Grid;
public class GridNode(FVector2 position)
{
/// <summary>
/// Represents the position of the GridNode in a two-dimensional space.
/// This position is defined using an instance of the FVector2 struct,
/// which encapsulates the X and Y coordinates as floating-point values.
/// </summary>
public FVector2 Position { get; } = position;
/// <summary>
/// Represents a collection of neighboring GridNode instances that are connected to the current GridNode.
/// The connections define the relationships or pathways between this node and others in the grid structure.
/// </summary>
public List<GridNode?> Connections { get; } = [];
}
+48
View File
@@ -0,0 +1,48 @@
using System.Collections;
namespace GridCasting.Models.GridGraph;
/// <summary>
/// Represents a graph structure designed for grid-based systems. The GridGraph class serves as a foundational component for
/// creating, managing, and transforming grids, paths, and related operations in systems that require spatial organization.
/// </summary>
public class GridGraph : IEnumerable<GridGraphNode>
{
/// <summary>
/// Represents the collection of grid nodes within the grid graph.
/// Each node symbolizes a distinct element or point in the graph.
/// This list enables navigation and structural definition of the graph.
/// </summary>
public List<GridGraphNode> Nodes { get; } = [];
/// <summary>
/// Provides an indexer to access a specific grid graph node from the collection
/// by its index. The indexer allows direct retrieval of a node based on its
/// position in the node list.
/// </summary>
/// <param name="index">The zero-based index of the node to retrieve.</param>
/// <returns>The grid graph node at the specified index in the collection.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when the specified index is outside the bounds of the node collection.
/// </exception>
public GridGraphNode this[int index] => Nodes[index];
/// <summary>
/// Returns an enumerator that iterates through the collection of nodes
/// in the grid graph.
/// </summary>
/// <returns>
/// An enumerator that can be used to iterate through the nodes of the grid graph.
/// </returns>
public IEnumerator<GridGraphNode> GetEnumerator() => Nodes.GetEnumerator();
/// <summary>
/// Returns an enumerator that iterates through the collection of nodes
/// in the grid graph.
/// </summary>
/// <returns>
/// An enumerator that can be used to iterate through the nodes of the grid graph.
/// </returns>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
@@ -0,0 +1,27 @@
namespace GridCasting.Models.GridGraph;
/// <summary>
/// Represents an edge in a grid system, connecting two nodes and including metadata about its geometry.
/// </summary>
public class GridGraphEdge(GridGraphNode nodeA, GridGraphNode nodeB, float angle, float length)
{
/// <summary>
/// Represents the starting node of the grid edge.
/// </summary>
public GridGraphNode NodeA { get; } = nodeA;
/// <summary>
/// Represents the ending node of the grid edge.
/// </summary>
public GridGraphNode NodeB { get; } = nodeB;
/// <summary>
/// Represents the angle of the grid edge in degrees.
/// </summary>
public float Angle { get; } = angle;
/// <summary>
/// Represents the length of the grid edge.
/// </summary>
public float Length { get; } = length;
}
@@ -0,0 +1,15 @@
namespace GridCasting.Models.GridGraph;
/// <summary>
/// Represents a node in a grid system. Each node may be connected to other nodes
/// in the grid via grid edges, which define relationships and structural topology.
/// </summary>
public class GridGraphNode
{
/// <summary>
/// A collection of edges connected to the grid node, representing its relationships
/// with adjacent nodes. Each edge contains information about the connected nodes
/// and additional metadata such as length and angle.
/// </summary>
public List<GridGraphEdge> Edges { get; } = [];
}
+39
View File
@@ -0,0 +1,39 @@
using System.Collections;
namespace GridCasting.Models;
/// <summary>
/// Represents a path structure consisting of a start node and a series of directions.
/// The path is iterable, allowing iteration over the start node followed by the direction sequence.
/// </summary>
public struct Path(int startNode, int[] directions) : IEnumerable<int>
{
/// <summary>
/// Gets or sets the starting node of the path. This represents the initial point
/// in the sequence before any directions are taken.
/// </summary>
public int StartNode { get; } = startNode;
/// <summary>
/// Gets or sets the sequence of directions within the path. Each direction
/// represents a step or movement from the initial starting node.
/// </summary>
public int[] Directions { get; } = directions;
/// <summary>
/// Returns an enumerator that iterates through the path, starting with the StartNode followed by the sequence of Directions.
/// </summary>
/// <returns>An enumerator that iterates through the elements of the path.</returns>
public IEnumerator<int> GetEnumerator()
{
yield return StartNode;
foreach (var direction in Directions)
yield return direction;
}
/// <summary>
/// Returns an enumerator that iterates through the path, starting with the StartNode followed by the sequence of Directions.
/// </summary>
/// <returns>An enumerator that iterates through the elements of the path.</returns>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
+360
View File
@@ -0,0 +1,360 @@
using GridCasting.Models.Grid;
using GridCasting.Models.GridGraph;
using GridCasting.Utils.BVH;
using IgdrasilEngine.Engine.Math.Boxes;
using IgdrasilEngine.Engine.Math.Vectors;
using IgdrasilEngine.Engine.Utils;
using Path = GridCasting.Models.Path;
namespace GridCasting.Transform;
/// <summary>
/// Provides functionality for resolving, validating, and manipulating grid-based structures,
/// as well as transforming those structures into paths or other representations.
/// </summary>
/// <remarks>
/// The <c>GridResolver</c> class enables operations on grid graphs by integrating a set of transforms and ensuring
/// that grid structures are consistent and functional.
/// </remarks>
public class GridResolver
{
/// <summary>
/// Represents the underlying grid graph structure used by the <c>GridResolver</c> for processing grid-related sequences and operations.
/// </summary>
/// <remarks>
/// The <c>_graph</c> field is an instance of <see cref="GridGraph"/>, which serves as the core data structure to store grid nodes and their relationships.
/// It is used for graph traversal, pathfinding, range-based queries, and various graph-related manipulations within the <c>GridResolver</c> class.
/// </remarks>
private readonly GridGraph _graph;
/// <summary>
/// Defines the sensitivity threshold for operations within the <c>GridResolver</c>, impacting the resolution or precision
/// of grid-based calculations, transformations, and spatial queries.
/// </summary>
/// <remarks>
/// The <c>_sensitivity</c> field is primarily used to control tolerance levels in various methods, such as determining
/// proximity, distance checks, and bounding region adjustments. It directly affects the accuracy and granularity of
/// operations such as pathfinding and spatial queries within grid structures handled by the <c>GridResolver</c>.
/// </remarks>
private readonly float _sensitivity;
/// <summary>
/// Represents a 2D Bounding Volume Hierarchy (BVH) structure for efficiently storing and querying grid graph points
/// used within the <c>GridResolver</c> class for spatial operations such as nearest-neighbor searches.
/// </summary>
/// <remarks>
/// The <c>_bvh</c> field is an instance of <see cref="PointBVH2D{T}"/>, specifically of type <see cref="GridGraphPoint"/>.
/// It facilitates spatial organization and query optimizations, enabling efficient handling of operations within the grid,
/// such as pathfinding, point lookups, and range-based searches.
/// This structure is dynamically built when the <c>GridResolver</c> is initialized based on the nodes of the associated grid graph.
/// </remarks>
private readonly PointBVH2D<GridGraphPoint> _bvh = new();
private readonly FBox2 _graphAABB;
/// <summary>
/// Provides functionality for resolving, validating, and manipulating grid-based structures,
/// as well as transforming those structures into paths or other representations.
/// </summary>
/// <remarks>
/// The <c>GridResolver</c> class enables operations on grid graphs by integrating a set of transforms and ensuring
/// that grid structures are consistent and functional.
/// </remarks>
public GridResolver(GridGraph graph, float sensitivity)
{
_graph = graph;
_sensitivity = sensitivity;
var aabb = new FBox2(FVector2.Zero, FVector2.Zero);
HashSet<GridGraphNode> completedNodes = [];
TraverseGraph(_graph,
point =>
{
aabb = FBox2.Union(aabb, point.TreePosition);
completedNodes.Add(point.Node);
_bvh.Add(point);
return point;
},
(_, to) => completedNodes.Contains(to.Node) ? TraversalResult.SkipNode : TraversalResult.EnqueueNode
);
_graphAABB = aabb;
}
/// <summary>
/// Creates a grid graph from a given grid structure, converting the grid's nodes into a graph format,
/// which enables advanced graph-based operations and transformations.
/// </summary>
/// <param name="graph">The source grid structure containing nodes to be converted into a graph representation.</param>
/// <returns>
/// A new instance of <c>GridGraph</c> representing the grid in a graph format if the conversion is successful;
/// otherwise, returns null if the grid cannot be converted.
/// </returns>
public static GridGraph? CreateGridGraph(Grid graph)
{
throw new NotImplementedException();
}
/// <summary>
/// Verifies the structural integrity of a given grid graph by ensuring that each node maintains
/// consistent positional relationships and no node is assigned multiple positions.
/// </summary>
/// <param name="graph">The grid graph to be verified.</param>
/// <returns>
/// A boolean value indicating whether the grid graph is valid. Returns true if the graph is structurally consistent,
/// and false if inconsistencies, such as multiple positions for the same node, are detected.
/// </returns>
public static bool VerifyGridGraph(GridGraph graph)
{
Dictionary<GridGraphNode, FVector2> nodes = new();
return TraverseGraph(
graph,
point =>
{
nodes.Add(point.Node, point.TreePosition);
return point;
},
(_, to) => nodes.TryGetValue(to.Node, out var existingPosition)
? FVector2.Distance(to.TreePosition, existingPosition) > 1e-5
? TraversalResult.AbortTraversal // Multiple positions for same node
: TraversalResult.SkipNode // Already traversed
: TraversalResult.EnqueueNode // New node
);
}
/// <summary>
/// Generates a grid structure within a specified range by traversing a grid graph.
/// It adds nodes and their corresponding connections based on the positions defined
/// in the graph and the constraints of the given range.
/// </summary>
/// <param name="range">The rectangular range within which the grid structure is generated.</param>
/// <returns>
/// A new <see cref="Grid"/> instance containing nodes and connections that exist within
/// the specified range in the grid graph.
/// </returns>
public Grid GenerateGrid(FBox2 range)
{
Dictionary<FVector2, GridNode> points = [];
Grid grid = new();
TraverseGraph(
_graph,
point => {
if (points.TryGetValue(point.TreePosition, out var node)) return node;
node = new GridNode(point.TreePosition);
grid.Nodes.Add(node);
return node;
},
(from, to) =>
{
if (!range.ContainsInclusive(to.TreePosition))
{
from.Connections.Add(null);
return TraversalResult.SkipNode;
}
if (points.TryGetValue(to.TreePosition, out var node))
{
from.Connections.Add(node);
return TraversalResult.SkipNode;
}
node = new GridNode(to.TreePosition);
points.Add(node.Position, node);
grid.Nodes.Add(node);
from.Connections.Add(node);
return TraversalResult.EnqueueNode;
}
);
return grid;
}
/// <summary>
/// Retrieves all grid positions from the specified range within the grid graph.
/// Ensures that only unique positions are included and limits results to the defined range.
/// </summary>
/// <param name="range">The bounding box that defines the area within which grid positions are collected.</param>
/// <returns>
/// An array of FVector2 objects representing the unique grid positions contained within the specified range.
/// </returns>
public FVector2[] GetGridPositions(FBox2 range)
{
HashSet<FVector2> points = [];
TraverseGraph(
_graph,
point => {
points.Add(point.TreePosition);
return point;
},
(_, to) => !range.ContainsInclusive(to.TreePosition) || points.Contains(to.TreePosition)
? TraversalResult.SkipNode
: TraversalResult.EnqueueNode
);
return points.ToArray();
}
/// <summary>
/// Generates a navigable path through a grid graph based on a sequence of positions.
/// Validates the possibility of connecting the provided positions within the grid graph.
/// </summary>
/// <param name="positions">An array of positional vectors specifying the targeted path in the grid graph.</param>
/// <returns>
/// An instance of the <c>Path</c> struct if a valid path exists connecting the given positions,
/// otherwise returns null if the path cannot be constructed due to inconsistencies or breaks.
/// </returns>
public Path? GetPath(FVector2[] positions)
{
var point = GetNearestPoint(positions[0]);
if (point == null) return null;
var directions = new int[positions.Length - 1];
var node = point.Node;
var position = point.TreePosition;
for (var i = 1; i < positions.Length; i++)
{
var updated = false;
for (var j = 0; j < node.Edges.Count; j++)
{
var newPos = position + new FVector2(
node.Edges[j].Length * MathF.Cos(node.Edges[j].Angle),
node.Edges[j].Length * MathF.Sin(node.Edges[j].Angle)
);
if (FVector2.Distance(positions[i], newPos) > _sensitivity) continue;
updated = true;
directions[i - 1] = j;
position = newPos;
node = node.Edges[j].NodeA == node ? node.Edges[j].NodeB : node.Edges[j].NodeA;
break;
}
if (!updated) return null; // Path doesn't exist
}
return new Path(_graph.IndexOf(node), directions);
}
private GridGraphPoint? GetNearestPoint(FVector2 position)
{
var points = _bvh.FindNearest(position, _sensitivity);
switch (points.Count)
{
case 1:
return points[0];
case > 1:
return null;
}
var completedPoints = new HashSet<GridGraphPoint>();
PriorityQueue<GridGraphPoint, float> queue = new();
var threshold = _graphAABB.Size.Length;
var min = new GridGraphPoint(new FVector2(0, 0), _graph.Nodes[0]);
var minDistance = FVector2.Distance(position, min.TreePosition);
queue.Enqueue(min, minDistance);
while (queue.TryDequeue(out var point, out var priority))
{
if (priority < _sensitivity) break;
if (!completedPoints.Add(point)) continue;
if (_bvh.FindNearest(point.TreePosition, 1e-5f).Count == 0) _bvh.Add(point);
if (priority < minDistance)
{
minDistance = priority;
min = point;
}
else if (priority > minDistance + threshold) break;
foreach (var edge in point.Node.Edges)
{
var nextNode = edge.NodeA == point.Node ? edge.NodeB : edge.NodeA;
var dir = new FVector2(
MathF.Cos(edge.Angle),
MathF.Sin(edge.Angle)
);
var nextPosition = point.TreePosition + edge.Length * dir;
var nextPoint = new GridGraphPoint(nextPosition, nextNode);
var dirToTarget = (position - point.TreePosition).Normalized;
if (FVector2.Dot(dirToTarget, dir) < -0.3f) continue;
queue.Enqueue(nextPoint, FVector2.Distance(nextPoint.TreePosition, position));
}
}
return minDistance < _sensitivity ? min : null;
}
/// <summary>
/// Traverses the specified grid graph using a breadth-first approach. The traversal process is controlled
/// through a combination of a point update function and a traversal callback to determine the next course of action
/// for each node being evaluated.
/// </summary>
/// <typeparam name="T">The type parameter determined by the result of the update function, which is forwarded to the traversal callback.</typeparam>
/// <param name="graph">The grid graph to traverse.</param>
/// <param name="update">A function that processes each grid point during traversal and returns a value of type <typeparamref name="T"/>.</param>
/// <param name="callback">
/// A function that decides the traversal behavior for each edge of the graph, using the result of the update function
/// and the next grid point being evaluated. The callback returns a <see cref="TraversalResult"/> to either enqueue the node, skip it, or abort the traversal.
/// </param>
/// <param name="start">The starting point of the traversal. If not provided, the first node in the graph is used.</param>
/// <returns>
/// A boolean value indicating the success of the traversal. Returns true if the entire graph was processed without
/// encountering a condition to abort traversal, and false otherwise.
/// </returns>
private static bool TraverseGraph<T>(GridGraph graph, Func<GridGraphPoint, T> update,
Func<T, GridGraphPoint, TraversalResult> callback, GridGraphPoint? start = null)
{
Queue<GridGraphPoint> queue = new();
queue.Enqueue(start ?? new GridGraphPoint(new FVector2(0, 0), graph.Nodes[0]));
while (queue.TryDequeue(out var point))
{
var pointRepresentation = update(point);
foreach (var edge in point.Node.Edges)
{
var nextNode = edge.NodeA == point.Node ? edge.NodeB : edge.NodeA;
var nextPosition = point.TreePosition + new FVector2(
edge.Length * MathF.Cos(edge.Angle),
edge.Length * MathF.Sin(edge.Angle)
);
var nextPoint = new GridGraphPoint(nextPosition, nextNode);
switch (callback(pointRepresentation, nextPoint))
{
case TraversalResult.AbortTraversal:
return false;
case TraversalResult.SkipNode:
break;
case TraversalResult.EnqueueNode:
default:
queue.Enqueue(nextPoint);
break;
}
}
}
return true;
}
/// <summary>
/// Represents the result of a traversal operation in a grid graph.
/// </summary>
private enum TraversalResult
{
EnqueueNode,
SkipNode,
AbortTraversal
}
/// <summary>
/// Represents a point in a grid graph, used within a 2D BVH (Bounding Volume Hierarchy) tree structure.
/// </summary>
/// <remarks>
/// The <c>GridGraphPoint</c> class is used to store and manage nodes of a grid graph, allowing for spatial operations
/// such as nearest neighbor searches in 2D space. It inherits from <c>PointBVH2DTransform</c>, enabling integration
/// with BVH for fast spatial queries.
/// </remarks>
private class GridGraphPoint(FVector2 position, GridGraphNode node) : PointBVH2DTransform<GridGraphPoint>
{
public override FVector2 TreePosition { get; protected set; } = position;
public GridGraphNode Node { get; } = node;
private static FVector2 Quantize(FVector2 p)
{
const float eps = 0.001f;
return new FVector2(
MathF.Round(p.X / eps) * eps,
MathF.Round(p.Y / eps) * eps
);
}
public override int GetHashCode() => HashCode.Combine(Node, Quantize(TreePosition));
}
}
+27
View File
@@ -0,0 +1,27 @@
using GridCasting.Models.Grid;
using GridCasting.Models.GridGraph;
using Path = GridCasting.Models.Path;
namespace GridCasting.Transform;
/// <summary>
/// Defines methods to transform and reverse-transform paths based on a grid graph.
/// </summary>
public interface IPathTransform
{
/// <summary>
/// Transforms the given path based on the specified grid graph.
/// </summary>
/// <param name="graph">The grid graph on which the transformation is based.</param>
/// <param name="path">The path to be transformed.</param>
/// <returns>A transformed path as per the grid graph rules.</returns>
public Path Transform(GridGraph graph, Path path);
/// <summary>
/// Reverses the transformation applied to the provided path based on the specified grid graph.
/// </summary>
/// <param name="graph">The grid graph used to reverse the transformation.</param>
/// <param name="path">The path to be reversed.</param>
/// <returns>A path that has the reverse transformation applied based on the grid graph rules.</returns>
public Path Reverse(GridGraph graph, Path path);
}
@@ -0,0 +1,35 @@
using IgdrasilEngine.Engine.Math.Boxes;
using IgdrasilEngine.Engine.Math.Vectors;
namespace GridCasting.Utils.BVH.Point;
/// <summary>
/// Интерфейс для чтения BVH дерева точек в 2D пространстве
/// </summary>
/// <typeparam name="T">Тип точек в дереве BVH.</typeparam>
public interface IReadOnlyPointBVH2D<T> where T : PointBVH2DTransform<T>
{
/// <summary>
/// Глубина BVH дерева.
/// </summary>
/// <returns>Глубина дерева.</returns>
public uint Depth();
/// <summary>
/// Находит все точки в пределах заданного радиуса от указанной позиции.
/// </summary>
/// <param name="position">Позиция для поиска ближайших точек.</param>
/// <param name="radius">Радиус поиска.</param>
/// <returns>Список точек, найденных в пределах радиуса.</returns>
public List<T> FindNearest(FVector2 position, float radius);
/// <summary>
/// Находит все точки в пределах заданного радиуса от указанной позиции и добавляет их в предоставленный список.
/// </summary>
/// <param name="position">Позиция для поиска ближайших точек.</param>
/// <param name="radius">Радиус поиска.</param>
/// <param name="result">Список для добавления найденных точек.</param>
public void FindNearest(FVector2 position, float radius, List<T> result);
/// <summary>
/// Получает граничный прямоугольник, охватывающий все точки в BVH дереве.
/// </summary>
/// <returns>Граничный прямоугольник.</returns>
public FBox2 GetBoundaryBox();
}
+702
View File
@@ -0,0 +1,702 @@
using GridCasting.Utils.BVH.Point;
using IgdrasilEngine.Engine.Math.Boxes;
using IgdrasilEngine.Engine.Math.Vectors;
namespace GridCasting.Utils.BVH;
/// <summary>
/// Дерево BVH для точек в 2D пространстве.
/// </summary>
/// <typeparam name="T">Тип точек в дереве BVH.</typeparam>
public class PointBVH2D<T> : IReadOnlyPointBVH2D<T> where T : PointBVH2DTransform<T>
{
/// <summary>
/// Стек для обхода дерева BVH.
/// </summary>
private readonly Stack<Node> _stack = new();
/// <summary>
/// Корневой узел BVH дерева.
/// </summary>
private readonly Branch _root;
/// <summary>
/// Инициализирует новый экземпляр дерева BVH для точек в 2D пространстве.
/// </summary>
public PointBVH2D()
{
_root = new Branch(null, default!);
_root.Root = _root;
}
/// <summary>
/// Глубина BVH дерева.
/// </summary>
/// <returns>Глубина дерева.</returns>
public uint Depth() => _root.Depth();
/// <summary>
/// Добавляет точку в BVH дерево.
/// </summary>
/// <param name="value">Точка для добавления.</param>
public void Add(T value)
{
lock (_root)
{
_root.Add(value);
}
}
/// <summary>
/// Оптимизированно добавляет точку в BVH дерево.
/// </summary>
/// <param name="value">Точка для добавления.</param>
public void OptimizedAdd(T value)
{
lock (_root)
{
_root.OptimizedAdd(value);
}
}
/// <summary>
/// Удаляет точку из BVH дерева.
/// </summary>
/// <param name="value">Точка для удаления.</param>
public void Remove(T value)
{
lock (_root)
{
_root.Remove(value);
}
}
/// <summary>
/// Находит все точки в пределах заданного радиуса от указанной позиции и добавляет их в предоставленный список.
/// </summary>
/// <param name="position">Позиция для поиска ближайших точек.</param>
/// <param name="radius">Радиус поиска.</param>
/// <param name="result">Список для добавления найденных точек.</param>
public void FindNearest(FVector2 position, float radius, List<T> result)
{
lock (_root)
{
_root.FindNearestFwd(position, radius, result);
}
}
/// <summary>
/// Находит все точки в пределах заданного радиуса от указанной позиции.
/// </summary>
/// <param name="position">Позиция для поиска ближайших точек.</param>
/// <param name="radius">Радиус поиска.</param>
/// <returns>Список найденных точек.</returns>
public List<T> FindNearest(FVector2 position, float radius)
{
var result = new List<T>();
FindNearest(position, radius, result);
return result;
}
/// <summary>
/// Очищает BVH дерево, удаляя все точки.
/// </summary>
public void Clear()
{
lock (_root)
{
_root.Left = null;
_root.Right = null;
_root.AABB = new FBox2(FVector2.Zero, FVector2.Zero);
}
}
/// <summary>
/// Получает граничный прямоугольник, охватывающий все точки в BVH дереве.
/// </summary>
/// <returns>Граничный прямоугольник.</returns>
public FBox2 GetBoundaryBox() => _root.AABB;
/// <summary>
/// Базовый класс для узлов BVH дерева.
/// </summary>
public abstract class Node
{
/// <summary>
/// Корень BVH дерева.
/// </summary>
protected internal Branch Root;
/// <summary>
/// Родительский узел BVH дерева.
/// </summary>
protected internal Branch? Parent;
/// <summary>
/// Ограничивающий прямоугольник узла BVH дерева.
/// </summary>
protected internal FBox2 AABB;
/// <summary>
/// Инициализирует новый экземпляр узла BVH дерева.
/// </summary>
/// <param name="aabb">Ограничивающий прямоугольник узла.</param>
/// <param name="parent">Родительский узел.</param>
/// <param name="root">Корень дерева.</param>
protected Node(FBox2 aabb, Branch? parent, Branch root)
{
AABB = aabb;
Parent = parent;
Root = root;
}
/// <summary>
/// Глубина BVH дерева.
/// </summary>
/// <returns>Глубина дерева.</returns>
public abstract uint Depth();
/// <summary>
/// Добавляет точку в BVH дерево.
/// </summary>
/// <param name="value">Точка для добавления.</param>
public abstract void Add(T value);
/// <summary>
/// Добавляет точку в BVH дерево.
/// </summary>
/// <param name="value">Точка для добавления.</param>
/// <param name="stack">Стек узлов для оптимизации добавления.</param>
public abstract void Add(T value, Stack<Node> stack);
/// <summary>
/// Оптимизированно добавляет точку в BVH дерево.
/// </summary>
/// <param name="value">Точка для добавления.</param>
public abstract void OptimizedAdd(T value);
/// <summary>
/// Оптимизированно добавляет точку в BVH дерево.
/// </summary>
/// <param name="value">Точка для добавления.</param>
/// <param name="stack">Стек узлов для оптимизации добавления.</param>
public abstract void OptimizedAdd(T value, Stack<Node> stack);
/// <summary>
/// Удаляет точку из BVH дерева.
/// </summary>
/// <param name="value">Точка для удаления.</param>
public abstract void Remove(T value);
/// <summary>
/// Удаляет точку из BVH дерева.
/// </summary>
/// <param name="value">Точка для удаления.</param>
/// <param name="stack">Стек узлов для оптимизации удаления.</param>
public abstract void Remove(T value, Stack<Node> stack);
/// <summary>
/// Находит все точки в пределах заданного радиуса от указанной позиции.
/// </summary>
/// <param name="position">Позиция для поиска ближайших точек.</param>
/// <param name="radius">Радиус поиска.</param>
/// <param name="result">Список для хранения найденных точек.</param>
public abstract void FindNearestFwd(FVector2 position, float radius, List<T> result);
/// <summary>
/// Находит все точки в пределах заданного радиуса от указанной позиции.
/// </summary>
/// <param name="position">Позиция для поиска ближайших точек.</param>
/// <param name="radius">Радиус поиска.</param>
/// <param name="result">Список для хранения найденных точек.</param>
/// <param name="stack">Стек узлов для оптимизации поиска.</param>
public abstract void FindNearestFwd(FVector2 position, float radius, List<T> result, Stack<Node> stack);
/// <summary>
/// Находит все точки в пределах заданного радиуса от указанной позиции, обходя дерево вверх.
/// </summary>
/// <param name="position">Позиция для поиска ближайших точек.</param>
/// <param name="radius">Радиус поиска.</param>
/// <param name="result">Список для хранения найденных точек.</param>
public void FindNearestBwd(FVector2 position, float radius, List<T> result)
{
lock (Root)
{
if (Parent == null) return;
if (Parent.Left == this)
Parent.Right?.FindNearestFwd(position, radius, result);
else
Parent.Left?.FindNearestFwd(position, radius, result);
Parent.FindNearestBwd(position, radius, result);
}
}
/// <summary>
/// Удаляет текущий узел из BVH дерева.
/// </summary>
protected void RemoveCurrentNode()
{
if (Parent == null) return;
if (Parent.Left == this)
{
Parent.Left = null;
if (Parent.Right == null)
Parent.RemoveCurrentNode();
else
Parent.Replace(Parent.Right);
}
else
{
Parent.Right = null;
if (Parent.Left == null)
Parent.RemoveCurrentNode();
else
Parent.Replace(Parent.Left);
}
}
/// <summary>
/// Заменяет текущий узел на указанный узел в BVH дереве
/// </summary>
/// <param name="node">Узел для замены.</param>
protected void Replace(Node node)
{
if (Parent == null) return;
if (Parent.Left == this)
Parent.Left = node;
else
Parent.Right = node;
node.Parent = Parent;
Parent.UpdateAABB();
}
}
/// <summary>
/// Ветвь BVH дерева.
/// </summary>
public class Branch : Node
{
/// <summary>
/// Левый дочерний узел ветви BVH дерева.
/// </summary>
protected internal Node? Left;
/// <summary>
/// Правый дочерний узел ветви BVH дерева.
/// </summary>
protected internal Node? Right;
/// <summary>
/// Инициализирует новый экземпляр ветви BVH дерева.
/// </summary>
/// <param name="parent">Родительская ветвь.</param>
/// <param name="root">Корневая ветвь.</param>
public Branch(Branch? parent, Branch root) : base(new FBox2(FVector2.Zero, FVector2.Zero), parent, root)
{
}
/// <summary>
/// Глубина BVH дерева.
/// </summary>
/// <returns>Глубина дерева.</returns>
public override uint Depth()
{
var left = Left?.Depth() ?? 0;
var right = Right?.Depth() ?? 0;
return System.Math.Max(left, right) + 1;
}
/// <summary>
/// Добавляет точку в BVH дерево.
/// </summary>
/// <param name="value">Точка для добавления.</param>
public override void Add(T value)
{
var left = Left == null ? 0 : FBox2.Distance(Left.AABB, value.TreePosition);
var right = Right == null ? 0 : FBox2.Distance(Right.AABB, value.TreePosition);
if (left < right)
{
if (Left == null)
Left = new Leaf(this, Root, value);
else Left.Add(value);
}
else
{
if (Right == null)
Right = new Leaf(this, Root, value);
else Right.Add(value);
}
UpdateAABB();
}
/// <summary>
/// Добавляет точку в BVH дерево.
/// </summary>
/// <param name="value">Точка для добавления.</param>
/// <param name="stack">Стек узлов для обхода.</param>
public override void Add(T value, Stack<Node> stack)
{
var left = Left == null ? 0 : FBox2.Distance(Left.AABB, value.TreePosition);
var right = Right == null ? 0 : FBox2.Distance(Right.AABB, value.TreePosition);
if (left < right)
{
if (Left == null)
Left = new Leaf(this, Root, value);
else stack.Push(Left);
}
else
{
if (Right == null)
Right = new Leaf(this, Root, value);
else stack.Push(Right);
}
}
/// <summary>
/// Добавляет точку в BVH дерево.
/// </summary>
/// <param name="value">Точка для добавления.</param>
public override void OptimizedAdd(T value)
{
var left = Left == null ? 0 : FBox2.Distance(Left.AABB, value.TreePosition);
var right = Right == null ? 0 : FBox2.Distance(Right.AABB, value.TreePosition);
if (left < right)
{
if (Left == null)
{
Left = new Leaf(this, Root, value);
Balance();
UpdateAABB();
}
else if (left <= 0)
Left.OptimizedAdd(value);
else
{
var node = new Branch(this, Root);
node.Left = new Leaf(node, Root, value);
node.Right = Left;
Left.Parent = node;
Left = node;
node.Balance();
node.UpdateAABB();
}
}
else
{
if (Right == null)
{
Right = new Leaf(this, Root, value);
Balance();
UpdateAABB();
}
else if (right <= 0)
Right.OptimizedAdd(value);
else
{
var node = new Branch(this, Root);
node.Left = new Leaf(node, Root, value);
node.Right = Right;
Right.Parent = node;
Right = node;
node.Balance();
node.UpdateAABB();
}
}
UpdateAABB();
}
/// <summary>
/// Добавляет точку в BVH дерево.
/// </summary>
/// <param name="value">Точка для добавления.</param>
/// <param name="stack">Стек узлов для обхода.</param>
public override void OptimizedAdd(T value, Stack<Node> stack)
{
var left = Left == null ? 0 : FBox2.Distance(Left.AABB, value.TreePosition);
var right = Right == null ? 0 : FBox2.Distance(Right.AABB, value.TreePosition);
if (left < right)
{
if (Left == null)
Left = new Leaf(this, Root, value);
else if (left <= 0)
stack.Push(Left);
else
{
var node = new Branch(Left.Parent, Root);
node.Left = new Leaf(node, Root, value);
node.Right = Left;
Left.Parent = node;
Left = node;
node.Balance();
node.UpdateAABB();
}
}
else
{
if (Right == null)
Right = new Leaf(this, Root, value);
else if (right <= 0)
stack.Push(Right);
else
{
var node = new Branch(Right.Parent, Root);
node.Left = new Leaf(node, Root, value);
node.Right = Right;
Right.Parent = node;
Right = node;
node.Balance();
node.UpdateAABB();
}
}
}
/// <summary>
/// Балансирует BVH дерево, удаляя пустые узлы.
/// </summary>
private void Balance() => RemoveHoles();
/// <summary>
/// Удаляет пустые узлы из BVH дерева.
/// </summary>
private void RemoveHoles()
{
if (Left == null)
{
if (Right == null)
RemoveCurrentNode();
else
{
if (Right is Branch br)
br.RemoveHoles();
Replace(Right);
}
return;
}
if (Right == null)
{
if (Left == null)
RemoveCurrentNode();
else
{
if (Left is Branch br)
br.RemoveHoles();
Replace(Left);
}
return;
}
}
/// <summary>
/// Удаляет точку из BVH дерева.
/// </summary>
/// <param name="value">Точка для удаления.</param>
public override void Remove(T value)
{
Left?.Remove(value);
Right?.Remove(value);
if (Left == null && Right == null)
RemoveCurrentNode();
else UpdateAABB();
}
/// <summary>
/// Удаляет точку из BVH дерева.
/// </summary>
/// <param name="value">Точка для удаления.</param>
/// <param name="stack">Стек узлов для обхода.</param>
public override void Remove(T value, Stack<Node> stack)
{
if (Left != null) stack.Push(Left);
if (Right != null) stack.Push(Right);
}
/// <summary>
/// Находит все точки в пределах заданного радиуса от указанной позиции.
/// </summary>
/// <param name="position">Позиция центра поиска.</param>
/// <param name="radius">Радиус поиска.</param>
/// <param name="result">Список для хранения найденных точек.</param>
public override void FindNearestFwd(FVector2 position, float radius, List<T> result)
{
if (!FBox2.SphereIntersection(AABB, position, radius)) return;
Left?.FindNearestFwd(position, radius, result);
Right?.FindNearestFwd(position, radius, result);
}
/// <summary>
/// Находит все точки в пределах заданного радиуса от указанной позиции.
/// </summary>
/// <param name="position">Позиция центра поиска.</param>
/// <param name="radius">Радиус поиска.</param>
/// <param name="result">Список для хранения найденных точек.</param>
/// <param name="stack">Стек узлов для обхода.</param>
public override void FindNearestFwd(FVector2 position, float radius, List<T> result, Stack<Node> stack)
{
if (!FBox2.SphereIntersection(AABB, position, radius)) return;
if (Left != null) stack.Push(Left);
if (Right != null) stack.Push(Right);
}
/// <summary>
/// Обновляет ограничивающий прямоугольник узла BVH дерева.
/// </summary>
protected internal void UpdateAABB()
{
if (Left == null && Right == null) return;
var left = Left?.AABB ?? Right!.AABB;
var right = Right?.AABB ?? Left!.AABB;
AABB = FBox2.Union(left, right);
}
/// <summary>
/// Перемещает точку в BVH дереве.
/// </summary>
/// <param name="value">Точка для перемещения.</param>
public void Relocate(T value)
{
if (Parent == null)
{
OptimizedAdd(value);
return;
}
if (Left == null && Right == null)
RemoveCurrentNode();
else Parent.UpdateAABB();
if (Parent.AABB.ContainsInclusive(value.TreePosition))
{
Parent.OptimizedAdd(value);
return;
}
Parent.Relocate(value);
}
}
/// <summary>
/// Лист BVH дерева.
/// </summary>
public class Leaf : Node
{
/// <summary>
/// Значение точки, хранящейся в листе BVH дерева.
/// </summary>
private readonly T _value;
/// <summary>
/// Инициализирует новый экземпляр листа BVH дерева.
/// </summary>
/// <param name="parent">Родительский узел.</param>
/// <param name="root">Корень дерева.</param>
/// <param name="value">Значение точки.</param>
public Leaf(Branch? parent, Branch root, T value) : base(new FBox2(value.TreePosition, value.TreePosition),
parent, root)
{
_value = value;
_value.Location = this;
}
/// <summary>
/// Глубина BVH дерева.
/// </summary>
/// <returns>Глубина дерева.</returns>
public override uint Depth() => 1;
/// <summary>
/// Добавляет точку в BVH дерево.
/// </summary>
/// <param name="value">Точка для добавления.</param>
public override void Add(T value) => OptimizedAdd(value);
/// <summary>
/// Добавляет точку в BVH дерево.
/// </summary>
/// <param name="value">Точка для добавления.</param>
/// <param name="stack">Стек узлов.</param>
public override void Add(T value, Stack<Node> stack) => OptimizedAdd(value);
/// <summary>
/// Перемещает точку в BVH дереве.
/// </summary>
public void Relocate()
{
lock (Root)
{
// if (Parent == null)
// {
// OptimizedAdd(_value);
// return;
// }
Root.Remove(_value);
Root.OptimizedAdd(_value);
// Parent.UpdateAABB();
//
// if (Parent.AABB.ContainsInclusive(_value.TreePosition))
// {
// Parent.OptimizedAdd(_value);
// return;
// }
// Parent.Relocate(_value);
}
}
/// <summary>
/// Оптимизированно добавляет точку в BVH дерево.
/// </summary>
/// <param name="value">Точка для добавления.</param>
public override void OptimizedAdd(T value)
{
if (Parent == null) return;
if (Parent.Left == this)
{
var node = new Branch(Parent, Root);
node.Left = new Leaf(node, Root, value);
node.Right = this;
Parent.Left = node;
Parent = node;
node.UpdateAABB();
}
else
{
var node = new Branch(Parent, Root);
node.Left = new Leaf(node, Root, value);
node.Right = this;
Parent.Right = node;
Parent = node;
node.UpdateAABB();
}
}
/// <summary>
/// Оптимизированно добавляет точку в BVH дерево.
/// </summary>
/// <param name="value">Точка для добавления.</param>
/// <param name="stack">Стек узлов.</param>
public override void OptimizedAdd(T value, Stack<Node> stack) => OptimizedAdd(value);
/// <summary>
/// Удаляет точку из BVH дерева.
/// </summary>
/// <param name="value">Точка для удаления.</param>
public override void Remove(T value)
{
if (!_value.Equals(value)) return;
RemoveCurrentNode();
}
/// <summary>
/// Удаляет точку из BVH дерева.
/// </summary>
/// <param name="value">Точка для удаления.</param>
/// <param name="stack">Стек узлов.</param>
public override void Remove(T value, Stack<Node> stack) => Remove(value);
/// <summary>
/// Находит все точки в пределах заданного радиуса от указанной позиции.
/// </summary>
/// <param name="position">Позиция для поиска.</param>
/// <param name="radius">Радиус поиска.</param>
/// <param name="result">Список для хранения найденных точек.</param>
public override void FindNearestFwd(FVector2 position, float radius, List<T> result)
{
if (FVector2.Distance(position, _value.TreePosition) > radius) return;
result.Add(_value);
}
/// <summary>
/// Находит все точки в пределах заданного радиуса от указанной позиции.
/// </summary>
/// <param name="position">Позиция для поиска.</param>
/// <param name="radius">Радиус поиска.</param>
/// <param name="result">Список для хранения найденных точек.</param>
/// <param name="stack">Стек узлов.</param>
public override void FindNearestFwd(FVector2 position, float radius, List<T> result, Stack<Node> stack) => FindNearestFwd(position, radius, result);
}
}
@@ -0,0 +1,39 @@
using GridCasting.Utils.BVH.Point;
using IgdrasilEngine.Engine.Math.Vectors;
namespace GridCasting.Utils.BVH;
/// <summary>
/// Базовый класс для точек, хранящихся в BVH дереве в 2D пространстве.
/// </summary>
/// <typeparam name="T">Тип точек в дереве BVH.</typeparam>
public abstract class PointBVH2DTransform<T> where T : PointBVH2DTransform<T>
{
/// <summary>
/// Позиция точки в дереве BVH.
/// </summary>
public abstract FVector2 TreePosition { get; protected set; }
/// <summary>
/// Лист BVH дерева, в котором находится эта точка.
/// </summary>
public PointBVH2D<T>.Leaf? Location { get; protected internal set; }
/// <summary>
/// Находит все точки в пределах заданного радиуса от позиции этой точки и добавляет их в предоставленный список.
/// </summary>
/// <param name="radius">Радиус поиска.</param>
/// <param name="result">Список для добавления найденных точек.</param>
public void FindNearest(float radius, ref List<T> result) => Location?.FindNearestBwd(TreePosition, radius, result);
/// <summary>
/// Находит все точки в пределах заданного радиуса от позиции этой точки.
/// </summary>
/// <param name="radius">Радиус поиска.</param>
/// <returns>Список найденных точек.</returns>
public List<T> FindNearest(float radius)
{
var result = new List<T>();
FindNearest(radius, ref result);
return result;
}
}
+178
View File
@@ -0,0 +1,178 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
namespace GridCasting.Utils;
/// <summary>
/// ListenableDictionary is a wrapper around a standard dictionary that extends its functionality
/// by providing event-based notifications for certain operations such as adding, removing, or updating entries.
/// </summary>
/// <typeparam name="TKey">The type of keys maintained in the dictionary.</typeparam>
/// <typeparam name="TValue">The type of values maintained in the dictionary.</typeparam>
public class ListenableDictionary<TKey, TValue>(IDictionary<TKey, TValue> baseDictionary) : IDictionary<TKey, TValue>
{
/// <summary>
/// Represents an event triggered when the <see cref="ListenableDictionary{TKey, TValue}"/> is cleared using the <c>Clear</c> method.
/// Subscribing to this event allows monitoring actions where all dictionary entries are removed at once.
/// </summary>
public event Action? OnClear;
/// <summary>
/// Represents an event triggered when an entry is removed from the <see cref="ListenableDictionary{TKey, TValue}"/>
/// using the <c>Remove</c> method. Subscribing to this event allows monitoring the removal of specific keys from the dictionary.
/// </summary>
public event Action<TKey>? OnRemove;
/// <summary>
/// Represents an event triggered whenever an entry in the <see cref="ListenableDictionary{TKey, TValue}"/> is updated or replaced.
/// Subscribing to this event allows monitoring changes to existing key-value pairs within the dictionary,
/// providing the updated key and corresponding value.
/// </summary>
public event Action<TKey, TValue>? OnUpdate;
/// <summary>
/// Returns an enumerator that iterates through the ListenableDictionary.
/// </summary>
/// <returns>
/// An enumerator for the entries in the dictionary.
/// </returns>
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => baseDictionary.GetEnumerator();
/// <summary>
/// Returns an enumerator that iterates through the ListenableDictionary as a non-generic collection.
/// </summary>
/// <returns>
/// An enumerator for the entries in the dictionary as a non-generic IEnumerable.
/// </returns>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary>
/// Removes all entries from the ListenableDictionary.
/// </summary>
/// <remarks>
/// This operation clears the underlying dictionary and triggers the <see cref="OnClear"/> event if any subscribers are registered.
/// </remarks>
public void Clear()
{
baseDictionary.Clear();
OnClear?.Invoke();
}
/// <summary>
/// Gets the total number of key-value pairs contained within the <see cref="ListenableDictionary{TKey, TValue}"/>.
/// </summary>
/// <value>
/// An integer representing the count of elements currently stored in the dictionary.
/// </value>
/// <remarks>
/// This property retrieves the count directly from the underlying base dictionary.
/// The count updates dynamically as items are added or removed.
/// </remarks>
public int Count => baseDictionary.Count;
/// <summary>
/// Indicates whether the <see cref="ListenableDictionary{TKey, TValue}"/> is read-only.
/// </summary>
/// <remarks>
/// A read-only dictionary does not allow adding, removing, or modifying its elements.
/// This property reflects the underlying dictionary's read-only status.
/// </remarks>
public bool IsReadOnly => baseDictionary.IsReadOnly;
/// <summary>
/// Adds the specified key and value to the ListenableDictionary.
/// </summary>
/// <param name="key">The key of the element to add to the dictionary.</param>
/// <param name="value">The value of the element to add to the dictionary.</param>
public void Add(TKey key, TValue value)
{
baseDictionary.Add(key, value);
OnUpdate?.Invoke(key, value);
}
/// <summary>
/// Determines whether the ListenableDictionary contains the specified key.
/// </summary>
/// <param name="key">The key to locate in the dictionary.</param>
/// <returns>
/// true if the ListenableDictionary contains an element with the specified key; otherwise, false.
/// </returns>
public bool ContainsKey(TKey key) => baseDictionary.ContainsKey(key);
/// <summary>
/// Removes the value with the specified key from the ListenableDictionary.
/// </summary>
/// <param name="key">The key of the element to remove from the dictionary.</param>
/// <returns>
/// true if the element is successfully removed; otherwise, false.
/// This method also returns false if the key was not found in the dictionary.
/// </returns>
public bool Remove(TKey key)
{
var result = baseDictionary.Remove(key);
if (result) OnRemove?.Invoke(key);
return result;
}
/// <summary>
/// Attempts to get the value associated with the specified key from the ListenableDictionary.
/// </summary>
/// <param name="key">The key whose value to retrieve.</param>
/// <param name="value">When this method returns, contains the value associated with the specified key,
/// if the key is found; otherwise, the default value for the type of the value parameter.</param>
/// <returns>
/// <c>true</c> if the ListenableDictionary contains an element with the specified key; otherwise, <c>false</c>.
/// </returns>
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) =>
baseDictionary.TryGetValue(key, out value);
/// <summary>
/// Gets or sets the value associated with the specified key in the ListenableDictionary.
/// </summary>
/// <param name="key">The key of the value to get or set.</param>
/// <value>
/// The value associated with the specified key. If the key does not exist, a get operation throws a KeyNotFoundException.
/// A set operation will update the value and trigger the <see cref="OnUpdate"/> event.
/// </value>
/// <exception cref="KeyNotFoundException">Thrown when attempting to get a value for a key that does not exist.</exception>
/// <remarks>
/// Setting a value will replace the existing value if the key is already present, and notify subscribers via the <see cref="OnUpdate"/> event.
/// </remarks>
public TValue this[TKey key]
{
get => baseDictionary[key];
set
{
baseDictionary[key] = value;
OnUpdate?.Invoke(key, value);
}
}
/// <summary>
/// Gets a collection containing the keys in the <see cref="ListenableDictionary{TKey, TValue}"/>.
/// </summary>
/// <remarks>
/// The returned collection reflects the current state of the dictionary and provides a way to iterate through all the keys.
/// </remarks>
public ICollection<TKey> Keys => baseDictionary.Keys;
/// <summary>
/// Gets a collection containing the values in the <see cref="ListenableDictionary{TKey, TValue}"/>.
/// </summary>
/// <remarks>
/// The returned collection directly reflects the values present in the underlying dictionary at the time of access.
/// Changes to the ListenableDictionary will be reflected in this collection.
/// </remarks>
public ICollection<TValue> Values => baseDictionary.Values;
void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item) => Add(item.Key, item.Value);
bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> item) => baseDictionary.Contains(item);
void ICollection<KeyValuePair<TKey, TValue>>.CopyTo(KeyValuePair<TKey, TValue>[] array, int index) => baseDictionary.CopyTo(array, index);
bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
{
if (!baseDictionary.TryGetValue(item.Key, out var value)) return false;
if (!EqualityComparer<TValue>.Default.Equals(value, item.Value)) return false;
Remove(item.Key);
return true;
}
}
+133
View File
@@ -0,0 +1,133 @@
using System.Diagnostics.CodeAnalysis;
namespace GridCasting.Utils;
/// <summary>
/// Represents a generic prefix tree (Trie) data structure, which maps keys
/// composed of sequences of <typeparamref name="TKey"/> into values of <typeparamref name="TValue"/>.
/// This data structure allows hierarchical storage and efficient retrieval of values associated
/// with a sequence (path) of keys.
/// </summary>
/// <typeparam name="TKey">
/// Specifies the type of the key elements that make up the paths. Must be a non-nullable type.
/// </typeparam>
/// <typeparam name="TValue">
/// Specifies the type of the values that are stored in the trie.
/// </typeparam>
/// <remarks>
/// - The trie supports key sequences of arbitrary length.
/// - Child nodes are stored internally in a dictionary for efficient access.
/// - Enables operations such as setting, retrieving, and removing values at specific paths.
/// - Implements lazy creation for child nodes during insertion.
/// - Allows for weak leaf and overwrite behavior during the modification of its structure.
/// </remarks>
public class Trie<TKey, TValue> where TKey : notnull
{
/// <summary>
/// Represents the stored value of the current node in the trie. This value
/// is assigned when a key-path is associated with a specific value in the trie.
/// It can be null if no value has been set for this node.
/// </summary>
private TValue? _value;
/// <summary>
/// Indicates whether the current node in the trie serves as a "weak leaf," which means
/// it is treated as a terminus for key-path lookup but may represent a non-terminal node in the trie structure.
/// When this flag is set to true, it allows the trie to consider this node as a valid endpoint for key-path retrieval, even if it has child nodes.
/// This is useful for scenarios where certain paths should be considered complete and retrievable, regardless of whether they have further branches in the trie.
/// </summary>
private bool _isWeakLeaf;
/// <summary>
/// Represents the collection of child nodes for the current node in the trie.
/// Each child node is associated with a key and serves as the next level in the trie structure.
/// This dictionary enables the hierarchical representation of key-paths.
/// </summary>
private readonly Dictionary<TKey, Trie<TKey, TValue>> _children = new();
/// <summary>
/// Provides access to the value associated with a specific key path in the trie.
/// If the key path exists, the corresponding value is returned; otherwise, a
/// KeyNotFoundException is thrown. The indexer facilitates value retrieval
/// using a sequence of keys, aligning with trie-based key-path structures.
/// </summary>
/// <param name="path">An array of keys representing the path to a value in the trie.</param>
/// <returns>The value associated with the specified key path.</returns>
/// <exception cref="KeyNotFoundException">
/// Thrown if the specified key path does not exist in the trie.
/// </exception>
public TValue this[TKey[] path] => TryGetValue(path, out var value)
? value
: throw new KeyNotFoundException($"The given path was not present in the trie: [{string.Join(", ", path)}]");
/// <summary>
/// Sets a value in the trie at the specified key path. If the key path does not exist, it will be created.
/// </summary>
/// <param name="path">The key path where the value should be set. Each element in the array represents a level in the trie.</param>
/// <param name="weakLeaf">Indicates whether the node at the specified key path should be marked as a weak leaf.</param>
/// <param name="value">The value to set at the specified key path.</param>
/// <param name="rewrite">Indicates whether the value should be overwritten if a value already exists at the specified key path. Defaults to true.</param>
/// <returns>Returns true if the value was successfully set, otherwise false.</returns>
public bool Set(TKey[] path, bool weakLeaf, TValue value, bool rewrite = true)
{
if (path.Length == 0)
{
if (rewrite || _value == null)
{
_value = value;
_isWeakLeaf = weakLeaf;
return true;
}
return false;
}
if (!_children.TryGetValue(path[0], out var child))
_children[path[0]] = child = new Trie<TKey, TValue>();
return child.Set(path[1..], weakLeaf, value, rewrite);
}
/// <summary>
/// Removes the value associated with the specified key path in the trie.
/// If the specified path exists and has a corresponding value, the value will be removed.
/// Intermediate nodes may also be adjusted if they become empty after the removal.
/// </summary>
/// <param name="path">The key path where the value should be removed. Each element in the array represents a level in the trie.</param>
/// <param name="weakLeafDrop">Indicates whether to drop all children of a weak leaf node when it is removed. Defaults to true.</param>
/// <returns>Returns true if the value was successfully removed, otherwise false.</returns>
public bool Remove(TKey[] path, bool weakLeafDrop = true)
{
if (path.Length == 0 || _isWeakLeaf)
{
if (_value == null) return false;
_value = default;
// If this node is a weak leaf, we can drop all children as well, since they are not reachable anymore.
if (_isWeakLeaf && weakLeafDrop)_children.Clear();
_isWeakLeaf = false;
return true;
}
if (!_children.TryGetValue(path[0], out var child)) return false;
var childRemoved = child.Remove(path[1..]);
// Fold branch if empty
if (child._children.Count == 0)
_children.Remove(path[0]);
return childRemoved;
}
/// <summary>
/// Attempts to retrieve a value from the trie at the specified key path.
/// </summary>
/// <param name="path">The key path to search within the trie. Each element represents a level in the trie.</param>
/// <param name="value">When this method returns, contains the value associated with the specified key path, if the key path is found; otherwise, contains the default value for the type of the value parameter. This parameter is passed uninitialized.</param>
/// <returns>Returns true if a value is found at the specified key path; otherwise, false.</returns>
public bool TryGetValue(TKey[] path, [NotNullWhen(true)] out TValue? value)
{
if (path.Length == 0 || _isWeakLeaf)
{
value = _value;
return value != null;
}
if (_children.TryGetValue(path[0], out var child))
return child.TryGetValue(path[1..], out value);
value = default;
return false;
}
}
+7
View File
@@ -0,0 +1,7 @@
{
"sdk": {
"version": "8.0.0",
"rollForward": "latestMinor",
"allowPrerelease": false
}
}