defmodule Isotope.NoiseMap do @moduledoc """ A compact representation of a 2D noise map backed by a flat binary of little-endian `f32` values in row-major order. ## Memory Layout The `data` field is a binary where each value is a 32-bit float in little-endian byte order. Values are stored row by row (row-major). A 100x100 map uses exactly 40,000 bytes (~40 KB), compared to ~3.2 MB for the equivalent nested Elixir list. ## Accessing Values {:ok, noise} = Isotope.Noise.new() nm = Isotope.Noise.noise_map(noise, {100, 100}) # Single value at column 10, row 5 Isotope.NoiseMap.get(nm, 10, 5) # Entire row as a list of floats Isotope.NoiseMap.row(nm, 0) # Convert to nested list (for compatibility or serialization) Isotope.NoiseMap.to_list(nm) # Dimensions {width, height} = Isotope.NoiseMap.size(nm) ## Enumerable The `Enumerable` protocol is implemented, iterating over rows (each row is a `[float()]`). This means standard `Enum` functions work directly: # Average value per row Enum.map(nm, fn row -> Enum.sum(row) / length(row) end) # Find the row with the highest peak Enum.max_by(nm, fn row -> Enum.max(row) end) # First 5 rows Enum.take(nm, 5) ## Migration from `[[float()]]` If you have code that expects nested lists, use `Isotope.NoiseMap.to_list/1`: nm = Isotope.Noise.noise_map(noise, {100, 100}) nested_list = Isotope.NoiseMap.to_list(nm) """ @enforce_keys [:data, :width, :height] defstruct [:data, :width, :height] @type t :: %__MODULE__{ data: binary(), width: non_neg_integer(), height: non_neg_integer() } @doc """ Returns the noise value at column `x`, row `y`. Raises `ArgumentError` if the coordinates are out of bounds. iex> nm = %Isotope.NoiseMap{data: <<1.0::float-little-32, 2.0::float-little-32, 3.0::float-little-32, 4.0::float-little-32>>, width: 2, height: 2} iex> Isotope.NoiseMap.get(nm, 1, 0) 2.0 """ @spec get(t(), non_neg_integer(), non_neg_integer()) :: float() def get(%__MODULE__{data: data, width: w, height: h}, x, y) when x >= 0 and x < w and y >= 0 and y < h do offset = (y * w + x) * 4 <<_::binary-size(offset), val::float-little-32, _::binary>> = data val end def get(%__MODULE__{width: w, height: h}, x, y) do raise ArgumentError, "coordinates (#{x}, #{y}) out of bounds for #{w}x#{h} noise map" end @doc """ Returns row `y` as a list of floats. Raises `ArgumentError` if the row index is out of bounds. iex> nm = %Isotope.NoiseMap{data: <<1.0::float-little-32, 2.0::float-little-32, 3.0::float-little-32, 4.0::float-little-32>>, width: 2, height: 2} iex> Isotope.NoiseMap.row(nm, 0) [1.0, 2.0] """ @spec row(t(), non_neg_integer()) :: [float()] def row(%__MODULE__{data: data, width: w, height: h}, y) when y >= 0 and y < h do offset = y * w * 4 row_bytes = w * 4 <<_::binary-size(offset), row_data::binary-size(row_bytes), _::binary>> = data decode_row(row_data, []) end def row(%__MODULE__{height: h}, y) do raise ArgumentError, "row #{y} out of bounds for noise map with #{h} rows" end @doc """ Converts the noise map to a nested list `[[float()]]` (list of rows). iex> nm = %Isotope.NoiseMap{data: <<1.0::float-little-32, 2.0::float-little-32, 3.0::float-little-32, 4.0::float-little-32>>, width: 2, height: 2} iex> Isotope.NoiseMap.to_list(nm) [[1.0, 2.0], [3.0, 4.0]] """ @spec to_list(t()) :: [[float()]] def to_list(%__MODULE__{height: h} = nm) do for y <- 0..(h - 1), do: row(nm, y) end @doc """ Returns `{width, height}` of the noise map. iex> nm = %Isotope.NoiseMap{data: <<0::128>>, width: 2, height: 2} iex> Isotope.NoiseMap.size(nm) {2, 2} """ @spec size(t()) :: {non_neg_integer(), non_neg_integer()} def size(%__MODULE__{width: w, height: h}), do: {w, h} defp decode_row(<<>>, acc), do: Enum.reverse(acc) defp decode_row(<>, acc), do: decode_row(rest, [val | acc]) defimpl Enumerable do def count(%Isotope.NoiseMap{height: h}), do: {:ok, h} def member?(_nm, _element), do: {:error, __MODULE__} def reduce(%Isotope.NoiseMap{height: h} = nm, acc, fun) do reduce_rows(nm, 0, h, acc, fun) end defp reduce_rows(_nm, _y, _h, {:halt, acc}, _fun), do: {:halted, acc} defp reduce_rows(nm, y, h, {:suspend, acc}, fun), do: {:suspended, acc, &reduce_rows(nm, y, h, &1, fun)} defp reduce_rows(_nm, h, h, {:cont, acc}, _fun), do: {:done, acc} defp reduce_rows(nm, y, h, {:cont, acc}, fun) do row = Isotope.NoiseMap.row(nm, y) reduce_rows(nm, y + 1, h, fun.(row, acc), fun) end def slice(%Isotope.NoiseMap{height: h} = nm) do {:ok, h, &slice_fun(nm, &1, &2, &3)} end defp slice_fun(nm, start, length, step) do start..(start + (length - 1) * step)//step |> Enum.map(&Isotope.NoiseMap.row(nm, &1)) end end end