# vim: set et sw=4 sts=4 fileencoding=utf-8:
#
# An alternate Python Minecraft library for the Rasperry-Pi
# Copyright (c) 2013-2015 Dave Jones <dave@waveform.org.uk>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the copyright holder nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""
The vector module defines the :class:`Vector` class, which is the usual method
of represent coordinates or vectors when dealing with the Minecraft world. It
also provides the :func:`vector_range` function for generating a sequence of
vectors.
.. note::
All items in this module are available from the :mod:`picraft` namespace
without having to import :mod:`picraft.vector` directly.
The following items are defined in the module:
Vector
======
.. autoclass:: Vector(x, y, z)
vector_range
============
.. autoclass:: vector_range
:members:
"""
from __future__ import (
unicode_literals,
absolute_import,
print_function,
division,
)
str = type('')
from .compat import range
import sys
import math
from functools import reduce, total_ordering
from collections import namedtuple
try:
from itertools import zip_longest
except ImportError:
# Py2 compat
from itertools import izip_longest as zip_longest
try:
from collections.abc import Sequence
except ImportError:
# Py2 compat
from collections import Sequence
[docs]class Vector(namedtuple('Vector', ('x', 'y', 'z'))):
"""
Represents a 3-dimensional vector.
This tuple derivative represents a 3-dimensional vector with x, y, z
components. Instances can be constructed in a number of ways. By explicitly
specifying the x, y, and z components (optionally with keyword
identifiers), or leaving the empty to default to 0::
>>> Vector(1, 1, 1)
Vector(x=1, y=1, z=1)
>>> Vector(x=2, y=0, z=0)
Vector(x=2, y=0, z=0)
>>> Vector()
Vector(x=0, y=0, z=0)
>>> Vector(y=10)
Vector(x=0, y=10, z=0)
Note that vectors don't much care whether their components are integers,
floating point values, or ``None``::
>>> Vector(1.0, 1, 1)
Vector(x=1.0, y=1, z=1)
>>> Vector(2, None, None)
Vector(x=2, y=None, z=None)
The class supports simple arithmetic operations with other vectors such as
addition and subtraction, along with multiplication and division with
scalars, raising to powers, bit-shifting, and so on. Such operations are
performed element-wise [1]_::
>>> v1 = Vector(1, 1, 1)
>>> v2 = Vector(2, 2, 2)
>>> v1 + v2
Vector(x=3, y=3, z=3)
>>> 2 * v2
Vector(x=4, y=4, z=4)
Simple arithmetic operations with scalars return a new vector with that
operation performed on all elements of the original. For example::
>>> v = Vector()
>>> v
Vector(x=0, y=0, z=0)
>>> v + 1
Vector(x=1, y=1, z=1)
>>> 2 * (v + 2)
Vector(x=4, y=4, z=4)
>>> Vector(y=2) ** 2
Vector(x=0, y=4, z=0)
Within the Minecraft world, the X,Z plane represents the ground, while the
Y vector represents height.
.. note::
Note that, as a derivative of tuple, instances of this class are
immutable. That is, you cannot directly manipulate the x, y, and z
attributes; instead you must create a new vector (for example, by
adding two vectors together). The advantage of this is that vector
instances can be used in sets or as dictionary keys.
.. [1] I realize math purists will hate this (and demand that abs() should
be magnitude and * should invoke matrix multiplication), but the
element wise operations are sufficiently useful to warrant the
short-hand syntax.
.. automethod:: replace
.. automethod:: ceil
.. automethod:: floor
.. automethod:: dot
.. automethod:: cross
.. automethod:: distance_to
.. attribute:: x
The position or length of the vector along the X-axis. In the Minecraft
world this can be considered to run left-to-right.
.. attribute:: y
The position or length of the vector along the Y-axis. In the Minecraft
world this can be considered to run vertically up and down.
.. attribute:: z
The position or length of the vector along the Z-axis. In the Minecraft
world this can be considered as depth (in or out of the screen).
.. autoattribute:: magnitude
.. autoattribute:: unit
"""
def __new__(cls, x=0, y=0, z=0):
return super(Vector, cls).__new__(cls, x, y, z)
@classmethod
def from_string(cls, s, type=int):
x, y, z = s.split(',', 2)
return cls(type(x), type(y), type(z))
def __str__(self):
return '%s,%s,%s' % (self.x, self.y, self.z)
def __add__(self, other):
try:
return Vector(self.x + other.x, self.y + other.y, self.z + other.z)
except AttributeError:
return Vector(self.x + other, self.y + other, self.z + other)
__radd__ = __add__
def __sub__(self, other):
try:
return Vector(self.x - other.x, self.y - other.y, self.z - other.z)
except AttributeError:
return Vector(self.x - other, self.y - other, self.z - other)
def __mul__(self, other):
try:
return Vector(self.x * other.x, self.y * other.y, self.z * other.z)
except AttributeError:
return Vector(self.x * other, self.y * other, self.z * other)
__rmul__ = __mul__
def __truediv__(self, other):
try:
return Vector(self.x / other.x, self.y / other.y, self.z / other.z)
except AttributeError:
return Vector(self.x / other, self.y / other, self.z / other)
def __floordiv__(self, other):
try:
return Vector(self.x // other.x, self.y // other.y, self.z // other.z)
except AttributeError:
return Vector(self.x // other, self.y // other, self.z // other)
def __mod__(self, other):
try:
return Vector(self.x % other.x, self.y % other.y, self.z % other.z)
except AttributeError:
return Vector(self.x % other, self.y % other, self.z % other)
def __pow__(self, other):
try:
return Vector(self.x ** other.x, self.y ** other.y, self.z ** other.z)
except AttributeError:
return Vector(self.x ** other, self.y ** other, self.z ** other)
def __lshift__(self, other):
try:
return Vector(self.x << other.x, self.y << other.y, self.z << other.z)
except AttributeError:
return Vector(self.x << other, self.y << other, self.z << other)
def __rshift__(self, other):
try:
return Vector(self.x >> other.x, self.y >> other.y, self.z >> other.z)
except AttributeError:
return Vector(self.x >> other, self.y >> other, self.z >> other)
def __neg__(self):
return Vector(-self.x, -self.y, -self.z)
def __pos__(self):
return self
def __abs__(self):
return Vector(abs(self.x), abs(self.y), abs(self.z))
def __bool__(self):
return bool(self.x or self.y or self.z)
# Py2 compat
__nonzero__ = __bool__
__div__ = __truediv__
[docs] def replace(self, x=None, y=None, z=None):
"""
Return the vector with the x, y, or z axes replaced with the specified
values. For example::
>>> Vector(1, 2, 3).replace(z=4)
Vector(x=1, y=2, z=4)
"""
# XXX What if I want to use None?
return Vector(
self.x if x is None else x,
self.y if y is None else y,
self.z if z is None else z)
[docs] def floor(self):
"""
Return the vector with the floor of each component. This is only useful
for vectors containing floating point components::
>>> Vector(0.5, -0.5, 1.9)
Vector(0.0, -1.0, 1.0)
"""
return Vector(
math.floor(self.x),
math.floor(self.y),
math.floor(self.z))
[docs] def ceil(self):
"""
Return the vector with the ceiling of each component. This is only
useful for vectors containing floating point components::
>>> Vector(0.5, -0.5, 1.2)
Vector(1.0, 0.0, 2.0)
"""
return Vector(
math.ceil(self.x),
math.ceil(self.y),
math.ceil(self.z))
[docs] def dot(self, other):
"""
Return the `dot product`_ of the vector with the *other* vector. The
result is a scalar value. For example::
>>> Vector(1, 2, 3).dot(Vector(2, 2, 2))
12
>>> Vector(1, 2, 3).dot(Vector(x=1))
1
.. _dot product: http://en.wikipedia.org/wiki/Dot_product
"""
return self.x * other.x + self.y * other.y + self.z * other.z
[docs] def cross(self, other):
"""
Return the `cross product`_ of the vector with the *other* vector. The
result is another vector. For example::
>>> Vector(1, 2, 3).cross(Vector(2, 2, 2))
Vector(x=-2, y=4, z=-2)
>>> Vector(1, 2, 3).cross(Vector(x=1))
Vector(x=0, y=3, z=-2)
.. _cross product: http://en.wikipedia.org/wiki/Cross_product
"""
return Vector(
self.y * other.z - self.z * other.y,
self.z * other.x - self.x * other.z,
self.x * other.y - self.y * other.x)
[docs] def distance_to(self, other):
"""
Return the Euclidian distance between two three dimensional points
(represented as vectors), calculated according to `Pythagoras'
theorem`_. For example::
>>> Vector(1, 2, 3).distance_to(Vector(2, 2, 2))
1.4142135623730951
>>> Vector().distance_to(Vector(x=1))
1.0
.. _Pythagoras' theorem: http://en.wikipedia.org/wiki/Pythagorean_theorem
"""
return math.sqrt(
(self.x - other.x) ** 2 +
(self.y - other.y) ** 2 +
(self.z - other.z) ** 2)
@property
def magnitude(self):
"""
Returns the magnitude of the vector. This could also be considered the
distance of the vector from the origin, i.e. ``v.magnitude`` is
equivalent to ``Vector().distance_to(v)``. For example::
>>> Vector(2, 4, 4).magnitude
6.0
>>> Vector().distance_to(Vector(2, 4, 4))
6.0
"""
return math.sqrt(self.x ** 2 + self.y ** 2 + self.z ** 2)
@property
def unit(self):
"""
Return a `unit vector`_ (a vector with a magnitude of one) with the
same direction as this vector::
>>> Vector(1, 0, 0).unit
Vector(x=1.0, y=0.0, z=0.0)
>>> Vector(0, 2, 0).unit
Vector(x=0.0, y=1.0, z=0.0)
.. note::
If the vector's magnitude is zero, this property returns the
original vector.
.. _unit vector: http://en.wikipedia.org/wiki/Unit_vector
"""
if self.magnitude > 0:
return self / self.magnitude
else:
return self
# TODO Yes, I'm being lazy with total_ordering ... probably ought to define all
# six comparison methods but I haven't got time right now ...
@total_ordering
[docs]class vector_range(Sequence):
"""
Like :func:`range`, :class:`vector_range` is actually a type which
efficiently represents a range of vectors. The arguments to the constructor
must be :class:`Vector` instances (or objects which have integer ``x``,
``y``, and ``z`` attributes).
If *step* is omitted, it defaults to ``Vector(1, 1, 1)``. If the *start*
argument is omitted, it defaults to ``Vector(0, 0, 0)``. If any element
of the *step* vector is zero, :exc:`ValueError` is raised.
The contents of the range are largely determined by the *step* and *order*
which specifies the order in which the axes of the range will be
incremented. For example, with the order ``'xyz'``, the X-axis will be
incremented first, followed by the Y-axis, and finally the Z-axis. So, for
a range with the default *start*, *step*, and *stop* set to ``Vector(3, 3,
3)``, the contents of the range will be::
>>> list(vector_range(Vector(3, 3, 3), order='xyz'))
[Vector(0, 0, 0), Vector(1, 0, 0), Vector(2, 0, 0),
Vector(0, 1, 0), Vector(1, 1, 0), Vector(2, 1, 0),
Vector(0, 2, 0), Vector(1, 2, 0), Vector(2, 2, 0),
Vector(0, 0, 1), Vector(1, 0, 1), Vector(2, 0, 1),
Vector(0, 1, 1), Vector(1, 1, 1), Vector(2, 1, 1),
Vector(0, 2, 1), Vector(1, 2, 1), Vector(2, 2, 1),
Vector(0, 0, 2), Vector(1, 0, 2), Vector(2, 0, 2),
Vector(0, 1, 2), Vector(1, 1, 2), Vector(2, 1, 2),
Vector(0, 2, 2), Vector(1, 2, 2), Vector(2, 2, 2)]
Vector ranges implement all common sequence operations except concatenation
and repetition (due to the fact that range objects can only represent
sequences that follow a strict pattern and repetition and concatenation
usually cause the resulting sequence to violate that pattern).
Vector ranges are extremely efficient compared to an equivalent
:class:`list` or :class:`tuple` as they take a small (fixed) amount of
memory, storing only the arguments passed in its construction and
calculating individual items and sub-ranges as requested.
Vector range objects implement the :class:`collections.abc.Sequence` ABC,
and provide features such as containment tests, element index lookup,
slicing and support for negative indices.
The default order (``'zxy'``) may seem an odd choice. This is primarily
used as it's the order used by the Raspberry Juice server when returning
results from the ``getBlocks`` call. In turn, Raspberry Juice probably uses
this order as it results in returning a horizontal layer of vectors at a
time (given the Y-axis is used for height in the Minecraft world).
.. warning::
Bear in mind that the ordering of a vector range may have a bearing on
tests for its ordering and equality. Two ranges with different orders
are unlikely to test equal even though they may have the same *start*,
*stop*, and *step* attributes (and thus contain the same vectors, but
in a different order).
Vector ranges can be accessed by integer index, by :class`Vector` index,
or by a slice of vectors. For example::
>>> v = vector_range(Vector() + 1, Vector() + 3)
>>> list(v)
[Vector(x=1, y=1, z=1),
Vector(x=1, y=1, z=2),
Vector(x=2, y=1, z=1),
Vector(x=2, y=1, z=2),
Vector(x=1, y=2, z=1),
Vector(x=1, y=2, z=2),
Vector(x=2, y=2, z=1),
Vector(x=2, y=2, z=2)]
>>> v[0]
Vector(x=1, y=1, z=1)
>>> v[Vector(0, 0, 0)]
Vector(x=1, y=1, z=1)
>>> v[Vector(1, 0, 0)]
Vector(x=2, y=1, z=1)
>>> v[-1]
Vector(x=2, y=2, z=2)
>>> v[Vector() - 1]
Vector(x=2, y=2, z=2)
>>> v[Vector(x=1):]
vector_range(Vector(x=2, y=1, z=1), Vector(x=3, y=3, z=3),
Vector(x=1, y=1, z=1), order='zxy')
>>> list(v[Vector(x=1):])
[Vector(x=2, y=1, z=1),
Vector(x=2, y=1, z=2),
Vector(x=2, y=2, z=1),
Vector(x=2, y=2, z=2)]
However, integer slices are not currently permitted.
"""
def __init__(
self, start, stop=None, step=None, order='zxy'):
if stop is None:
start, stop = Vector(), start
if step is None:
step = Vector(1, 1, 1)
if (start != start // 1) or (stop != stop // 1) or (step != step // 1):
raise TypeError('integer vectors are required')
if order not in ('xyz', 'xzy', 'yxz', 'yzx', 'zxy', 'zyx'):
raise ValueError('invalid order: %s' % order)
if not (step.x and step.y and step.z):
raise ValueError('no element of step may be zero')
self._start = start
self._stop = stop
self._step = step
self._order = order
self._ranges = [
range(
getattr(self.start, axis),
getattr(self.stop, axis),
getattr(self.step, axis))
for axis in order
]
self._indexes = [
order.index(axis)
for axis in 'xyz'
]
self._xrange, self._yrange, self._zrange = (
self._ranges[i] for i in self._indexes
)
self._len = len(self._xrange) * len(self._yrange) * len(self._zrange)
@property
def start(self):
return self._start
@property
def stop(self):
return self._stop
@property
def step(self):
return self._step
@property
def order(self):
return self._order
def __repr__(self):
if self.start == Vector() and self.step == Vector() + 1:
return 'vector_range(%r, order=%r)' % (self.stop, self.order)
elif self.step == Vector() + 1:
return 'vector_range(%r, %r, order=%r)' % (
self.start, self.stop, self.order)
else:
return 'vector_range(%r, %r, %r, order=%r)' % (
self.start, self.stop, self.step, self.order)
def __len__(self):
return self._len
def __lt__(self, other):
for v1, v2 in zip_longest(self, other):
if v1 < v2:
return True
elif v1 > v2:
return False
return False
def __eq__(self, other):
# Fast-path: if the other object is an identical vector_range we
# can quickly test whether we're equal
if isinstance(other, vector_range):
return (
self._xrange == other._xrange and
self._yrange == other._yrange and
self._zrange == other._zrange and
self.order == other.order
)
# Normal case: test every element in each sequence
for v1, v2 in zip_longest(self, other):
if v1 != v2:
return False
return True
def __ne__(self, other):
return not self.__eq__(other)
def __iter__(self):
for i in range(len(self)):
yield self[i]
def __reversed__(self):
for i in reversed(range(len(self))):
yield self[i]
def __contains__(self, value):
try:
self.index(value)
except ValueError:
return False
else:
return True
def __bool__(self):
return len(self) > 0
# Py2 compat
__nonzero__ = __bool__
def __getitem__(self, index):
if isinstance(index, slice):
return self._get_slice(index)
elif isinstance(index, Vector):
try:
return Vector(*(
self._ranges[i][j]
for i, j in zip(self._indexes, index)
))
except IndexError:
raise IndexError('list index out of range')
else:
if index < 0:
index += len(self)
if not (0 <= index < len(self)):
raise IndexError('list index out of range')
v = (
self._ranges[0][index % len(self._ranges[0])],
self._ranges[1][(index // len(self._ranges[0])) % len(self._ranges[1])],
self._ranges[2][index // (len(self._ranges[0]) * len(self._ranges[1]))],
)
return Vector(*(v[i] for i in self._indexes))
def _get_slice(self, s):
try:
step = Vector() + 1 if s.step is None else s.step
start = Vector(None, None, None) if s.start is None else s.start
stop = Vector(None, None, None) if s.stop is None else s.stop
if not (step.x and step.y and step.z):
raise ValueError(
"every element of the slice's step must be non-zero")
x_range = self._xrange[slice(start.x, stop.x, step.x)]
y_range = self._yrange[slice(start.y, stop.y, step.y)]
z_range = self._zrange[slice(start.z, stop.z, step.z)]
except AttributeError:
raise ValueError(
"vector_range slices must be composed of Vectors")
return vector_range(
Vector(x_range.start, y_range.start, z_range.start),
Vector(x_range.stop, y_range.stop, z_range.stop),
Vector(x_range.step, y_range.step, z_range.step),
self.order)
[docs] def index(self, value):
"""
Return the zero-based index of *value* within the range, or raise
:exc:`ValueError` if *value* does not exist in the range.
"""
ranges = self._ranges
i, j, k = (getattr(value, axis) for axis in self.order)
try:
i_indexes = set(rmod(len(ranges[0]), ranges[0].index(i), range(len(self))))
j_indexes = set(
b
for a in rmod(len(ranges[1]), ranges[1].index(j),
range(len(self) // len(ranges[0])))
for b in rdiv(len(ranges[0]), a)
)
k_indexes = set(rdiv(len(ranges[0]) * len(ranges[1]), ranges[2].index(k)))
result = i_indexes & j_indexes & k_indexes
assert len(result) == 1
result = next(iter(result))
except ValueError:
raise ValueError('%r is not in range' % (value,))
else:
return result
[docs] def count(self, value):
"""
Return the count of instances of *value* within the range (note this
can only be 0 or 1 in the case of a range, and thus is equivalent to
testing membership with ``in``).
"""
# count is provided by the ABC but inefficiently; given no vectors in
# the range can be duplicated we provide a more efficient version here
if value in self:
return 1
else:
return 0
def rmod(denom, result, num_range):
"""
Calculates the inverse of a mod operation.
The *denom* parameter specifies the denominator of the original mod (%)
operation. In this implementation, *denom* must be greater than 0. The
*result* parameter specifies the result of the mod operation. For obvious
reasons this value must be in the range ``[0, denom)`` (greater than or
equal to zero and less than the denominator).
Finally, *num_range* specifies the range that the numerator of the original
mode operation can lie in. This must be an iterable sorted in ascending
order with unique values (e.g. most typically a :func:`range`).
The function returns the set of potential numerators (guaranteed to be a
subset of *num_range*).
"""
if denom <= 0:
raise ValueError('invalid denominator')
if not (0 <= result < denom):
return set()
if len(num_range) == 0:
return set()
assert num_range[-1] >= num_range[0]
start = num_range[0] + (result - num_range[0] % denom) % denom
stop = num_range.stop
return range(start, stop, denom)
def rdiv(denom, result):
"""
Calculates the inverse of a div operation.
The *denom* parameter specifies the denominator of the original div (//)
operation. In this implementation, *denom* must be greater than 0. The
*result* parameter specifies the result of the div operation.
The function returns the set of potential numerators.
"""
if denom <= 0:
raise ValueError('invalid denominator')
return range(result * denom, result * denom + denom)