# 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 functions like :func:`vector_range` for generating sequences 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=0, y=0, z=0)
Short-hand variants
===================
The :class:`Vector` class is used sufficiently often to justify the inclusion
of some shortcuts. The class itself is also available as ``V``, and vectors
representing the three axes are each available as ``X``, ``Y``, and ``Z``.
Finally, a vector representing the origin is available as ``O``::
>>> from picraft import V, O, X, Y, Z
>>> O
Vector(x=0, y=0, z=0)
>>> 2 * X
Vector(x=2, y=0, z=0)
>>> X + Y
Vector(x=1, y=1, z=0)
>>> (X + Y).angle_between(X)
45.00000000000001
>>> V(3, 4, 5).projection(X)
3.0
>>> X.rotate(90, about=Y)
Vector(x=0.0, y=0.0, z=1.0)
vector_range
============
.. autoclass:: vector_range
:members:
line
====
.. autofunction:: line
lines
=====
.. autofunction:: lines
circle
======
.. autofunction:: circle
sphere
======
.. autofunction:: sphere
filled
======
.. autofunction:: filled
"""
from __future__ import (
unicode_literals,
absolute_import,
print_function,
division,
)
str = type('')
from .compat import range
import math
from functools import total_ordering
from collections import namedtuple, Sequence
try:
from itertools import zip_longest, islice, tee
except ImportError:
# Py2 compat
from itertools import izip_longest as zip_longest, islice, tee
[docs]class Vector(namedtuple('Vector', ('x', 'y', 'z'))):
"""
Represents a 3-dimensional vector.
This :func:`~collections.namedtuple` derivative represents a 3-dimensional
vector with :attr:`x`, :attr:`y`, :attr:`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)
Shortcuts are available for vectors representing the X, Y, and Z axes::
>>> X
Vector(x=1, y=0, z=0)
>>> Y
Vector(x=0, y=1, 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, 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)
>>> v1 * v2
Vector(x=2, y=2, z=2)
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 :func:`~collections.namedtuple`,
instances of this class are immutable. That is, you cannot directly
manipulate the :attr:`x`, :attr:`y`, and :attr:`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
members of :class:`set` or keys in a :class:`dict`.
.. [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
.. automethod:: angle_between
.. automethod:: project
.. automethod:: rotate
.. 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
"""
__slots__ = ()
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(',')
return cls(type(x), type(y), type(z))
@property
def __dict__(self):
# This is required to work around a subtle issue encountered in Python
# 3.3 and above. In these versions (probably deliberately), the
# __dict__ property is not inherited by namedtuple descendents
return super(Vector, self).__dict__
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, modulo=None):
if modulo is not None:
try:
# XXX What about other vector, modulo scalar, and other scalar, modulo vector?
return Vector(
pow(self.x, other.x, modulo.x),
pow(self.y, other.y, modulo.y),
pow(self.z, other.z, modulo.z))
except AttributeError:
return Vector(
pow(self.x, other, modulo),
pow(self.y, other, modulo),
pow(self.z, other, modulo))
try:
return Vector(
pow(self.x, other.x),
pow(self.y, other.y),
pow(self.z, other.z))
except AttributeError:
return Vector(
pow(self.x, other),
pow(self.y, other),
pow(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 __and__(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 __xor__(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 __or__(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)
def __trunc__(self):
return Vector(math.trunc(self.x), math.trunc(self.y), math.trunc(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(
int(math.floor(self.x)),
int(math.floor(self.y)),
int(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(
int(math.ceil(self.x)),
int(math.ceil(self.y)),
int(math.ceil(self.z)))
def round(self, ndigits=0):
"""
Return the vector with the rounded value of each component. This is
only useful for vectors containing floating point components::
>>> Vector(0.5, -0.5, 1.2)
Vector(1.0, -1.0, 1.0)
The *ndigits* argument operates as it does in the built-in
:func:`round` function, specifying the number of decimal (or integer)
places to round to.
"""
if ndigits <= 0:
return Vector(
int(round(self.x, ndigits)),
int(round(self.y, ndigits)),
int(round(self.z, ndigits)))
else:
return Vector(
round(self.x, ndigits),
round(self.y, ndigits),
round(self.z, ndigits))
[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(X)
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(X)
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(X)
1.0
.. _Pythagoras' theorem: http://en.wikipedia.org/wiki/Pythagorean_theorem
"""
return (other - self).magnitude
[docs] def angle_between(self, other):
"""
Returns the angle between this vector and the *other* vector on a plane
that contains both vectors. The result is measured in degrees. For
example::
>>> X.angle_between(Y)
90.0
>>> (X + Y).angle_between(X)
45.00000000000001
"""
return math.degrees(math.acos(self.unit.dot(other.unit)))
[docs] def project(self, other):
"""
Return the `scalar projection`_ of this vector onto the *other* vector.
This is a scalar indicating the length of this vector in the direction
of the *other* vector. For example::
>>> Vector(1, 2, 3).project(2 * Y)
2.0
>>> Vector(3, 4, 5).project(Vector(3, 4, 0))
5.0
.. _scalar projection: https://en.wikipedia.org/wiki/Scalar_projection
"""
return self.dot(other.unit)
[docs] def rotate(self, angle, about, origin=None):
"""
Return this vector after `rotation`_ of *angle* degrees about the line
passing through *origin* in the direction *about*. Origin defaults to
the vector 0, 0, 0. Hence, if this parameter is omitted this method
calculates rotation about the axis (through the origin) defined by
*about*. For example::
>>> Y.rotate(90, about=X)
Vector(x=0, y=6.123233995736766e-17, z=1.0)
>>> Vector(3, 4, 5).rotate(30, about=X, origin=10 * Y)
Vector(x=3.0, y=2.3038475772933684, z=1.330127018922194)
Information about rotation around arbitrary lines was obtained from
`Glenn Murray's informative site`_.
.. _rotation: https://en.wikipedia.org/wiki/Rotation_group_SO%283%29
.. _Glenn Murray's informative site: http://inside.mines.edu/fs_home/gmurray/ArbitraryAxisRotation/
"""
r = math.radians(angle)
sin = math.sin(r)
cos = math.cos(r)
x, y, z = self
if origin is None:
# Fast-paths: rotation about a specific unit axis
if about == X:
return Vector(x, y * cos - z * sin, y * sin + z * cos)
elif about == Y:
return Vector(z * sin + x * cos, y, z * cos - x * sin)
elif about == Z:
return Vector(x * cos - y * sin, x * sin + y * cos, z)
elif about == negX:
return Vector(x, y * cos + z * sin, z * cos - y * sin)
elif about == negY:
return Vector(x * cos - z * sin, y, z * cos + x * sin)
elif about == negZ:
return Vector(x * cos + y * sin, y * cos - x * sin, z)
# Rotation about an arbitrary axis
u, v, w = about.unit
return Vector(
u * (u * x + v * y + w * z) * (1 - cos) + x * cos + (-w * y + v * z) * sin,
v * (u * x + v * y + w * z) * (1 - cos) + y * cos + ( w * x - u * z) * sin,
w * (u * x + v * y + w * z) * (1 - cos) + z * cos + (-v * x + u * y) * sin)
# Rotation about an arbitrary line
a, b, c = origin
u, v, w = about.unit
return Vector(
(a * (v ** 2 + w ** 2) - u * (b * v + c * w - u * x - v * y - w * z)) * (1 - cos) + x * cos + (-c * v + b * w - w * y + v * z) * sin,
(b * (u ** 2 + w ** 2) - v * (a * u + c * w - u * x - v * y - w * z)) * (1 - cos) + y * cos + ( c * u - a * w + w * x - u * z) * sin,
(c * (u ** 2 + v ** 2) - w * (a * u + b * v - u * x - v * y - w * z)) * (1 - cos) + z * cos + (-b * u + a * v - v * x + u * y) * sin)
@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::
>>> X.unit
Vector(x=1.0, y=0.0, z=0.0)
>>> (2 * Y).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
"""
try:
return self / self.magnitude
except ZeroDivisionError:
return self
# Short-hand variants
V = Vector
O = V()
X = V(x=1)
Y = V(y=1)
Z = V(z=1)
# These aren't exposed as short-hands; they're only pre-calculated here to
# speed up the fast-paths in the rotate() method
negX = V(x=-1)
negY = V(y=-1)
negZ = V(z=-1)
# 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
:func:`list` or :func:`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.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 :ref:`world.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 affect 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)
def sign(v):
"""
Returns the sign of v as -1, 0, or 1; works for scalar values or
:class:`Vector` instances.
"""
try:
return Vector(sign(v.x), sign(v.y), sign(v.z))
except AttributeError:
return 1 if v > 0 else -1 if v < 0 else 0
[docs]def line(start, end):
"""
Generates the coordinates of a line joining the *start* and *end*
:class:`Vector` instances inclusive. This is a generator function; points
are yielded from *start*, proceeding to *end*. If you don't require all
points you may terminate the generator at any point.
For example::
>>> list(line(O, V(10, 5, 0)))
[Vector(x=0, y=0, z=0),
Vector(x=1, y=1, z=0),
Vector(x=2, y=1, z=0),
Vector(x=3, y=2, z=0),
Vector(x=4, y=2, z=0),
Vector(x=5, y=3, z=0),
Vector(x=6, y=3, z=0),
Vector(x=7, y=4, z=0),
Vector(x=8, y=4, z=0),
Vector(x=9, y=5, z=0),
Vector(x=10, y=5, z=0)]
To draw the resulting line you can simply assign a block to the collection
of vectors generated (or assign a sequence of blocks of equal length if you
want the line to have varying block types)::
>>> world.blocks[line(O, V(10, 5, 0))] = Block('stone')
This is a three-dimensional implementation of `Bresenham's line
algorithm`_, derived largely from `Bob Pendelton's implementation`_ (public
domain).
.. _Bresenham's line algorithm: https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
.. _Bob Pendelton's implementation: ftp://ftp.isc.org/pub/usenet/comp.sources.unix/volume26/line3d
"""
delta = end - start
# Calculate the amount to increment each axis by; only the dominant axis
# will advance by this amount on *every* iteration. Other axes will only
# increment when the error demands it
pos_inc = sign(delta)
# Set up a vector containing the error incrementor. This will be added to
# values tracking the axis error on each iteration
error_inc = abs(delta) << 1
# Calculate the subordinate and dominant axes. The dominant axis is simply
# the one in which we must move furthest
sub_axis1, sub_axis2, dominant_axis = sorted(
'xyz', key=lambda axis: getattr(error_inc, axis))
# Set up the error decrementor. This will be subtracted from the error
# values when they turn positive (indicating that the corresponding axis
# should advance)
error_dec = getattr(error_inc, dominant_axis)
# Set up a vector to track the error (this is only really required for the
# subordinate axes)
error = error_inc - (error_dec >> 1)
# Convert vectors to dicts for the remainder of the algorithm
error = error._asdict()
error_inc = error_inc._asdict()
pos_inc = pos_inc._asdict()
pos = start._asdict()
end = getattr(end, dominant_axis)
while True:
yield Vector(**pos)
if pos[dominant_axis] == end:
break
pos[dominant_axis] += pos_inc[dominant_axis]
if error[sub_axis1] >= 0:
pos[sub_axis1] += pos_inc[sub_axis1]
error[sub_axis1] -= error_dec
error[sub_axis1] += error_inc[sub_axis1]
if error[sub_axis2] >= 0:
pos[sub_axis2] += pos_inc[sub_axis2]
error[sub_axis2] -= error_dec
error[sub_axis2] += error_inc[sub_axis2]
[docs]def lines(points, closed=True):
"""
Generator function which extends the :func:`line` function; this yields all
vectors necessary to render the lines connecting the specified *points*
(which is an iterable of :class:`Vector` instances).
If the optional *closed* parameter is ``True`` (the default) the last point
in the *points* sequence will be connected to the first point. Otherwise,
the lines will be left disconnected (assuming the last point is not
coincident with the first). For example::
>>> points = [O, 4*X, 4*Z]
>>> list(lines(points))
[Vector(x=0, y=0, z=0),
Vector(x=1, y=0, z=0),
Vector(x=2, y=0, z=0),
Vector(x=3, y=0, z=0),
Vector(x=4, y=0, z=0),
Vector(x=3, y=0, z=1),
Vector(x=2, y=0, z=2),
Vector(x=1, y=0, z=3),
Vector(x=0, y=0, z=4),
Vector(x=0, y=0, z=3),
Vector(x=0, y=0, z=2),
Vector(x=0, y=0, z=1),
Vector(x=0, y=0, z=0)]
To draw the resulting polygon you can simply assign a block to the
collection of vectors generated (or assign a sequence of blocks of equal
length if you want the polygon to have varying block types)::
>>> world.blocks[lines(points)] = Block('stone')
To generate the coordinates of a filled polygon, see the :func:`filled`
function.
"""
first = None
start = None
point = None
for point in points:
if start is None:
first = point
yield first
else:
for v in islice(line(start, point), 1, None):
yield v
start = point
if point is None:
raise ValueError('no points specified')
if closed and first != point:
for v in islice(line(point, first), 1, None):
yield v
[docs]def circle(center, radius, plane=Y):
"""
Generator function which yields the coordinates of a three-dimensional
circle centered at the :class:`Vector` *center*. The *radius* parameter is
a vector specifying the distance of the circumference from the center. The
optional *plane* parameter (which defaults to the Y unit vector) specifies
another vector which, in combination with the *radius* vector, gives the
plane that the circle exists within.
For example, to generate the coordinates of a circle centered at (0, 10,
0), with a radius of 5 units, existing in the X-Y plane::
>>> list(circle(O, 5*X))
[Vector(x=-5, y=0, z=0), Vector(x=-5, y=1, z=0), Vector(x=-4, y=2, z=0),
Vector(x=-4, y=3, z=0), Vector(x=-5, y=-1, z=0), Vector(x=-4, y=-2, z=0),
Vector(x=-4, y=-3, z=0), Vector(x=-3, y=4, z=0), Vector(x=-3, y=-4, z=0),
Vector(x=-2, y=4, z=0), Vector(x=-2, y=-4, z=0), Vector(x=-1, y=4, z=0),
Vector(x=-1, y=-4, z=0), Vector(x=0, y=5, z=0), Vector(x=0, y=-5, z=0),
Vector(x=1, y=4, z=0), Vector(x=1, y=-4, z=0), Vector(x=2, y=4, z=0),
Vector(x=2, y=-4, z=0), Vector(x=3, y=4, z=0), Vector(x=3, y=-4, z=0),
Vector(x=4, y=3, z=0), Vector(x=4, y=-3, z=0), Vector(x=4, y=2, z=0),
Vector(x=5, y=1, z=0), Vector(x=5, y=0, z=0), Vector(x=4, y=-2, z=0),
Vector(x=5, y=-1, z=0)]
To generate another set of coordinates with the same center and radius, but
existing in the X-Z (ground) plane::
>>> list(circle(O, 5*X, plane=Z))
[Vector(x=-5, y=0, z=0), Vector(x=-5, y=0, z=1), Vector(x=-4, y=0, z=2),
Vector(x=-4, y=0, z=3), Vector(x=-5, y=0, z=-1), Vector(x=-4, y=0, z=-2),
Vector(x=-4, y=0, z=-3), Vector(x=-3, y=0, z=4), Vector(x=-3, y=0, z=-4),
Vector(x=-2, y=0, z=4), Vector(x=-2, y=0, z=-4), Vector(x=-1, y=0, z=4),
Vector(x=-1, y=0, z=-4), Vector(x=0, y=0, z=5), Vector(x=0, y=0, z=-5),
Vector(x=1, y=0, z=4), Vector(x=1, y=0, z=-4), Vector(x=2, y=0, z=4),
Vector(x=2, y=0, z=-4), Vector(x=3, y=0, z=4), Vector(x=3, y=0, z=-4),
Vector(x=4, y=0, z=3), Vector(x=4, y=0, z=-3), Vector(x=4, y=0, z=2),
Vector(x=5, y=0, z=1), Vector(x=5, y=0, z=0), Vector(x=4, y=0, z=-2),
Vector(x=5, y=0, z=-1)]
To draw the resulting circle you can simply assign a block to the
collection of vectors generated (or assign a sequence of blocks of equal
length if you want the circle to have varying block types)::
>>> world.blocks[circle(O, 5*X)] = Block('stone')
The algorithm used by this function is based on a straight-forward
differences of roots method, extended to three dimensions. This produces
`worse looking`_ circles than the `midpoint circle algorithm`_ (also known
as a the Bresenham circle algorithm), but isn't restricted to working in a
simple cartesian plane.
.. note::
If you know of a three dimensional generalization of the midpoint
circle algorithm (which handles entirely arbitrary planes, not merely
simple X-Y, X-Z, etc. planes), please contact the `author`_!
To create a filled circle, see the :func:`filled` function.
.. _worse looking: https://sites.google.com/site/ruslancray/lab/projects/bresenhamscircleellipsedrawingalgorithm/bresenham-s-circle-ellipse-drawing-algorithm
.. _midpoint circle algorithm: https://en.wikipedia.org/wiki/Midpoint_circle_algorithm
.. _author: mailto:dave@waveform.org.uk
"""
try:
if radius.angle_between(plane) != 90:
plane = radius.cross(-(radius.cross(plane)))
except AttributeError:
raise ValueError('radius must be a Vector instance')
perp = plane.unit
r = radius.magnitude**2
last_points = None
result = set()
result_len = 0
for radial_point in line(-radius, radius):
circum_v = (perp * math.sqrt(r - radial_point.magnitude**2)).floor()
top_point = (radial_point + circum_v)
bottom_point = (radial_point - circum_v)
if last_points is not None:
top_last, bottom_last = last_points
for p in line(top_last, top_point):
p += center
result.add(p)
if len(result) > result_len:
result_len += 1
yield p
for p in line(bottom_last, bottom_point):
p += center
result.add(p)
if len(result) > result_len:
result_len += 1
yield p
last_points = top_point, bottom_point
[docs]def sphere(center, radius):
"""
Generator function which yields the coordinates of a hollow sphere. The
*center* :class:`Vector` specifies the center of the sphere, and *radius*
is a scalar number of blocks giving the distance from the center to the
edge of the sphere.
For example to create the coordinates of a sphere centered at the origin
with a radius of 5 units::
>>> list(sphere(O, 5))
To draw the resulting sphere you can simply assign a block to the
collection of vectors generated (or assign a sequence of blocks of equal
length if you want the sphere to have varying block types)::
>>> world.blocks[sphere(O, 5)] = Block('stone')
The algorithm generates concentric circles covering the sphere's surface,
advancing along the X, Y, and Z axes with duplicate elimination to prevent
repeated coordinates being yielded. Three axes are required to eliminate
gaps in the surface.
"""
r = radius**2
last_points = None
result = set()
result_len = 0
for radial_point in range(-radius, radius + 1):
circum_dist = int(round(math.sqrt(r - radial_point**2)))
for p in circle(X * radial_point, Z * circum_dist):
p += center
result.add(p)
if len(result) > result_len:
result_len += 1
yield p
for p in circle(Y * radial_point, Z * circum_dist, plane=X):
p += center
result.add(p)
if len(result) > result_len:
result_len += 1
yield p
for p in circle(Z * radial_point, X * circum_dist):
p += center
result.add(p)
if len(result) > result_len:
result_len += 1
yield p
def pairwise(iterable):
"s -> (s0,s1), (s1,s2), (s2, s3), ..."
a, b = tee(iterable)
next(b, None)
return zip(a, b)
[docs]def filled(points):
"""
Generator function which yields the coordinates necessary to fill the space
enclosed by the specified *points*.
This function can be applied to anything that returns a sequence of points.
For example, to create a filled triangle::
>>> triangle = [O, 4*X, 4*Z]
>>> list(filled(lines(triangle)))
[Vector(x=0, y=0, z=0), Vector(x=0, y=0, z=1), Vector(x=0, y=0, z=2),
Vector(x=0, y=0, z=3), Vector(x=0, y=0, z=4), Vector(x=1, y=0, z=2),
Vector(x=1, y=0, z=1), Vector(x=1, y=0, z=0), Vector(x=1, y=0, z=3),
Vector(x=2, y=0, z=1), Vector(x=2, y=0, z=0), Vector(x=2, y=0, z=2),
Vector(x=3, y=0, z=1), Vector(x=3, y=0, z=0), Vector(x=4, y=0, z=0)]
Or to create a filled circle::
>>> list(filled(circle(O, 4*X)))
[Vector(x=-4, y=0, z=0), Vector(x=-3, y=-1, z=0), Vector(x=-3, y=-2, z=0),
Vector(x=-3, y=0, z=0), Vector(x=-3, y=1, z=0), Vector(x=-3, y=2, z=0),
Vector(x=-2, y=-1, z=0), Vector(x=-2, y=-2, z=0), Vector(x=-2, y=-3, z=0),
Vector(x=-2, y=0, z=0), Vector(x=-2, y=1, z=0), Vector(x=-2, y=2, z=0),
Vector(x=-2, y=3, z=0), Vector(x=-1, y=0, z=0), Vector(x=-1, y=-1, z=0),
Vector(x=-1, y=-2, z=0), Vector(x=-1, y=-3, z=0), Vector(x=-1, y=1, z=0),
Vector(x=-1, y=2, z=0), Vector(x=-1, y=3, z=0), Vector(x=0, y=-1, z=0),
Vector(x=0, y=-2, z=0), Vector(x=0, y=-3, z=0), Vector(x=0, y=-4, z=0),
Vector(x=0, y=0, z=0), Vector(x=0, y=1, z=0), Vector(x=0, y=2, z=0),
Vector(x=0, y=3, z=0), Vector(x=0, y=4, z=0), Vector(x=1, y=0, z=0),
Vector(x=1, y=-1, z=0), Vector(x=1, y=-2, z=0), Vector(x=1, y=-3, z=0),
Vector(x=1, y=1, z=0), Vector(x=1, y=2, z=0), Vector(x=1, y=3, z=0),
Vector(x=2, y=0, z=0), Vector(x=2, y=-1, z=0), Vector(x=2, y=-2, z=0),
Vector(x=2, y=-3, z=0), Vector(x=2, y=1, z=0), Vector(x=2, y=2, z=0),
Vector(x=2, y=3, z=0), Vector(x=3, y=0, z=0), Vector(x=3, y=-1, z=0),
Vector(x=3, y=-2, z=0), Vector(x=3, y=1, z=0), Vector(x=3, y=2, z=0),
Vector(x=4, y=0, z=0), Vector(x=4, y=-1, z=0), Vector(x=4, y=1, z=0)]
To draw the resulting filled object you can simply assign a block to the
collection of vectors generated (or assign a sequence of blocks of equal
length if you want the object to have varying block types)::
>>> world.blocks[filled(lines(triangle))] = Block('stone')
A simple brute-force algorithm is used that simply generates all the lines
connecting all specified points. However, duplicate elimination is used to
ensure that no point within the filled space is yielded twice.
Note that if you pass the coordinates of a polyhedron which contains holes
or gaps compared to its convex hull, this function *may* fill those holes
or gaps (but it will depend on the orientation of the object).
"""
result = set()
result_len = 0
for p, q in pairwise(sorted(points)):
for l in line(p, q):
result.add(l)
if len(result) > result_len:
result_len += 1
yield l