Data-SpatialHash-Shared

 view release on metacpan or  search on metacpan

lib/Data/SpatialHash/Shared.pm  view on Meta::CPAN

=head2 2D vs 3D

All methods that accept coordinates accept either (x, y) or (x, y, z).
When z is omitted it is treated as 0, so a 2D query matches the z=0 cell layer
(points within one C<cell_size> of the z=0 plane), not an infinite z-column, and
z does not enter the distance for a 2D call.  A handle created with a 2D
insert can be queried with either 2D or 3D calls.

=head2 Toroidal space

By default the grid is unbounded and Euclidean.  Construct with
C<< wrap => [$Wx, $Wy] >> (2D) or C<< [$Wx, $Wy, $Wz] >> (3D) to make space a
seamless B<torus>: neighbour-cell expansion wraps around the grid edges and
C<query_radius>, C<query_knn>, C<each_in_radius>, C<query_radius_many>, and the
pair emitters all use the minimum-image (shortest wrapping) distance

    dx = abs(ax - bx);  dx = $Wx - dx if dx > $Wx / 2;   # per axis

so an entry near C<0> and one near C<$Wx> are neighbours.  Keep positions within
C<[0, $W)> per axis for the metric to be meaningful.  Each wrapped extent must be
a positive multiple of C<cell_size> so the cells tile the world exactly,
otherwise the constructor croaks.  C<query_cell> resolves its coordinate to a
wrapped cell, but C<query_aabb> uses the literal (non-wrapping) box even in a
wrapping world.  The wrap configuration is part of the mapped format and is
restored on reopen; the C<world> accessor returns the extents.

=head1 METHODS

=head2 Constructors

    my $s = Data::SpatialHash::Shared->new($path, $max, $buckets, $cell);
    my $s = Data::SpatialHash::Shared->new(undef, $max, $buckets, $cell);
    my $s = Data::SpatialHash::Shared->new_memfd($name, $max, $buckets, $cell);
    my $s = Data::SpatialHash::Shared->new_from_fd($fd);
    my $s = Data::SpatialHash::Shared->new($path, $max, $buckets, $cell, wrap => [$Wx, $Wy]);

C<$path> is the backing file path; C<undef> creates an anonymous mapping.
C<$max> is the maximum number of entries.  C<$buckets> is the bucket count
(0 = auto).  C<$cell> is the cell size (float).

Pass C<< wrap => [$Wx, $Wy] >> (or C<< [$Wx, $Wy, $Wz] >>) to make the world a
seamless torus of those extents (see L</Toroidal space>); omit it for an
unbounded Euclidean space.

When reopening an existing backing file or memfd, the stored header wins: the
caller's C<$max>, C<$buckets>, C<$cell>, C<wrap>, and C<sphere> arguments are
ignored and the file's original values are used. They are still validated for
well-formedness first (for example a non-positive C<$cell> croaks), so pass
plausible values even on a reopen; only the stored geometry is authoritative.

C<new_memfd> creates a Linux memfd (anonymous but transferable via C<memfd>
file descriptor).  C<new_from_fd> reopens an existing memfd in another
process.

=head2 Mutators

    my $h = $s->insert(x, y, value);        # 2D insert -- returns handle or undef
    my $h = $s->insert(x, y, z, value);     # 3D insert
    my $h = $s->insert(x, y, z, value, r);  # 3D insert with an interaction radius
    $s->set_radius($h, $r);                 # set/replace an entry's radius
    $s->move($h, x, y);                     # relocate entry (2D)
    $s->move($h, x, y, z);                  # relocate entry (3D)
    $s->remove($h);                         # free entry slot
    $s->set_value($h, $v);                  # update stored value
    $s->clear;                              # remove all entries
    my @ids = $s->insert_many([ [x,y,value], [x,y,value,r], ... ]);  # bulk insert
    my $n   = $s->move_many([ [handle,x,y], [handle,x,y,z], ... ]);  # bulk move

C<insert> returns a handle (opaque integer) on success, or C<undef> if
C<max_entries> is exhausted.  C<move> and C<remove> return true on success,
or false if the handle is invalid or already removed.  C<set_value> instead
croaks on an invalid or freed handle; it and C<clear> return nothing.

Each entry may carry an B<interaction radius> (default 0; must be finite and
non-negative), used by C<each_colliding_pair>.  Set it with the 5-argument C<insert> or with
C<set_radius> (which croaks on an invalid or freed handle); for a 2D entry with
a radius, insert then call C<set_radius>.  C<insert_many> and C<move_many> apply
a whole batch under a single lock acquisition -- each row is an arrayref.
C<insert_many> inserts B<2D> entries (rows C<[x,y,value]> or
C<[x,y,value,radius]>; use C<insert> in a loop for 3D) and returns the list of
handles, with C<undef> for any row that overflowed the pool, was malformed
(not an arrayref of length 3 or 4), or carried a negative or non-finite radius.  C<move_many> takes C<[handle,x,y]> or
C<[handle,x,y,z]> rows and returns the count successfully moved; freed/invalid
handles and malformed rows are skipped.

Handles are entry slot indices starting at B<0>, and B<0 is false in
Perl>.  The very first insert into a fresh hash returns handle C<0>.
Always test the result with C<defined $h>, never for truthiness:

    my $h = $s->insert($x, $y, $v);
    die "full" unless defined $h;   # correct: handle 0 is valid
    # WRONG: "unless $h" would treat the first handle (0) as failure

=head2 Accessors

    $s->has($h);              # true if handle is live
    $s->value($h);            # stored value
    $s->get_radius($h);       # stored interaction radius (0 if unset)
    my ($x, $y, $z) = $s->position($h);   # current position

C<has> is the safe predicate for a possibly-freed handle; C<value>,
C<position>, and C<get_radius> croak on an invalid or freed handle.

=head2 Queries

Most query methods return a list of stored values (not handles) for
matching entries; C<query_radius_many> instead returns an arrayref of id-list
arrayrefs (see L</Batched radius queries>).  For C<query_knn>, results are in
nearest-first order.

    my @ids = $s->query_radius(x, y, r);         # 2D radius search
    my @ids = $s->query_radius(x, y, z, r);      # 3D radius search
    my @ids = $s->query_aabb(x0, y0, x1, y1);    # 2D axis-aligned box
    my @ids = $s->query_aabb(x0, y0, z0, x1, y1, z1);  # 3D box
    my @ids = $s->query_cell(x, y);              # single cell (2D)
    my @ids = $s->query_cell(x, y, z);           # single cell (3D)
    my @ids = $s->query_knn(x, y, k);            # k nearest (2D)
    my @ids = $s->query_knn(x, y, z, k);         # k nearest (3D)
    $s->each_in_radius(x, y, r, sub { my ($v) = @_; ... });    # 2D cb
    $s->each_in_radius(x, y, z, r, sub { my ($v) = @_; ... }); # 3D cb
    my $lists = $s->query_radius_many([ [x,y,r], [x,y,z,r] ]); # N radius queries, one lock



( run in 0.490 second using v1.01-cache-2.11-cpan-84de2e75c66 )