Source code for picraft.vector

# 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
"""

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
except ImportError:
    # Py2 compat
    from itertools import izip_longest as zip_longest, islice


[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) Shortcuts are available for 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 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 .. 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 """ 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)) @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. """ 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 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) 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): """ A three-dimensional implementation of `Bresenham's line algorithm`_, derived largely from `Bob Pendelton's implementation`_ (public domain). Given the end points of the line as the *start* and *end* vectors, this generator function yields the coordinate of each block (inclusive of the *start* and *end* vectors) that should be filled in to render the line. 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)] .. _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): """ Extension of the :func:`line` function which returns 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)] """ first = None start = 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 closed and first != point: for v in islice(line(point, first), 1, None): yield v
def circle(center, radius, plane=Y): """ Generates the coordinates of a three-dimensional circle centered at the vector *center*. The *radius* parameter is a vector specifying the distance of the circumference from the center. The optional *plane* parameter specifies another vector which, in combination with the *radius* vector, gives the plane that the circle exists within. For example, to create a circle centered at (0, 10, 0), with a radius of 5 units, existing in the X-Y plane:: >>> circle(10*Y, 5*X, Y) To create another circle with the same center and radius, but existing in the X-Z (ground) plane:: >>> circle(10*Y, 5*X, Z) The algorithm used by this function is based on the `differences of roots`_ method, extended to three dimensions. """ if radius.angle_between(plane) != 90: plane = radius.cross(-(radius.cross(plane))) perp = plane.unit r = radius.magnitude**2 last_points = None result = set() 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: for p in line(last_points[0], top_point): result.add(center + p) for p in line(last_points[1], bottom_point): result.add(center + p) last_points = (top_point, bottom_point) return result