Current section
Files
Jump to
Current section
Files
src/spatial@grid.erl
-module(spatial@grid).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/spatial/grid.gleam").
-export([new/2, query_all/1, count/1, bounds/1, cell_size/1, cell_count/1, insert/3, remove/3, query_radius/3, 'query'/2]).
-export_type([grid/1]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Hash Grid spatial partitioning data structure.\n"
"\n"
" Uses Erlang maps (HAMT with branching factor 32) providing O(log₃₂ n) operations,\n"
" which is effectively O(1) in practice. At 1 million cells, operations take only\n"
" ~5 steps. Excellent for uniformly distributed objects like particles or crowds.\n"
).
-opaque grid(KRP) :: {grid,
float(),
spatial@collider:internal_collider(),
gleam@dict:dict({integer(), integer(), integer()}, list({vec@vec3:vec3(float()),
KRP}))}.
-file("src/spatial/grid.gleam", 42).
?DOC(
" Create a new empty hash grid.\n"
"\n"
" ## Parameters\n"
" - `cell_size`: Size of each grid cell (should match typical object size)\n"
" - `bounds`: The spatial region this grid covers\n"
"\n"
" ## Example\n"
" ```gleam\n"
" let bounds = collider.box(\n"
" min: vec3.Vec3(-100.0, -100.0, -100.0),\n"
" max: vec3.Vec3(100.0, 100.0, 100.0),\n"
" )\n"
" let grid = grid.new(cell_size: 10.0, bounds: bounds)\n"
" ```\n"
).
-spec new(float(), spatial@collider:internal_collider()) -> grid(any()).
new(Cell_size, Bounds) ->
{grid, Cell_size, Bounds, maps:new()}.
-file("src/spatial/grid.gleam", 156).
?DOC(
" Query all items in the grid (useful for iteration).\n"
"\n"
" **Time Complexity**: O(n) where n is the total number of items.\n"
).
-spec query_all(grid(KSJ)) -> list({vec@vec3:vec3(float()), KSJ}).
query_all(Grid) ->
_pipe = maps:values(erlang:element(4, Grid)),
lists:append(_pipe).
-file("src/spatial/grid.gleam", 164).
?DOC(
" Count total items in the grid.\n"
"\n"
" **Time Complexity**: O(m) where m is the number of occupied cells.\n"
).
-spec count(grid(any())) -> integer().
count(Grid) ->
gleam@dict:fold(
erlang:element(4, Grid),
0,
fun(Acc, _, Items) -> Acc + erlang:length(Items) end
).
-file("src/spatial/grid.gleam", 169).
?DOC(" Get the bounds of the grid.\n").
-spec bounds(grid(any())) -> spatial@collider:internal_collider().
bounds(Grid) ->
erlang:element(3, Grid).
-file("src/spatial/grid.gleam", 174).
?DOC(" Get the cell size of the grid.\n").
-spec cell_size(grid(any())) -> float().
cell_size(Grid) ->
erlang:element(2, Grid).
-file("src/spatial/grid.gleam", 179).
?DOC(" Get the number of non-empty cells.\n").
-spec cell_count(grid(any())) -> integer().
cell_count(Grid) ->
maps:size(erlang:element(4, Grid)).
-file("src/spatial/grid.gleam", 185).
-spec position_to_cell(vec@vec3:vec3(float()), float()) -> {integer(),
integer(),
integer()}.
position_to_cell(Position, Cell_size) ->
X = begin
_pipe = math:floor(case Cell_size of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> erlang:element(2, Position) / Gleam@denominator
end),
erlang:round(_pipe)
end,
Y = begin
_pipe@1 = math:floor(case Cell_size of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@1 -> erlang:element(3, Position) / Gleam@denominator@1
end),
erlang:round(_pipe@1)
end,
Z = begin
_pipe@2 = math:floor(case Cell_size of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@2 -> erlang:element(4, Position) / Gleam@denominator@2
end),
erlang:round(_pipe@2)
end,
{X, Y, Z}.
-file("src/spatial/grid.gleam", 50).
?DOC(
" Insert an item at a position into the grid.\n"
"\n"
" **Time Complexity**: O(log₃₂ n) where n is the number of occupied cells,\n"
" effectively O(1) in practice. Faster than octree for uniform distributions.\n"
).
-spec insert(grid(KRS), vec@vec3:vec3(float()), KRS) -> grid(KRS).
insert(Grid, Position, Item) ->
case spatial@collider:contains_point(erlang:element(3, Grid), Position) of
false ->
Grid;
true ->
Cell_key = position_to_cell(Position, erlang:element(2, Grid)),
Current_items = begin
_pipe = gleam_stdlib:map_get(erlang:element(4, Grid), Cell_key),
gleam@result:unwrap(_pipe, [])
end,
Updated_items = [{Position, Item} | Current_items],
Updated_cells = gleam@dict:insert(
erlang:element(4, Grid),
Cell_key,
Updated_items
),
{grid,
erlang:element(2, Grid),
erlang:element(3, Grid),
Updated_cells}
end.
-file("src/spatial/grid.gleam", 67).
?DOC(
" Remove an item from the grid.\n"
"\n"
" **Time Complexity**: O(log₃₂ n) cell lookup + O(k) filtering where n is the\n"
" number of occupied cells and k is items in that cell. Effectively O(k) in practice.\n"
).
-spec remove(grid(KRW), vec@vec3:vec3(float()), fun((KRW) -> boolean())) -> grid(KRW).
remove(Grid, Position, Predicate) ->
case spatial@collider:contains_point(erlang:element(3, Grid), Position) of
false ->
Grid;
true ->
Cell_key = position_to_cell(Position, erlang:element(2, Grid)),
case gleam_stdlib:map_get(erlang:element(4, Grid), Cell_key) of
{error, _} ->
Grid;
{ok, Items} ->
Filtered_items = gleam@list:filter(
Items,
fun(Item_pair) ->
{Pos, Item} = Item_pair,
Is_at_position = vec@vec3f:distance(Pos, Position) < 0.0001,
Matches_predicate = Predicate(Item),
not (Is_at_position andalso Matches_predicate)
end
),
Updated_cells = case Filtered_items of
[] ->
gleam@dict:delete(erlang:element(4, Grid), Cell_key);
_ ->
gleam@dict:insert(
erlang:element(4, Grid),
Cell_key,
Filtered_items
)
end,
{grid,
erlang:element(2, Grid),
erlang:element(3, Grid),
Updated_cells}
end
end.
-file("src/spatial/grid.gleam", 192).
-spec get_cells_in_radius(vec@vec3:vec3(float()), float(), float()) -> list({integer(),
integer(),
integer()}).
get_cells_in_radius(Center, Radius, Cell_size) ->
Center_cell = position_to_cell(Center, Cell_size),
Cell_radius = begin
_pipe = math:ceil(case Cell_size of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> Radius / Gleam@denominator
end),
erlang:round(_pipe)
end,
Range_x = gleam@list:range(
erlang:element(1, Center_cell) - Cell_radius,
erlang:element(1, Center_cell) + Cell_radius
),
Range_y = gleam@list:range(
erlang:element(2, Center_cell) - Cell_radius,
erlang:element(2, Center_cell) + Cell_radius
),
Range_z = gleam@list:range(
erlang:element(3, Center_cell) - Cell_radius,
erlang:element(3, Center_cell) + Cell_radius
),
gleam@list:flat_map(
Range_x,
fun(X) ->
gleam@list:flat_map(
Range_y,
fun(Y) -> gleam@list:map(Range_z, fun(Z) -> {X, Y, Z} end) end
)
end
).
-file("src/spatial/grid.gleam", 128).
?DOC(
" Query all items within a radius of a point.\n"
"\n"
" Extremely fast for uniform distributions - only checks nearby cells.\n"
"\n"
" **Time Complexity**: O(c·log₃₂ m + k) where c is cells checked, m is occupied\n"
" cells, and k is results. Effectively O(c + k) in practice.\n"
).
-spec query_radius(grid(KSE), vec@vec3:vec3(float()), float()) -> list({vec@vec3:vec3(float()),
KSE}).
query_radius(Grid, Center, Radius) ->
Cells_to_check = get_cells_in_radius(
Center,
Radius,
erlang:element(2, Grid)
),
Radius_sq = Radius * Radius,
gleam@list:fold(
Cells_to_check,
[],
fun(Acc, Cell_key) ->
case gleam_stdlib:map_get(erlang:element(4, Grid), Cell_key) of
{error, _} ->
Acc;
{ok, Items} ->
gleam@list:fold(
Items,
Acc,
fun(Acc_inner, Item_pair) ->
{Pos, _} = Item_pair,
case spatial_ffi:distance_squared(Center, Pos) =< Radius_sq of
true ->
[Item_pair | Acc_inner];
false ->
Acc_inner
end
end
)
end
end
).
-file("src/spatial/grid.gleam", 212).
-spec get_cells_in_bounds(spatial@collider:internal_collider(), float()) -> list({integer(),
integer(),
integer()}).
get_cells_in_bounds(Bounds, Cell_size) ->
Center = spatial@collider:center(Bounds),
Size = spatial@collider:size(Bounds),
Radius = gleam@float:max(
erlang:element(2, Size),
gleam@float:max(erlang:element(3, Size), erlang:element(4, Size))
)
/ 2.0,
get_cells_in_radius(Center, Radius, Cell_size).
-file("src/spatial/grid.gleam", 104).
?DOC(
" Query all items within a collider region.\n"
"\n"
" More efficient than octree for uniform distributions.\n"
"\n"
" **Time Complexity**: O(c·log₃₂ m + k) where c is cells checked, m is occupied\n"
" cells, and k is results. Effectively O(c + k) in practice.\n"
).
-spec 'query'(grid(KSA), spatial@collider:internal_collider()) -> list({vec@vec3:vec3(float()),
KSA}).
'query'(Grid, Query_bounds) ->
Cells_to_check = get_cells_in_bounds(Query_bounds, erlang:element(2, Grid)),
gleam@list:fold(
Cells_to_check,
[],
fun(Acc, Cell_key) ->
case gleam_stdlib:map_get(erlang:element(4, Grid), Cell_key) of
{error, _} ->
Acc;
{ok, Items} ->
gleam@list:fold(
Items,
Acc,
fun(Acc_inner, Item_pair) ->
{Pos, _} = Item_pair,
case spatial@collider:contains_point(
Query_bounds,
Pos
) of
true ->
[Item_pair | Acc_inner];
false ->
Acc_inner
end
end
)
end
end
).