11. API - Vector, vector_range, etc.

The vector module defines the Vector class, which is the usual method of representing coordinates or vectors when dealing with the Minecraft world. It also provides functions like vector_range() for generating sequences of vectors.

Note

All items in this module are available from the picraft namespace without having to import picraft.vector directly.

The following items are defined in the module:

11.1. Vector

class picraft.vector.Vector(x=0, y=0, z=0)[source]

Represents a 3-dimensional vector.

This namedtuple() 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 them 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 namedtuple(), 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 members of a set or keys in a 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.
replace(x=None, y=None, z=None)[source]

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)
ceil()[source]

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)
floor()[source]

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)
dot(other)[source]

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
cross(other)[source]

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)
distance_to(other)[source]

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
>>> O.distance_to(X)
1.0
angle_between(other)[source]

Returns the angle between this vector and the other vector on a plane that contains both vectors. The result is measured in degrees between 0 and 180. For example:

>>> X.angle_between(Y)
90.0
>>> (X + Y).angle_between(X)
45.00000000000001
project(other)[source]

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
rotate(angle, about, origin=None)[source]

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.

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.

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.

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).

magnitude

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
unit

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.

11.2. Short-hand variants

The 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)

11.3. vector_range

class picraft.vector.vector_range(start, stop=None, step=None, order=u'zxy')[source]

Like range(), vector_range is actually a type which efficiently represents a range of vectors. The arguments to the constructor must be 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, 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 list() or 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 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 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 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.

count(value)[source]

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).

index(value)[source]

Return the zero-based index of value within the range, or raise ValueError if value does not exist in the range.

11.4. line

picraft.vector.line(start, end)[source]

Generates the coordinates of a line joining the start and end 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).

11.5. lines

picraft.vector.lines(points, closed=True)[source]

Generator function which extends the line() function; this yields all vectors necessary to render the lines connecting the specified points (which is an iterable of 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 filled() function.

11.6. circle

picraft.vector.circle(center, radius, plane=Vector(x=0, y=1, z=0))[source]

Generator function which yields 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 (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), ...]

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), ...]

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 the midpoint circle algorithm (also known as a the Bresenham circle algorithm), but isn’t restricted to working in a simple cartesian plane. It does compute the Bresenham circle on the X-Y plane and then determines the rotation required to move the Z axis into the normal generated by the radius vector and the plane vector

To create a filled circle, see the filled() function.

11.7. sphere

picraft.vector.sphere(center, radius)[source]

Generator function which yields the coordinates of a hollow sphere. The center 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.

11.8. filled

picraft.vector.filled(points)[source]

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).