Plane (2D) Geometry

Math utility library for common two-dimensional constructs:

The pint library can be used to specify dimensions:

>>> from petrify import u
>>> p = Point(24, 12) * u.inches
>>> p.to(u.ft)
<Quantity(Point(2.0, 1.0), 'foot')>

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.

petrify.plane.ComplexPolygon

alias of petrify.plane.ComplexPolygon2

class petrify.plane.ComplexPolygon2(polygons=None, interior=None, exterior=None)[source]

Bases: object

Represents a complex polygon. A complex polygon is composed of one or more separate simple polygons, and may contain holes:

>>> from petrify.shape import Rectangle
>>> square = Rectangle(Point(0, 0), Vector(1, 1))
>>> complex = ComplexPolygon([square + Vector(1, 1), square * 3])
>>> len(complex)
8

Supports common built-in methods:

>>> square = Rectangle(Point(0, 0), Vector(1, 1))
>>> ComplexPolygon([square]) + Vector(1, 1)
ComplexPolygon([Polygon([Point(1, 1), Point(1, 2), Point(2, 2), Point(2, 1)])])
>>> ComplexPolygon([square]) - Vector(1, 1)
ComplexPolygon([Polygon([Point(-1, -1), Point(-1, 0), Point(0, 0), Point(0, -1)])])
centered(point)[source]

Center this polygon at a given point:

>>> from petrify.shape import Rectangle
>>> ComplexPolygon([
...   Rectangle(Point(0, 0), Vector(2, 2)),
...   Rectangle(Point(1, 1), Vector(1, 1)),
... ]).centered(Point(3, 3)).envelope()
Rectangle(Point(2.0, 2.0), Vector(2.0, 2.0))
envelope()[source]

Returns the bounding Rectangle around this polygon:

>>> square = Polygon([Point(0, 0), Point(0, 1), Point(1, 1), Point(1, 0)])
>>> complex = ComplexPolygon([square + Vector(1, 1), square * 3])
>>> complex.envelope()
Rectangle(Point(0, 0), Vector(3, 3))
offset(amount)[source]

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

Any inner polygons are offset in the reverse direction to the outer polygons:

>>> square = Polygon([Point(0, 0), Point(0, 1), Point(1, 1), Point(1, 0)])
>>> complex = ComplexPolygon([square + Vector(1, 1), square * 3])
>>> complex.offset(-0.1).interior
[Polygon([Point(0.9, 0.9), Point(0.9, 2.1), Point(2.1, 2.1), Point(2.1, 0.9)])]
>>> complex.offset(-0.1).exterior
[Polygon([Point(0.1, 0.1), Point(0.1, 2.9), Point(2.9, 2.9), Point(2.9, 0.1)])]
to_clockwise()[source]

Converts all sub-polygons to clockwise:

>>> tri = Polygon([Point(2, 0), Point(0, 0), Point(1, 1)])
>>> ComplexPolygon([tri]).to_clockwise()
ComplexPolygon([Polygon([Point(2, 0), Point(0, 0), Point(1, 1)])])
>>> ComplexPolygon([tri.inverted()]).to_clockwise()
ComplexPolygon([Polygon([Point(2, 0), Point(0, 0), Point(1, 1)])])
to_counterclockwise()[source]

Converts all sub-polygons to counter-clockwise:

>>> tri = Polygon([Point(2, 0), Point(0, 0), Point(1, 1)])
>>> ComplexPolygon([tri]).to_counterclockwise()
ComplexPolygon([Polygon([Point(1, 1), Point(0, 0), Point(2, 0)])])
>>> ComplexPolygon([tri.inverted()]).to_counterclockwise()
ComplexPolygon([Polygon([Point(1, 1), Point(0, 0), Point(2, 0)])])
petrify.plane.Line

alias of petrify.plane.line.Line2

class petrify.plane.Line2(*args)[source]

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

Represents an infinite line:

>>> Line(Point(0, 0), Vector(1, 1))
Line(Point(0, 0), Vector(1, 1))
>>> Line(Point(0, 0), Point(1, 1))
Line(Point(0, 0), Vector(1, 1))

Implements many built-in methods:

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

Finds the closest connecting line segment between this object and another:

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

Finds the intersection of this object and another:

>>> l = Line(Point(0, 2), Vector(0, -2))
>>> l.intersect(Line(Point(2, 0), Vector(-2, 0)))
Point(0.0, 0.0)
>>> l.intersect(Line(Point(1, 2), Vector(0, -2))) is None
True
>>> from petrify.plane import Circle
>>> l.intersect(Circle(Point(0, 0), 1))
LineSegment(Point(0.0, -1.0), Point(0.0, 1.0))
normalized()[source]

Normalizes this line:

>>> Line(Point(0, 0), Point(2, 0)).normalized()
Line(Point(0, 0), Vector(1.0, 0.0))
petrify.plane.LineSegment

alias of petrify.plane.line.LineSegment2

class petrify.plane.LineSegment2(*args)[source]

Bases: petrify.plane.line.Line2

Represents a line segment:

>>> LineSegment(Point(0, 0), Vector(1, 1))
LineSegment(Point(0, 0), Point(1, 1))
connect(other)

Finds the closest connecting line segment between this object and another:

>>> l = Line(Point(0, 2), Vector(0, -2))
>>> l.connect(Point(1, 0))
LineSegment(Point(0.0, 0.0), Point(1.0, 0.0))
>>> from petrify.plane import Circle
>>> l.connect(Circle(Point(2, 0), 1))
LineSegment(Point(0.0, 0.0), Point(1.0, 0.0))
intersect(other)

Finds the intersection of this object and another:

>>> l = Line(Point(0, 2), Vector(0, -2))
>>> l.intersect(Line(Point(2, 0), Vector(-2, 0)))
Point(0.0, 0.0)
>>> l.intersect(Line(Point(1, 2), Vector(0, -2))) is None
True
>>> from petrify.plane import Circle
>>> l.intersect(Circle(Point(0, 0), 1))
LineSegment(Point(0.0, -1.0), Point(0.0, 1.0))
normalized()

Normalizes this line:

>>> Line(Point(0, 0), Point(2, 0)).normalized()
Line(Point(0, 0), Vector(1.0, 0.0))
petrify.plane.Matrix

alias of petrify.plane.Matrix2

class petrify.plane.Matrix2[source]

Bases: object

A matrix that can be used to transform two-dimensional vectors and points.

classmethod rotate(angle)[source]

Counter-clockwise rotational transform:

>>> (Point(1, 0) * Matrix2.rotate(tau / 4)).round(2)
Point(0.0, 1.0)
classmethod scale(x, y)[source]

A scale transform:

>>> Point(1, 1) * Matrix2.scale(2, 3)
Point(2, 3)
classmethod translate(x, y)[source]

Translates:

>>> Point(1, 1) * Matrix2.translate(2, 3)
Point(3, 4)
petrify.plane.Point

alias of petrify.plane.point.Point2

class petrify.plane.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)
angle(other)

Return the angle to the vector other:

>>> Vector(1, 0).angle(Vector(0, 1)) == tau / 4
True
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))
dot(other)

The dot product of this vector and other:

>>> Vector(2, 1).dot(Vector(2, 3))
7
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
normalized()

Return a new vector normalized to unit length:

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

Return one vector projected on the vector other

reflected(normal)

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)

Rounds this vector to the given decimal place:

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

Snaps this vector to a grid:

>>> Vector(1.15, 1.15).snap(0.25)
Vector(1.25, 1.25)
petrify.plane.Polygon

alias of petrify.plane.Polygon2

class petrify.plane.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)])
petrify.plane.Ray

alias of petrify.plane.line.Ray2

class petrify.plane.Ray2(*args)[source]

Bases: petrify.plane.line.Line2

Represents a line with an origin point that extends forever:

>>> Ray(Point(0, 0), Vector(1, 1))
Ray(Point(0, 0), Vector(1, 1))
connect(other)

Finds the closest connecting line segment between this object and another:

>>> l = Line(Point(0, 2), Vector(0, -2))
>>> l.connect(Point(1, 0))
LineSegment(Point(0.0, 0.0), Point(1.0, 0.0))
>>> from petrify.plane import Circle
>>> l.connect(Circle(Point(2, 0), 1))
LineSegment(Point(0.0, 0.0), Point(1.0, 0.0))
intersect(other)

Finds the intersection of this object and another:

>>> l = Line(Point(0, 2), Vector(0, -2))
>>> l.intersect(Line(Point(2, 0), Vector(-2, 0)))
Point(0.0, 0.0)
>>> l.intersect(Line(Point(1, 2), Vector(0, -2))) is None
True
>>> from petrify.plane import Circle
>>> l.intersect(Circle(Point(0, 0), 1))
LineSegment(Point(0.0, -1.0), Point(0.0, 1.0))
normalized()

Normalizes this line:

>>> Line(Point(0, 0), Point(2, 0)).normalized()
Line(Point(0, 0), Vector(1.0, 0.0))
petrify.plane.Vector

alias of petrify.plane.point.Vector2

class petrify.plane.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)
petrify.plane.solve_matrix(A)[source]

Solve a system of equations using gauss-jordan elimination.

petrify.plane.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