Spatial (3D) Math

Math utility library for common three-dimensional constructs:

The pint library can be used to specify dimensions:

>>> from petrify import u
>>> p = Point(50, 25, 50) * u.mm
>>> p.to('m')
<Quantity(Point(0.05, 0.025, 0.05), 'meter')>

Many methods are nominally supported when wrapped with pint. We recommend you only use units when exporting and importing data, and pick a canonical unit for all petrify operations.

Big thanks to pyeuclid, the source of most of the code here.

Note

These examples and this library make heavy use of the tau constant for rotational math instead of Pi. Read why at the Tau Manifesto.

class petrify.space.Basis(origin, bx, by)[source]

Bases: object

Embeds a two-dimensional space into a three-dimensional space:

>>> basis = Basis(Point(1, 0, 0), Vector.basis.y, Vector.basis.z)
>>> basis.project(Point2(2, 3))
Point(1, 2, 3)
>>> basis.project(Vector2(-2, -3))
Vector(1, -2, -3)

Can be translated:

>>> translated = basis.xy + Vector(0, 0, 2)
>>> translated
Basis(Point(0, 0, 2), Vector(1, 0, 0), Vector(0, 1, 0))
>>> translated.project(Point2(2, 3))
Point(2, 3, 2)

Note

Any given Plane has an infinite number of associated Basis constructions.

There are special Basis objects for commonly used bases:

>>> Basis.unit
Basis(Point(0, 0, 0), Vector(1, 0, 0), Vector(0, 1, 0))
>>> Basis.xy
Basis(Point(0, 0, 0), Vector(1, 0, 0), Vector(0, 1, 0))
>>> Basis.yz
Basis(Point(0, 0, 0), Vector(0, 1, 0), Vector(0, 0, 1))
>>> Basis.xz
Basis(Point(0, 0, 0), Vector(1, 0, 0), Vector(0, 0, 1))
grid(ticks, count)[source]

Returns a visual petrify.visualize.Grid of the projected space.

class petrify.space.Face(basis, direction, polygon)[source]

Bases: petrify.space.PlanarPolygon

A PlanarPolygon with an associated polarity. Face.Positive polarity follows the right hand rule, Face.Negative is inverted.

>>> tri= Polygon2([Point2(0, 0), Point2(0, 2), Point2(1, 1)])
>>> triangle = Face(Basis.xy, Face.Positive, tri)
petrify.space.Line

alias of petrify.space.Line3

class petrify.space.Line3(*args)[source]

Bases: petrify.space.util.Spatial

An infinite line:

>>> Line(Point(0, 0, 0), Vector(1, 1, 1))
Line(Point(0, 0, 0), Vector(1, 1, 1))
>>> Line(Point(0, 0, 0), Point(1, 1, 1))
Line(Point(0, 0, 0), Vector(1, 1, 1))
connect(other)[source]

Find the shortest line segment connecting this object to the other object.

intersect(other)[source]

Find the point where this line intersects the other plane or sphere:

>>> l = Line(Point(0, 0, 0), Vector(1, 1, 1));
>>> p = Plane(Vector(0, 0, 1), 2);
>>> l.intersect(p)
Point(2.0, 2.0, 2.0)
petrify.space.LineSegment

alias of petrify.space.LineSegment3

class petrify.space.LineSegment3(*args)[source]

Bases: petrify.space.Line3

petrify.space.Matrix

alias of petrify.space.transform.Matrix3

class petrify.space.Matrix3[source]

Bases: object

A matrix that can be used to perform common transformations on three-dimensional points and vectors:

>>> from . import Point, Vector
>>> Matrix3.scale(*Vector(1, 2, 1).xyz) * Point(1, 1, 1)
Point(1, 2, 1)
get_quaternion()[source]

Returns a quaternion representing the rotation part of the matrix.

classmethod identity()[source]

The identity transform:

>>> from . import Point
>>> Matrix3.identity() * Point(1, 1, 1)
Point(1.0, 1.0, 1.0)
classmethod new(*values)[source]

Create a new matrix from the provided values array.

classmethod rotate_at(origin, axis, angle)[source]

A rotational transform:

>>> from . import Point, Vector
>>> rotation = Matrix3.rotate_at(Point(1, 1, 1), Vector.basis.z, tau / 4)
>>> (rotation * Point(2, 1, 1)).rounded()
Point(1, 2, 1)
classmethod rotate_axis(axis, angle)[source]

A rotational transform:

>>> from . import Point, Vector
>>> (Matrix3.rotate_axis(Vector.basis.z, tau / 4) * Point(1, 0, 0)).rounded()
Point(0, 1, 0)
classmethod scale(x, y, z)[source]

A scale transform:

>>> from . import Point, Vector
>>> Matrix3.scale(*Vector(1, 2, 1).xyz) * Point(1, 1, 1)
Point(1, 2, 1)
classmethod translate(x, y, z)[source]

A translation transform:

>>> from . import Point, Vector
>>> Matrix3.translate(*Vector(1, 2, 1).xyz) * Point(1, 1, 1)
Point(2.0, 3.0, 2.0)
class petrify.space.PlanarPolygon(basis, polygon)[source]

Bases: object

A two-dimensional Polygon2 or ComplexPolygon2 embedded in three-dimensional space via a Basis:

>>> tri = plane.Polygon2([
...     plane.Point2(0, 0),
...     plane.Point2(0, 2),
...     plane.Point2(1, 1)
... ])
>>> triangle = PlanarPolygon(Basis.xy, tri)
>>> triangle.project()
[Polygon([Point(0, 0, 0), Point(0, 2, 0), Point(1, 1, 0)])]
render()[source]

Visualize this polygon in a Jupyter notebook.

class petrify.space.Plane(*args)[source]

Bases: object

A three-dimensional plane.

Can be constructed with three coplanar points:

>>> Plane(Point(0, 0, 0), Point(1, 0, 0), Point(0, 1, 0))
Plane(Vector(0.0, 0.0, 1.0), 0.0)

Or an origin point and two basis vectors: >>> Plane(Point(0, 0, 0), Vector.basis.x, Vector.basis.y) Plane(Vector(0.0, 0.0, 1.0), 0.0)

Or a normal and solution scalar / point:

>>> Plane(Vector.basis.z, 0)
Plane(Vector(0.0, 0.0, 1.0), 0)
>>> Plane(Vector.basis.z, Point.origin)
Plane(Vector(0.0, 0.0, 1.0), 0.0)

Plane also defines convenience methods for commonly used origin planes:

>>> Plane.xy
Plane(Vector(0.0, 0.0, 1.0), 0.0)
>>> Plane.xz
Plane(Vector(0.0, 1.0, 0.0), 0.0)
>>> Plane.yz
Plane(Vector(1.0, 0.0, 0.0), 0.0)
connect(other)[source]

Find the shortest line segment connecting this object to the other object.

intersect(other)[source]

Find the point where this plane intersects the other line or plane:

>>> Plane(Vector(0, 1, 0), 1).intersect(Plane(Vector(1, 0, 0), 2))
Line(Point(2.0, 1.0, 0.0), Vector(0.0, 0.0, 1.0))
>>> Plane(Vector(0, 0, 1), 2).intersect(Line(Point(0, 0, 0), Vector(1, 1, 1)))
Point(2.0, 2.0, 2.0)
petrify.space.Point

alias of petrify.space.point.Point3

class petrify.space.Point2(x=0, y=0)[source]

Bases: petrify.plane.point.Vector2, petrify.geometry.Geometry, petrify.generic.Point

A close cousin of Vector2 used to represent points:

>>> Point(1, 1) + Vector(2, 3)
Point(3, 4)
>>> Point(1, 2) * 2
Point(2, 4)
connect(other)[source]

Connects this point to the other given geometry:

>>> from petrify.plane import Circle, Line
>>> Point(1, 1).connect(Line(Point(0, 0), Vector(1, 0)))
LineSegment(Point(1, 1), Point(1.0, 0.0))
>>> Point(1, 1).connect(Point(0, 0))
LineSegment(Point(1, 1), Point(0, 0))
>>> Point(0, 2).connect(Circle(Point(0, 0), 1))
LineSegment(Point(0, 2), Point(0.0, 1.0))
intersect(other)[source]

Used to determine if this point is within a circle:

>>> from petrify.plane import Circle
>>> Point(1, 1).intersect(Circle(Point(0, 0), 2))
True
>>> Point(3, 3).intersect(Circle(Point(0, 0), 2))
False
class petrify.space.Point3(x=0, y=0, z=0)[source]

Bases: petrify.space.point.Vector3, petrify.generic.Point, petrify.geometry.Geometry

A close cousin of Vector, used to represent a point instead of a transform:

>>> Point(1, 2, 3) + Vector(1, 1, 1)
Point(2, 3, 4)

Defines a convenience .origin attribute for this commonly-used point:

>>> Point.origin
Point(0, 0, 0)
connect(other)[source]

Find the shortest line segment connecting this object to the other object.

intersect(other)[source]

Returns whether the point lies within the given other sphere:

vector()[source]

The vector formed from the origin to this point.

petrify.space.Polygon

alias of petrify.space.Polygon3

class petrify.space.Polygon2(points)[source]

Bases: petrify.geometry.AbstractPolygon, petrify.plane.planar.Planar

A two-dimensional polygon:

>>> Polygon([Point(2, 0), Point(0, 0), Point(1, 1)])
Polygon([Point(2, 0), Point(0, 0), Point(1, 1)])

Supports scaling and translation:

>>> tri = Polygon([Point(2, 0), Point(0, 0), Point(1, 1)])
>>> tri * Vector(2, 3)
Polygon([Point(4, 0), Point(0, 0), Point(2, 3)])
>>> tri * Matrix2.scale(2, 3)
Polygon([Point(4, 0), Point(0, 0), Point(2, 3)])
>>> tri + Vector(2, 1)
Polygon([Point(4, 1), Point(2, 1), Point(3, 2)])
>>> tri - Vector(1, 1)
Polygon([Point(1, -1), Point(-1, -1), Point(0, 0)])
>>> len(tri)
3
PointsConstructor

alias of Polygon2

centered(point)[source]

Center this polygon at a given point:

>>> from petrify.shape import Rectangle
>>> Rectangle(Point(0, 0), Vector(2, 2)).centered(Point(3, 3))
Polygon([Point(2.0, 2.0), Point(2.0, 4.0), Point(4.0, 4.0), Point(4.0, 2.0)])
clockwise()[source]

Returns True if the points in this polygon are in clockwise order:

>>> Polygon([Point(2, 0), Point(0, 0), Point(1, 1)]).clockwise()
True
>>> Polygon([Point(1, 1), Point(0, 0), Point(2, 0)]).clockwise()
False
contains(p)[source]

Tests whether a point lies within this polygon:

>>> tri = Polygon([Point(2, 0), Point(0, 0), Point(1, 1)])
>>> tri.contains(Point(1.0, 0.5))
True
>>> tri.contains(Point(0.5, 1.5))
False
envelope()[source]

Returns the bounding Rectangle around this polygon:

>>> tri = Polygon([Point(2, 0), Point(0, 0), Point(1, 1)])
>>> tri.envelope()
Rectangle(Point(0, 0), Vector(2, 1))
index_of(point)[source]

Finds index of given point:

>>> Polygon([Point(0, 0), Point(1, 0), Point(1, 1)]).index_of(Point(1, 1))
2
inverted()[source]

Reverse the points in this polygon:

>>> tri = Polygon([Point(2, 0), Point(0, 0), Point(1, 1)])
>>> tri.inverted()
Polygon([Point(1, 1), Point(0, 0), Point(2, 0)])
inwards(edge)[source]

Finds the normalized Ray2 facing inwards for a given edge:

>>> tri = Polygon([Point(2, 0), Point(0, 0), Point(1, 1)])
>>> tri.inwards(tri.segments()[0])
Vector(0.0, 1.0)
is_convex()[source]

Return True if the polynomial defined by the sequence of 2D points is ‘strictly convex’:

>>> tri = Polygon([Point(2, 0), Point(0, 0), Point(1, 1)])
>>> tri.is_convex()
True
>>> indent = Polygon([
...     Point(0, 0),
...     Point(10, 0),
...     Point(5, 5),
...     Point(10, 10),
...     Point(0, 10)
... ])
>>> indent.is_convex()
False

Note

“strictly convex” is defined as follows:

  • points are valid
  • side lengths are non-zero
  • interior angles are strictly between zero and a straight angle
  • the polygon does not intersect itself
offset(amount)[source]

Finds the dynamic offset of a polygon by moving all edges by a given amount perpendicular to their direction:

>>> square = Polygon([Point(0, 0), Point(0, 1), Point(1, 1), Point(1, 0)])
>>> square.offset(-0.1).exterior[0]
Polygon([Point(0.1, 0.1), Point(0.1, 0.9), Point(0.9, 0.9), Point(0.9, 0.1)])
>>> square.offset(0.1).exterior[0]
Polygon([Point(-0.1, -0.1), Point(-0.1, 1.1), Point(1.1, 1.1), Point(1.1, -0.1)])
>>> square.offset(-10)
ComplexPolygon([])

Note

This always returns a ComplexPolygon. Collisions during the offset process can subdivide the polygon.

shift(n)[source]

Shift the points in this polygon by n:

>>> tri = Polygon([Point(2, 0), Point(0, 0), Point(1, 1)])
>>> tri.shift(1)
Polygon([Point(0, 0), Point(1, 1), Point(2, 0)])
simplify(tolerance=0.0001)[source]

Remove any duplicate points, within a certain tolerance:

>>> Polygon([Point(1, 1), Point(2, 0), Point(0, 0), Point(1, 1)]).simplify()
Polygon([Point(2, 0), Point(0, 0), Point(1, 1)])

Returns None if the resulting simplification would create a point:

>>> Polygon([
...     Point(1, 1),
...     Point(2, 0),
...     Point(0, 0),
...     Point(1, 1)
... ]).simplify(100) is None
True
to_clockwise()[source]

Converts this polygon to a clockwise one if necessary:

>>> Polygon([Point(2, 0), Point(0, 0), Point(1, 1)]).to_clockwise()
Polygon([Point(2, 0), Point(0, 0), Point(1, 1)])
>>> Polygon([Point(1, 1), Point(0, 0), Point(2, 0)]).to_clockwise()
Polygon([Point(2, 0), Point(0, 0), Point(1, 1)])
to_counterclockwise()[source]

Converts this polygon to a clockwise one if necessary:

>>> Polygon([Point(2, 0), Point(0, 0), Point(1, 1)]).to_counterclockwise()
Polygon([Point(1, 1), Point(0, 0), Point(2, 0)])
>>> Polygon([Point(1, 1), Point(0, 0), Point(2, 0)]).to_counterclockwise()
Polygon([Point(1, 1), Point(0, 0), Point(2, 0)])
class petrify.space.Polygon3(points)[source]

Bases: petrify.geometry.AbstractPolygon, petrify.space.util.Spatial

A linear cycle of coplanar convex points:

>>> tri= Polygon([Point(0, 0, 0), Point(0, 2, 0), Point(1, 1, 0)])
>>> tri.plane
Plane(Vector(0.0, 0.0, -1.0), 0.0)
>>> tri * Vector(1, 2, 3)
Polygon([Point(0, 0, 0), Point(0, 4, 0), Point(1, 2, 0)])
>>> tri * Matrix3.scale(1, 2, 3)
Polygon([Point(0, 0, 0), Point(0, 4, 0), Point(1, 2, 0)])
>>> tri + Vector(1, 2, 1)
Polygon([Point(1.0, 2.0, 1.0), Point(1.0, 4.0, 1.0), Point(2.0, 3.0, 1.0)])
>>> len(tri)
3
PointsConstructor

alias of Polygon3

has_edge(edge)[source]

Returns true if this polygon contains the given edge.

index_of(point)[source]

Finds index of given point:

>>> p = Polygon([Point(0, 0, 0), Point(1, 0, 0), Point(1, 1, 0)])
>>> p.index_of(Point(1, 1))
2
inverted()[source]

Reverses the points on a given polygon:

>>> p = Polygon([Point(0, 0, 0), Point(1, 0, 0), Point(1, 1, 0)])
>>> p.inverted()
Polygon([Point(1, 1, 0), Point(1, 0, 0), Point(0, 0, 0)])
render()[source]

Visualize this polygon in a Jupyter notebook.

segments()[source]

Returns all line segments composing this polygon’s edges.

simplify(tolerance=0.0001)[source]

Remove any duplicate points, within a certain tolerance:

>>> Polygon([Point(1, 1, 0), Point(2, 0, 0), Point(0, 0, 0), Point(1, 1, 0)]).simplify()
Polygon([Point(2, 0, 0), Point(0, 0, 0), Point(1, 1, 0)])

Returns None if the resulting simplification would create a point:

>>> Polygon([Point(1, 1, 0), Point(2, 0, 0), Point(0, 0, 0)]).simplify(100) is None
True
class petrify.space.Quaternion(w=1, x=0, y=0, z=0)[source]

Bases: object

Quaternions are composable representations of three-dimensional rotation operations.

Multiplication can be performed on Vector instances to get the transformed vector or point:

>>> from . import Vector
>>> r = Quaternion.rotate_axis(Vector.basis.x, tau / 4);
>>> (r * Vector(0, 1, 0)).rounded()
Vector(0, 0, 1)
petrify.space.Ray

alias of petrify.space.Ray3

class petrify.space.Ray3(*args)[source]

Bases: petrify.space.Line3

A Line with a fixed origin that continues indefinitely in the given direction.

class petrify.space.Sphere(center, radius)[source]

Bases: object

A perfect sphere with the provided center and radius:

>>> Sphere(Point(0, 0, 0), 1.0)
Sphere(Point(0, 0, 0), 1.0)
connect(other)[source]

Find the shortest line segment connecting this object to the other object.

intersect(other)[source]

Checks whether the other point lies within this sphere.

petrify.space.Vector

alias of petrify.space.point.Vector3

class petrify.space.Vector2(x=0, y=0)[source]

Bases: petrify.generic.Concrete, petrify.generic.Vector, petrify.plane.planar.Planar

A two-dimensional vector supporting all corresponding built-in math operators:

>>> Vector(1, 2) + Vector(2, 2)
Vector(3, 4)
>>> Vector(1, 2) - Vector(2, 2)
Vector(-1, 0)
>>> Vector(1, 1) * 5
Vector(5, 5)
>>> Vector(1, 1) / 5
Vector(0.2, 0.2)
>>> Vector(1, 1) == Vector(1, 1)
True

In addition to many other specialized vector operations.

angle(other)[source]

Return the angle to the vector other:

>>> Vector(1, 0).angle(Vector(0, 1)) == tau / 4
True
dot(other)[source]

The dot product of this vector and other:

>>> Vector(2, 1).dot(Vector(2, 3))
7
normalized()[source]

Return a new vector normalized to unit length:

>>> Vector(0, 5).normalized()
Vector(0.0, 1.0)
project(other)[source]

Return one vector projected on the vector other

reflected(normal)[source]

Reflects this vector across a line with the given perpendicular normal:

>>> Vector(1, 1).reflected(Vector(0, 1))
Vector(1, -1)

Warning

Assumes normal is normalized (has unit length).

round(places)[source]

Rounds this vector to the given decimal place:

>>> Vector(1.00001, 1.00001).round(2)
Vector(1.0, 1.0)
snap(grid)[source]

Snaps this vector to a grid:

>>> Vector(1.15, 1.15).snap(0.25)
Vector(1.25, 1.25)
class petrify.space.Vector3(x=0, y=0, z=0)[source]

Bases: petrify.generic.Concrete, petrify.generic.Vector, petrify.space.util.Spatial

A three-dimensional vector supporting all corresponding built-in math operators:

>>> Vector(1, 2, 3) + Vector(2, 2, 2)
Vector(3, 4, 5)
>>> Vector(1, 2, 3) - Vector(2, 2, 2)
Vector(-1, 0, 1)
>>> Vector(1, 0, 1) * 5
Vector(5, 0, 5)
>>> Vector(1, 0, 1) / 5
Vector(0.2, 0.0, 0.2)
>>> Vector(1, 1, 1) == Vector(1, 1, 1)
True

In addition to many other specialized vector operations.

Defines convenience .basis members for commonly used basis vectors:

>>> Vector.basis.x; Vector.bx
Vector(1, 0, 0)
Vector(1, 0, 0)
>>> Vector.basis.y; Vector.by
Vector(0, 1, 0)
Vector(0, 1, 0)
>>> Vector.basis.z; Vector.bz
Vector(0, 0, 1)
Vector(0, 0, 1)
angle(other)[source]

Return the angle to the vector other.

cross(other)[source]

The cross product of this vector and the other.

dot(other)[source]

The dot product of this vector and the other.

normalized()[source]

Returns a vector with the same direction but unit (1) length:

>>> Vector(0, 0, 5).normalized()
Vector(0.0, 0.0, 1.0)
point()[source]

Convert this vector into a point.

project(other)[source]

Return one vector projected on the vector other.

reflect(normal)[source]

Reflect this vector across a plane with the given normal

Note

Assumes the given normal has unit (1) length.

rotate(axis, theta)[source]

Return a new vector rotated around axis by angle theta. Right hand rule applies.

rounded(place=None)[source]

Rounds all elements to place decimals.

snap(grid)[source]

Snaps this vector to a grid:

>>> Vector(1.15, 1.15, 0.9).snap(0.25)
Vector(1.25, 1.25, 1.0)
petrify.space.valid_scalar(v)[source]

Checks if v is a valid scalar value for the purposes of this library:

>>> valid_scalar(1)
True
>>> valid_scalar(1.0)
True
>>> valid_scalar('abc')
False