Decodes element rows from a PLY body.
Row decoding is driven by a layout — a flat list of
{name, type_or_list_spec} derived once from the element and reused for
every row. Re-deriving it per row is the difference between one pass over a
few dozen properties and one pass per row over millions.
Binary loops thread the remaining binary as the first argument of a tail-recursive function, which keeps the BEAM's match-context optimisation alive; splitting the binary up front discards it and reintroduces copying.
ASCII input is handled as a list of already-tokenised lines rather than a string that gets re-split on every call. Rejoining a remainder back into a string between chunks makes streaming quadratic — a large file would be re-tokenised once per chunk.
Summary
Types
ASCII input, pre-split into lines of whitespace-separated tokens.
A decoded row: property name to value (or list of values).
Functions
Decodes a fixed-width element into one packed binary per property.
Decodes count rows from tokenised ASCII lines.
Decodes count rows of element from the front of a binary body.
The flat {name, type} layout used to drive row decoding.
Splits ASCII body text into lines of tokens.
Types
@type lines() :: [[String.t()]]
ASCII input, pre-split into lines of whitespace-separated tokens.
@type row() :: %{required(String.t()) => Ply.Types.value() | [Ply.Types.value()]}
A decoded row: property name to value (or list of values).
Functions
@spec columns( binary(), Ply.Element.t(), Ply.Types.endianness(), non_neg_integer(), MapSet.t(String.t()) | nil ) :: {:ok, %{required(String.t()) => binary()}} | {:error, Ply.Error.t()}
Decodes a fixed-width element into one packed binary per property.
This is the path for large numeric elements such as Gaussian splats: it
avoids materialising millions of boxed BEAM floats, and the result can be
handed to Nx.from_binary/2 without a copy.
Output binaries are little-endian regardless of the source file, so callers get one predictable layout; big-endian input is byte-swapped during extraction.
Returns {:error, %Ply.Error{kind: :variable_width}} for elements
containing list properties, which have no columnar representation.
only names the properties to extract; nil extracts all of them. Every
property is still walked, because a column's offset depends on the widths of
those before it — but the unwanted ones are never sliced, which is the whole
saving on a splat file where three of sixty columns are wanted.
@spec decode_ascii(lines(), Ply.Element.t(), non_neg_integer(), keyword()) :: {:ok, [row()], lines()} | {:error, Ply.Error.t()}
Decodes count rows from tokenised ASCII lines.
A row normally occupies exactly one line. A row is allowed to continue onto
the next line when the current one runs out of values, since the format
defines the body as a value stream rather than a line-oriented table — but
leftover tokens on a line where a row ended are an error. Ignoring them
silently shifts every subsequent row by one value, which corrupts the whole
element while still returning {:ok, _}.
@spec decode_rows( binary(), Ply.Element.t(), Ply.Types.endianness(), non_neg_integer(), keyword() ) :: {:ok, [row()], binary(), non_neg_integer()} | {:error, Ply.Error.t()}
Decodes count rows of element from the front of a binary body.
Returns the rows, the unconsumed remainder, and the absolute byte offset
reached, so errors can report where in the file they occurred rather than
where in the chunk. :first_row keeps row numbers absolute across chunked
reads.
@spec layout(Ply.Element.t()) :: [{String.t(), Ply.Property.kind()}]
The flat {name, type} layout used to drive row decoding.
Derived once per element and reused for every row.
Splits ASCII body text into lines of tokens.
Done once per file; the resulting list is threaded through decoding so no chunk ever re-scans text it has already seen.