2D Cartesian

The pyTooling.Cartesian2D package implements points, offsets, sizes and line segments in a 2-dimensional cartesian coordinate system, plus the shapes built from them.

See also

3D Cartesian

→ The same classes with a third axis, and the volumes built from them.

2D Cartesian Properties:

  • Coordinates as int or float.

  • Every class is generic in its coordinate type, so a grid of integers and a drawing of floats use the same classes.

  • A point and an offset are different types, which is what makes the arithmetic below unambiguous.

  • A vector is an immutable value (a direction, a normal), an offset a mutable displacement.

Features

  • An integer or a floating-point coordinate system, chosen per instance - every class is generic in its coordinate type, so a grid of integers and a drawing of floats use the same classes.

  • Points and offsets as separate types, with the arithmetic relating them: a point plus an offset is a point, a point minus a point is an offset - in both the copying and the in-place form.

  • Offsets as vectors - an offset reports its length, computes dot and cross products, normalizes to length 1 and scales by a number.

  • Distances measured - a line segment reports its length and its angle.

  • Curved segments - a circular ArcSegment2D around a center, reporting its radius, angles and length, and quadratic and cubic Bezier curves, with exact bounding boxes.

  • Paths - a Path chains line segments, arcs and Bezier curves, open or closed, like SVG’s <path>; a Polyline is a path of line segments, built from points. Paths, polylines and polygons convert into each other, where no approximation is needed.

  • Shapes placed by their corners, so a shape is positioned by the points it is built from - a circle by its center.

  • Shapes validated on construction: a Polygon with any number of corners, the four-cornered Trapezium narrowed down to a Square, a Circle and an Ellipse along any direction.

  • Closed shapes measured - every Geometry2D, like a polygon or a circle, reports the area it encloses and its perimeter, an ellipse an approximate one; a circle its diameter too.

  • Bounding boxes - every shape and segment reports the smallest axis-aligned BoundingBox2D enclosing it, and bounding boxes unite, intersect and contain points.

  • Shapes and segments moved as a whole - Move(offset) shifts all their points in place.

  • Lines in Hesse normal form - a Line2D from two points, a point and a direction, or a point and a normal; it measures the signed distance of a point, projects a point onto it and intersects a ray.

  • Mirroring at a shared line - a Line2D, like a VerticalLine2D or a HorizontalLine2D, is in absolute coordinates, so elements of different groups are mirrored at the same line, also at an oblique one.

  • Points owned by one element - a shape or segment owns its points, so a point can’t silently belong to two shapes. Coordinates of different frames aren’t mixed in a computation.

  • Groups with relative coordinates - a Group2D holds shapes, segments and groups at an offset, so moving a group moves everything in it. Absolute positions, alignment and distribution are computed across groups.

  • Data attached to elements - every shape, segment and group carries a value and key-value-pairs, so a drawing element knows the model object it draws.

  • A canvas knowing its y-axis - a Canvas2D is the root of a drawing, with the y-axis pointing up (mathematics) or down (screen, SVG), and flips a whole drawing to the other direction.

  • Drawing styles - colors, strokes, fills and fonts as values, which elements can share, and drawable shapes carrying an outline and a fill.

  • Independent copies - Copy(), copy.copy() and copy.deepcopy() return an object sharing no point with the original. A shape’s copy is of the same class, and its edges share the copied corners. A copy owns its points and isn’t in a group.

  • Conversion to plain tuples through ToTuple, for handing coordinates to a library that expects them.

Missing Features

  • Rotation and scaling - an element can be shifted and mirrored at any line, but not rotated or scaled.

  • Rotated ellipses and elliptical arcs.

  • The length of a Bezier curve.

  • Exact containment and intersection tests (is this point inside that rotated rectangle?). Only bounding boxes are compared.

Planned Features

  • The 3-dimensional volumes, which currently exist as empty classes - see Volumes.

Out of Scope

  • A general-purpose geometry or linear-algebra library. These classes exist to give coordinates a name and a type in the packages using them.
    For doing real geometry have a look at NumPy or Shapely.

  • Rendering. Styles describe how an element looks, but nothing here draws anything - ToTuple hands the numbers to whatever does.

  • Coordinate systems other than cartesian - no polar, spherical or geographic coordinates.

By Feature

Danger

Accessing internal fields of a origin, point, offset, … is strongly not recommended for users, as it might lead to a corrupted data structure. If a power-user wants to access these fields, feel free to use them for achieving a higher performance, but you got warned 😉.

Basic Classes

All of them are generic in Coordinate, a type variable bound to int or float, so the coordinate type is chosen once and then checked throughout.

Their coordinates, offsets, sizes and end points are properties. A constructor parameter and an assignment are checked alike: None raises ValueError, a value of another type raises TypeError. The lower-case names (x, xOffset, width, start, …) are deprecated aliases of these properties; they are removed in v11.0.0.

Point2D

A Point2D is a position: properties X and Y, and the arithmetic that keeps a position distinct from a displacement.

Expression

Result

point + vector

A new Point2D, shifted.

point - vector

A new Point2D, shifted back.

point - point

An Offset2D - the displacement between them.

point += vector

The same point, moved in place.

A vector is a Vector2D or an Offset2D.

from pyTooling.Cartesian2D import Point2D, Offset2D

start = Point2D(3, 4)
moved = start + Offset2D(1, 1)     # Point2D(4, 5)
delta = moved - start              # Offset2D(1, 1)

An offset may be written as a plain 2-tuple wherever one is accepted. Anything else raises TypeError, with the offending type in a note.

Copy() returns an independent point and ToTuple() the (x, y) pair. Absolute returns a new point relative to the origin, for a point inside a group.

A point is owned by at most one element, the segment or shape it belongs to: the read-only Parent. A new point and a copy have no owner, unless parameter parent sets one. See Ownership and Frames.

See also

Point3D

→ Its counterpart, the point with a z coordinate.

Origin2D

An Origin2D is a Point2D fixed at (0, 0) - its constructor takes no coordinates. It exists so a function needing the origin can say so in its signature instead of checking two numbers.

An origin can’t be copied: Copy(), copy.copy() and copy.deepcopy() raise InvalidOperationError (a RuntimeError).

See also

Point2D

→ Its base-class, the 2-dimensional point.

Origin3D

→ Its counterpart, the origin with a z coordinate.

Offset2D

An Offset2D is a displacement: properties XOffset and YOffset. Offsets add and subtract with each other, and negate:

right = Offset2D(1, 0)
left  = -right                     # Offset2D(-1, 0)
diag  = right + Offset2D(0, 1)     # Offset2D(1, 1)

Unlike a point, an offset compares with == and != - two displacements are equal when their components are.

An offset is also a vector, with the vector operations of its base-class BaseVector2D, shared with the immutable Vector2D:

Expression

Result

offset.Length

The Euclidean length, a float.

offset.ScalarProduct(other)

The dot product x1 * x2 + y1 * y2, zero for perpendicular offsets.

offset.VectorProduct(other)

The cross product x1 * y2 - y1 * x2, a number in 2D: zero for parallel offsets, positive if other points counterclockwise from offset.

offset.Normalized()

A new offset of length 1 in the same direction, with float components.

offset * number, number * offset

A new offset, scaled by the number.

offset *= number

The same offset, scaled in place.

from pyTooling.Cartesian2D import Offset2D

direction = Offset2D(3, 4)

direction.Length                           # 5.0
direction.ScalarProduct(Offset2D(-4, 3))   # 0, perpendicular
direction.VectorProduct(Offset2D(6, 8))    # 0, parallel
direction.Normalized()                     # Offset2D(0.6, 0.8)
2 * direction                              # Offset2D(6, 8)

ScalarProduct() (the dot product) and VectorProduct() (the cross product) take a Vector2D or an Offset2D; None raises ValueError, another type TypeError. A factor other than an int or float raises TypeError, and normalizing a zero vector raises InvalidOperationError, as it has no direction.

An offset is mutable: XOffset and YOffset can be assigned, and +=, -= and *= change the offset in place. Therefore, it isn’t hashable. A group’s Offset is an offset.

See also

Vector2D

→ Its sibling, the immutable vector.

Offset3D

→ Its counterpart, the offset with a ZOffset.

Vector2D

A Vector2D is an immutable vector, the sibling of Offset2D. Both derive from the abstract BaseVector2D, which holds the components, the read-only properties XOffset and YOffset, the vector operations and the operators creating a new vector.

Inheritance diagram:

Inheritance diagram of pyTooling.Cartesian2D.Vector2D, pyTooling.Cartesian2D.Offset2D

Vector2D

Offset2D

Use it for

A value: a direction, a normal.

A displacement that changes: a group’s offset, a move.

Components

Read-only.

Assignable.

+=, -=, *=

Bind a new vector to the name.

Change the offset in place.

Hashable

Yes: a dictionary key or a set member.

No.

Copy

Not needed; copy.copy() and copy.deepcopy() work.

Copy(), copy.copy() and copy.deepcopy().

An operator creating a new vector returns a vector of its left operand’s class; the right operand can be a vector of either class or a tuple. A vector and an offset compare equal, if their components are equal. Points, bounding boxes and every Move() take a vector of either class.

from pyTooling.Cartesian2D import Point2D, Vector2D, Offset2D

up =    Vector2D(0, 1)
shift = Offset2D(2, 0)

up + shift                         # Vector2D(2, 1)
shift + up                         # Offset2D(2, 1)
up == Offset2D(0, 1)               # True
Point2D(3, 4) - up                 # Point2D(3, 3)

names = {up: "up", Vector2D(1, 0): "right"}
names[Vector2D(0.0, 1.0)]          # 'up'

shift += up                        # the same offset, now Offset2D(2, 1)

Assigning up.XOffset raises AttributeError. Assigning a vector to a group’s Offset raises TypeError; convert it by Offset2D(*vector.ToTuple()).

See also

Offset2D

→ Its sibling, the mutable offset.

Vector3D

→ Its counterpart, the vector with a ZOffset.

Size2D

A Size2D is an extent: Width and Height. It is deliberately not an offset - adding a size to a point is meaningless, and keeping the types apart is what prevents it.

See also

Size3D

→ Its counterpart, the size with a depth.

BoundingBox2D

A BoundingBox2D is the smallest axis-aligned rectangle enclosing something: the read-only properties MinX, MinY, MaxX and MaxY, and from them Width, Height and Center. A maximum less than its minimum raises ValueError.

It is a computed value, not a shape: the BoundingBox property of a shape, segment or group returns a new one on every access, so it always follows moved points. FromPoints() encloses any number of points, and box + offset returns a shifted bounding box.

Expression

Result

box.Union(other)

A new BoundingBox2D enclosing both.

box.HasOverlap(other)

True, if the inner areas overlap - touching borders don’t count.

box.ContainsPoint(point), point in box

True, if the point lies inside or on the border.

box.ToRectangle()

A new Rectangle with the box’s corners.

from pyTooling.Cartesian2D        import Point2D
from pyTooling.Cartesian2D.Shapes import Circle, Polygon

triangle = Polygon((Point2D(0, 0), Point2D(4, 0), Point2D(2, 3)))
bubble =   Circle(Point2D(5, 1), 1)

triangle.BoundingBox                            # BoundingBox2D(0, 0, 4, 3)
triangle.BoundingBox.Union(bubble.BoundingBox)  # BoundingBox2D(0, 0, 6, 3)

Like an offset, a bounding box compares with == and !=, and Copy(), copy.copy() and copy.deepcopy() return an independent one.

Line2D

A Line2D is a line in the geometric sense: infinite in both directions, unlike a LineSegment2D, which ends at its start and end point.

A line is stored in Hesse normal form: a Normal (nx, ny) of length 1 and the signed Distance d of the line from the origin, so the line is all points (x, y) with nx * x + ny * y = d. The common operations are one dot product each. The first non-zero component of the normal is positive, so every line has exactly one form.

Line2D(normal, distance) takes any non-zero normal: a normal of another length than 1 is normalized, and the distance is divided by the normal’s length, so Line2D(Offset2D(3, 4), 10) is the line 3x + 4y = 10, stored as normal (0.6, 0.8) and distance 2.0. A normal of length 1, like (1, 0), keeps its values, so integers stay integers. A zero normal raises ValueError. The normal can be a vector or an offset; the line stores it as a Vector2D, which is immutable like the line itself. The classmethods create a line from what is at hand:

Classmethod

Line

FromPoints(first, second)

Through two points; two points at the same position raise ValueError.

FromPointAndDirection(point, direction)

Through a point in a direction; a zero direction raises ValueError.

FromPointAndNormal(point, normal)

Through a point, perpendicular to a normal; a zero normal raises ValueError.

The points are taken in absolute coordinates, relative to the origin: a point inside a group is converted, like the start point of a ray.

Operation

Result

line.SignedDistance(point)

nx * x + ny * y - d: positive on the side the normal points to, negative on the other side, zero on the line.

line.Project(point)

A new point, the foot of the perpendicular from the point to the line.

line.Intersects(ray)

True, if the ray hits the line.

line.Intersection(ray)

A new point, where the ray hits the line. A ray parallel to the line, or pointing away from it, raises ValueError. A ray starting on the line, also one running along it, hits it at its start point.

Points and results are in absolute coordinates.

from pyTooling.Cartesian2D import Point2D, Offset2D, Line2D, Ray2D

line = Line2D.FromPoints(Point2D(2, 1), Point2D(6, -2))

str(line)                                          # '0.6 * x + 0.8 * y = 2.0'
line.SignedDistance(Point2D(5, 5))                 # 5.0
line.Project(Point2D(5, 5))                        # (2.0, 1.0)
line.Intersection(Ray2D(Point2D(0, 0), Offset2D(0, 1)))   # (0.0, 2.5)

Two axis-parallel lines are derived classes with a fixed normal. They keep their coordinate as given, an integer stays an integer:

  • VerticalLine2D - the line x = c, normal (1, 0); all its points have the same X;

  • HorizontalLine2D - the line y = c, normal (0, 1); all its points have the same Y.

VerticalLine2D.FromElement creates the line through an edge or the center of an element’s absolute bounding box, named by an Alignment2D (MinX, CenterX or MaxX); HorizontalLine2D.FromElement takes MinY, CenterY or MaxY.

Called on VerticalLine2D or HorizontalLine2D, the classmethods FromPoints, FromPointAndDirection and FromPointAndNormal return that class and keep the coordinate as given. Points with different coordinates, or a direction or normal that doesn’t fit the line, raise ValueError:

from pyTooling.Cartesian2D import Point2D, Vector2D, VerticalLine2D, HorizontalLine2D

VerticalLine2D.FromPoints(Point2D(5, 0), Point2D(5, 3))                # VerticalLine2D(5)
HorizontalLine2D.FromPointAndDirection(Point2D(1, 7), Vector2D(2, 0))  # HorizontalLine2D(7)
VerticalLine2D.FromPointAndNormal(Point2D(4, 1), Vector2D(-1, 0))      # VerticalLine2D(4)

VerticalLine2D.FromPoints(Point2D(5, 0), Point2D(6, 3)) raises ValueError, as the points have different x-coordinates; Line2D.FromPoints creates the oblique line through them.

MirrorAt() mirrors an element at a line in the element’s frame, so elements of different groups are mirrored at the same line:

  • A line parallel to an axis, also a Line2D with normal (1, 0) or (0, 1), flips one coordinate, so integer coordinates stay integers.

  • At an oblique line nx * x + ny * y = d, every absolute point (x, y) moves by -2 * (nx * x + ny * y - d) times the normal. Segments mirror their end points and the points defining their curve, an arc reverses its direction. Shapes mirror their corners or their center, so a rectangle stays a rectangle. An ellipse mirrors the direction of its first axis too, so an oblique line rotates it. A group mirrors its offset like a point, and its children at the line through the group’s origin with the same normal. A text mirrors its position only.

from pyTooling.Cartesian2D        import Point2D, Offset2D, Alignment2D, VerticalLine2D, HorizontalLine2D
from pyTooling.Cartesian2D.Shapes import Circle, Square
from pyTooling.Cartesian2D.Groups import Group2D

pad =    Square(Point2D(0, 0), Point2D(2, 0), Point2D(2, 2), Point2D(0, 2))
wheel =  Circle(Point2D(1, 1), 1)
axle =   Group2D(Offset2D(10, 0), (wheel, ))
mirror = VerticalLine2D(20)

str(mirror)                        # 'x = 20'
pad.MirrorAt(mirror)               # pad.BoundingBox is BoundingBox2D(38, 0, 40, 2)
wheel.MirrorAt(mirror)             # wheel.Center is (19, 1), relative to axle
wheel.Center.Absolute              # (29, 1)

HorizontalLine2D.FromElement(wheel, Alignment2D.MinY)   # HorizontalLine2D(0)

Mirroring at the diagonal x = y swaps the coordinates, up to rounding:

from pyTooling.Cartesian2D        import Point2D, Offset2D, Line2D
from pyTooling.Cartesian2D.Shapes import Circle, Ellipse
from pyTooling.Cartesian2D.Groups import Group2D

diagonal = Line2D.FromPoints(Point2D(0, 0), Point2D(1, 1))
wheel =    Circle(Point2D(1, 2), 1)
axle =     Group2D(Offset2D(10, 20), (wheel, ))

axle.MirrorAt(diagonal)            # axle.Offset is (20, 10), wheel.Center is (2, 1)

egg = Ellipse(Point2D(1, 2), 3, 2)
egg.MirrorAt(diagonal)             # egg.Center is (2, 1), egg.Direction is (0, 1)

Like a style, a line is a value: its properties are read-only, it compares with == and != and is hashable. Equality is geometric - two lines are equal, if their normals and distances are, whatever class or classmethod created them, and equal lines have the same hash:

VerticalLine2D(5) == Line2D(Offset2D(1, 0), 5)                        # True
VerticalLine2D(5) == Line2D.FromPoints(Point2D(5, 0), Point2D(5, 3))  # True
VerticalLine2D(5) == HorizontalLine2D(5)                              # False

See also

Plane3D

→ Its counterpart, the plane in Hesse normal form.

Ray2D

A Ray2D is a half-line: it starts at a point and is infinite in one direction. It is built from a start point and a direction, a Vector2D or an Offset2D; it copies both, the direction into a new immutable vector, so it owns no point of an element. The start point is taken in absolute coordinates: a point inside a group is converted. A zero direction raises ValueError. Start and Direction return the stored point and vector. PointAt() returns the point at a distance from the start point, whatever the length of the direction; a negative distance raises ValueError.

from pyTooling.Cartesian2D import Point2D, Offset2D, Ray2D

ray = Ray2D(Point2D(1, 2), Offset2D(3, 4))

str(ray)                           # '(1, 2) + t * (3, 4), t >= 0'
ray.PointAt(5)                     # (4.0, 6.0)

Like a line, a ray is in absolute coordinates and compares by value; as its start point can be changed, it is unhashable (like an Offset2D). Two rays are equal (==), if they start at the same point and their directions point the same way - the cross product of the directions is zero and their dot product is positive. The lengths of the directions don’t matter, and integer coordinates compare exactly:

Ray2D(Point2D(1, 2), Offset2D(3, 4)) == Ray2D(Point2D(1, 2), Offset2D(6, 8))     # True
Ray2D(Point2D(1, 2), Offset2D(3, 4)) == Ray2D(Point2D(1, 2), Offset2D(-3, -4))   # False, opposite direction

Segment2D

A Segment2D is a Start and an End point.

It is abstract: a straight segment is a LineSegment2D, curved segments are described below.

Its constructor copies both points by default, so a segment doesn’t change when the points it was built from move later. copyPoints=False keeps the references instead: the segment takes ownership of both points. A point assigned to Start or End is referenced too; the segment owns it and releases the previous one.

A segment with a Parent shape is an edge of that shape, like the edges of a Polygon: it references two of the shape’s corners, which stay owned by the shape. An edge can’t replace its points.

BoundingBox encloses both points, and Move() shifts both by an Offset2D, in place.

See also

Segment3D

→ Its counterpart, the segment between two 3D points.

LineSegment2D

A LineSegment2D is a segment understood as a straight line, which is what gives it measurements:

  • Length - the Euclidean distance between its ends, a float even for integer coordinates;

  • AngleTo() - the angle to another line segment’s direction, from 0 to math.pi; a line segment of length 0 has no direction and raises ValueError;

  • ToOffset() - the segment as the displacement from start to end;

  • ToVector() - the same as an immutable Vector2D.

See also

Segment2D

→ Its base-class, the 2-dimensional segment.

LineSegment3D

→ Its counterpart, the line segment in 3D.

Curved Segments

A curved segment is a segment too: it has a start and an end point, a group can hold it, and a shape can use it as an edge. A Bezier curve’s control points are owned like the start and end point: copied by default, referenced with copyPoints=False, and moved and mirrored with the segment. An arc keeps only its end points and describes its curve relative to them.

ArcSegment2D

An ArcSegment2D is a piece of a circle from a start to an end point. It stores its curve relative to its chord, the line segment from the start to the end point: the ChordPosition of a root point on the chord, and the arc’s Height over the chord at that point, as a fraction of the chord’s length. The Center (a new point on each access), the Radius and the Direction from the start to the end point, a SweepDirection - Counterclockwise or Clockwise, seen with the y-axis pointing up - are computed from them. Besides the start and end point, the constructor takes exactly one group of keyword parameters describing the circle; none or more than one group raises ValueError:

center and direction

The center of the circle and the direction. The end point must have the start point’s distance to the center, compared with math.isclose(). Only the center’s coordinates are used.

arcPoint

A third point of the arc: the arc runs from the start point through this point to the end point, so the order of the three points gives the direction. Only its coordinates are used. Three points on a line raise ValueError.

height and optionally chordPosition

The arc’s distance from its chord, the line segment from the start to the end point, measured perpendicular at a root point. chordPosition places the root point on the chord, as a fraction from 0.0 at the start point to 1.0 at the end point; the default 0.5 is the chord’s midpoint, where the height is the arc’s sagitta. A positive height lies on the left of the chord, seen from the start point, so the arc runs clockwise; a negative height lies on the right. The height is given as a distance and stored relative to the chord’s length.

                arc point
            .-------x-------.
         .'         |         '.
       /            | height     \
start x-------------+-------------x end
                root point
      |<----------->|
      chordPosition * chord length
from pyTooling.Cartesian2D import Point2D, Offset2D, ArcSegment2D, SweepDirection

start = Point2D(0, 0)
end =   Point2D(2, 0)

# Three times the upper half of the circle around (1, 0) with radius 1, clockwise:
byCenter =   ArcSegment2D(start, end, center=Point2D(1, 0), direction=SweepDirection.Clockwise)
byArcPoint = ArcSegment2D(start, end, arcPoint=Point2D(1, 1))
byHeight =   ArcSegment2D(start, end, height=1)

flat = ArcSegment2D(Point2D(0, 0), Point2D(4, 0), height=-1)
flat.Center                        # (2.0, 1.5)
flat.Radius                        # 2.5
flat.Direction                     # SweepDirection.Counterclockwise

The geometry behind arcs and circles is public: chordPoint() computes the point at a height over a chord, circumcenter() the center of the circle through three points.

A center or an arcPoint is converted to the sagitta, the height at the chord’s midpoint, and Center computes the center back from it, so a given center can come back with different last digits (0.9999999999999432 for 1); compare it with math.isclose(). An arc isn’t a circle: a start and an end point at the same position have no chord and raise ValueError, whatever the group - also when assigned to Start or End. An end point moved in place onto the other one isn’t checked: the arc has no center then.

Moving an end point, or assigning another point to Start or End, keeps the arc’s shape: its height scales with the chord, and its center, radius and direction follow. So an arc stays valid when a joint of a path moves.

arc = ArcSegment2D(Point2D(0, 0), Point2D(2, 0), height=1)
end = arc.End
end += Offset2D(2, 0)              # the chord is twice as long
arc.Height                         # 2.0
arc.Center                         # (2.0, 0.0)
  • StartAngle and EndAngle - the angles of both points seen from the center, in radians, from -math.pi to math.pi;

  • SweepAngle - the angle from the start to the end point, positive counterclockwise and negative clockwise;

  • Length - the radius times the absolute sweep angle;

  • BoundingBox - exact: where the arc passes the leftmost, rightmost, lowest or highest point of its circle, it bulges past its start and end point;

  • ToChord() - a new LineSegment2D from the start to the end point.

arc = ArcSegment2D(Point2D(2, 0), Point2D(-2, 0), center=Point2D(0, 0), direction=SweepDirection.Counterclockwise)
arc.SweepAngle                     # 3.14159... (math.pi)
arc.Length                         # 6.28318...
arc.BoundingBox                    # BoundingBox2D(-2, 0, 2, 2.0)
arc.ToChord().Length               # 4.0
arc.MirrorY()                      # arc.Direction is SweepDirection.Clockwise

Mirroring an arc reverses its direction, so the mirrored arc bulges to the other side.

See also

Segment2D

→ Its base-class, the 2-dimensional segment.

Circle

→ The full circle as a shape.

BezierSegment2D

A Bezier curve runs from its start point to its end point and is pulled towards its ControlPoints, like SVG’s Q and C commands or TikZ’ .. controls ... Its points are given in curve order:

PointAt() returns the point of the curve at a curve parameter t, from 0 (the start point) to 1 (the end point). The BoundingBox is exact: it adds the points where the curve turns in x- or y-direction. The box of the control points is larger, as the curve passes them only in special cases. ToTuple() returns the points as tuples, in curve order.

from pyTooling.Cartesian2D import Point2D, QuadraticBezierSegment2D, CubicBezierSegment2D

hill = QuadraticBezierSegment2D(Point2D(0, 0), Point2D(2, 4), Point2D(4, 0))
hill.PointAt(0.5)                  # (2.0, 2.0)
hill.BoundingBox                   # BoundingBox2D(0, 0, 4, 2.0)

bow = CubicBezierSegment2D(Point2D(0, 0), Point2D(6, 0), Point2D(6, 6), Point2D(0, 6))
bow.BoundingBox                    # BoundingBox2D(0, 0, 4.5, 6)

A Bezier curve has no Length: a cubic curve’s length has no closed form, it needs a numeric approximation.

See also

Segment2D

→ Its base-class, the 2-dimensional segment.

Ownership and Frames

Every point is owned by at most one element: a standalone segment owns its two points, a shape owns its corners or its center. An element given a point with copyPoints=False takes ownership of it; a point owned by another element raises AlreadyOwnedError, so two shapes never move each other’s corners by accident. The same point object given twice raises DuplicatePointError. Copying is always possible - the copy has no owner.

Point2D(x, y, parent=shape) sets the owner, but doesn’t register the point: the shape’s points don’t change. Such a point is in the shape’s frame, so it can be combined with the shape’s points, and it can replace the start or end point of its parent segment.

The edges of a shape, like those of a polygon or a polyline, are views: they reference the shape’s corners, but don’t own them. Moving an edge moves the corners it references; replacing an edge’s point raises NotOwnedError.

from pyTooling.Cartesian2D        import Point2D
from pyTooling.Cartesian2D.Shapes import Circle, Polygon

center =   Point2D(1, 1)
bubble =   Circle(center, 1, copyPoints=False)   # center.Parent is bubble
triangle = Polygon((center, Point2D(4, 0), Point2D(2, 3)), copyPoints=False)
# AlreadyOwnedError: Point is owned by another element already.

The coordinates of a point are relative to a frame: Frame is the frame of its owner, and an element without a frame has None. Points, segments and shapes derive from the abstract Base2D, which holds the parent and returns the frame; segments and shapes also from Element2D. Subtracting points or measuring the angle between line segments of different frames raises NotInSameFrameError instead of mixing unrelated coordinates.

Shapes

The shapes live in pyTooling.Cartesian2D.Shapes. The four-cornered shapes form a chain in which each class is the one above it with one more constraint. The constraint is checked in the constructor, so an object that exists is an object of that shape. A polygon, a circle and an ellipse stand beside that chain. The closed shapes derive from Geometry2D, a circle and an ellipse through EllipticalGeometry2D, which holds their center; a path and a polyline, a path of line segments, are shapes, but no geometries.

Shape

Shape is the abstract base-class of every 2D shape. It carries no fields; it exists so a function can accept a shape. Instantiating it raises AbstractClassError.

A shape is an element, so a group can hold it. Every shape implements the element’s two abstract members:

  • BoundingBox - the smallest axis-aligned bounding box enclosing the shape, computed on every access;

  • Move() - shifts all points of the shape by an Offset2D, in place. Edges share the corners, so they move along.

Every shape is created by ExtendedType and stores its fields in slots, so a class derived from a shape can declare slots=True too and gets no instance dictionary.

See also

Volume

→ Its counterpart, the base-class of the 3-dimensional volumes.

Geometry2D

Geometry2D is the abstract base-class of the closed shapes, the ones enclosing an area. Besides the members of a shape, it declares two abstract read-only properties:

A polygon and a trapezium compute both from their corners, which may be given clockwise or counterclockwise; a rectangle and a square inherit them. A circle and an ellipse compute them from their radii; they share their center through EllipticalGeometry2D. A path is a shape, but no geometry, even when it is closed.

from pyTooling.Cartesian2D        import Point2D
from pyTooling.Cartesian2D.Shapes import Polygon

corner = Polygon((Point2D(0, 0), Point2D(4, 0), Point2D(4, 1), Point2D(1, 1), Point2D(1, 3), Point2D(0, 3)))
corner.Area                        # 6.0
corner.Perimeter                   # 14.0

See also

Shape

→ Its base-class, the base-class of the 2-dimensional shapes.

Polygon

A Polygon is a closed shape with three or more corners, given as one iterable of Point2D in drawing order. The edges connect the corners in that order, the last corner back to the first one.

It keeps both views: Points, the corners as a tuple, and Segments, one LineSegment2D edge per corner, with consecutive edges sharing a corner object. The corners are copied by default; copyPoints=False keeps the given points instead, as for a segment. The polygon owns its corners, and its edges have the polygon as parent.

from pyTooling.Cartesian2D        import Point2D
from pyTooling.Cartesian2D.Shapes import Polygon

triangle = Polygon((Point2D(0, 0), Point2D(4, 0), Point2D(2, 3)))
str(triangle)                      # ((0, 0) → (4, 0) → (2, 3) → (0, 0))

A corner that isn’t a Point2D raises TypeError, fewer than three corners raise NotEnoughCornersError. Edges must not cross: two edges may have only their shared corner in common, else CrossingEdgesError is raised - for crossing edges, a corner on another edge, or an edge running back along the previous one. The coordinates are compared exactly. Corners moved in place later aren’t checked again.

See also

Geometry2D

→ Its base-class, the base-class of the closed 2-dimensional shapes.

Polyline

→ A path of line segments, built from points like a polygon.

Trapezium

→ The polygon with exactly four corners, given as four parameters.

Trapezium

A Trapezium is a polygon with exactly four corners, given as four parameters in the order p00, p01, p11, p10 - around the outline, not across it. It copies them, and inherits everything else from the polygon: the corners in Points, the four LineSegment2D edges in Segments sharing the corners, its area and perimeter, and the check that its edges don’t cross.

Both properties are read-only. Their deprecated aliases points and segments are assignable until v11.0.0, but an assignment replaces only its own tuple: the other one isn’t updated, and the shape of a Rectangle or Square isn’t checked again.

A corner that isn’t a Point2D raises TypeError, crossing edges raise CrossingEdgesError.

See also

Polygon

→ Its base-class, the closed shape with any number of corners.

Rectangle

A Rectangle is a trapezium whose opposite edges are parallel and equal in length, with 90°inner angles. The constructor compares the opposite edges’ lengths and the four inner angles, and raises ValueError when the four corners describe something else - a parallelogram, for example. Two consecutive corners at the same point make edges touching each other, which the polygon rejects with CrossingEdgesError. Lengths and angles are compared with math.isclose(), so a rectangle rotated by an arbitrary angle, with floating-point corners, is accepted.

See also

Trapezium

→ Its base-class, the four-cornered shape every other one narrows.

Cuboid

→ Its counterpart, the volume a rectangle corresponds to.

Square

A Square is a rectangle whose edges are all the same length - one more comparison, one more ValueError. A rhombus has four equally long edges, but fails the rectangle’s angle check.

See also

Rectangle

→ Its base-class, the trapezium with four right angles.

Cube

→ Its counterpart, the volume a square corresponds to.

EllipticalGeometry2D

EllipticalGeometry2D is the abstract base-class of the round geometries, the closed shapes around a center they own: a circle and an ellipse derive from it. It holds the Center, a Point2D. The center is copied by default; with copyPoints=False, the given point is kept, and a point owned by another element raises AlreadyOwnedError. Moving or mirroring the shape moves or mirrors its center; an ellipse mirrors its direction too.

The radii are fields of the derived classes, and so are the bounding box, the area and the perimeter computed from them: a circle has one radius, an ellipse two and a direction, and neither class derives from the other. Instantiating EllipticalGeometry2D raises AbstractClassError.

from pyTooling.Cartesian2D        import Point2D, Offset2D
from pyTooling.Cartesian2D.Shapes import Circle, Ellipse

bubble = Circle(Point2D(0, 0), 2)
egg =    Ellipse(Point2D(5, 0), 3, 2)
for shape in (bubble, egg):
  shape.Move(Offset2D(1, 1))

bubble.Center                      # (1, 1)
egg.Center                         # (6, 1)

See also

Geometry2D

→ Its base-class, the base-class of the closed 2-dimensional shapes.

Circle

→ The round shape with one radius.

Ellipse

→ The round shape with two radii.

Circle

A Circle is placed by its Center, a Point2D, and sized by its Radius, a positive int or float. The center is copied by default; copyPoints=False keeps the given point instead. The circle owns its center.

Three read-only properties measure it:

  • Diameter - twice the radius, in the radius’ type;

  • Perimeter - the length of its outline (circumference), a float;

  • Area - the area it encloses, a float.

from pyTooling.Cartesian2D        import Point2D
from pyTooling.Cartesian2D.Shapes import Circle

bubble = Circle(Point2D(10, 5), 2)
bubble.Diameter                    # 4
bubble.Area                        # 12.566...

A center that isn’t a Point2D or a radius that isn’t a number raises TypeError; a radius of 0 or below raises ValueError.

Two class methods create a circle from the parameter groups of an arc; the circle owns a new center, the given points are only read:

  • FromPoints() - the circle through three or more points: the first three must be distinct and not on one line, and every further point must be on their circle;

  • FromChord() - the circle through the end points of a chord and a point at a signed height over the chord, measured at chordPosition (default 0.5, the midpoint). A positive height is on the left of the chord, seen from its start point.

ring = Circle.FromPoints((Point2D(0, 0), Point2D(4, 0), Point2D(0, 3), Point2D(4, 3)))
ring.Center                        # (2.0, 1.5)
ring.Radius                        # 2.5

dome = Circle.FromChord(Point2D(0, 0), Point2D(4, 0), 1)
dome.Center                        # (2.0, -1.5)

See also

EllipticalGeometry2D

→ Its base-class, the base-class of the round 2-dimensional shapes around a center.

Ellipse

→ The round shape with two radii.

ArcSegment2D

→ A piece of a circle, given by the same parameter groups.

Ellipse

An Ellipse is placed by its Center, a Point2D, and sized by two radii along two perpendicular axes: FirstRadius and SecondRadius, each a positive int or float. The keyword-only parameter direction, a Vector2D or an Offset2D of any length but 0, points along the first axis; the second axis is turned by +90 degrees (counterclockwise). Without a direction, the first axis points in x-direction, (1, 0), and the ellipse is axis-aligned, like SVG’s <ellipse> with rx and ry. The center is copied by default; copyPoints=False keeps the given point instead. The ellipse owns its center.

Three read-only properties describe its axes:

  • Direction - the direction of the first axis, an immutable Vector2D of length 1, its first non-zero component positive. A given direction is normalized, unless its length is 1 already: (0, 1) stays an integer vector. As d and -d describe the same ellipse, (-3, 4) is stored as (0.6, -0.8);

  • FirstAxis - a new vector from the center to the end of the first axis, the direction scaled by the first radius;

  • SecondAxis - a new vector from the center to the end of the second axis, the direction turned by +90 degrees and scaled by the second radius.

An ellipse is a closed shape, an EllipticalGeometry2D like a circle, measured by two read-only properties:

  • Area - the area it encloses, a float;

  • Perimeter - the length of its outline (circumference), a float. It is Ramanujan’s second approximation: exact for equal radii, otherwise less than 0.04 % too small.

Its BoundingBox is the smallest axis-aligned box around the rotated ellipse: with the semi-axes u and v, it reaches sqrt(ux ** 2 + vx ** 2) from the center in x-direction and sqrt(uy ** 2 + vy ** 2) in y-direction. Its coordinates are floats, also for an axis-aligned ellipse.

from pyTooling.Cartesian2D        import Point2D, Offset2D
from pyTooling.Cartesian2D.Shapes import Ellipse

egg = Ellipse(Point2D(10, 5), 3, 2)
egg.BoundingBox                    # BoundingBox2D(7.0, 3.0, 13.0, 7.0)
egg.Area                           # 18.849...
egg.Perimeter                      # 15.865...

tilted = Ellipse(Point2D(0, 0), 3, 2, direction=Offset2D(3, 4))
tilted.Direction                   # (0.6, 0.8)
tilted.FirstAxis                   # (1.8, 2.4), up to rounding
tilted.SecondAxis                  # (-1.6, 1.2)
tilted.BoundingBox                 # BoundingBox2D(-2.408..., -2.683..., 2.408..., 2.683...)

Moving an ellipse moves its center. Mirroring mirrors its center as a point and its direction as a vector, the radii stay: MirrorX() and MirrorY() both negate the direction’s y-component (a direction (0, 1) stays), and an oblique line rotates the ellipse. The ellipse gets a new direction vector; a direction read before stays as it was.

tilted.MirrorX()
tilted.Direction                   # (0.6, -0.8)

A center that isn’t a Point2D, a radius that isn’t a number or a direction that isn’t a vector raises TypeError; a radius of 0 or below or a direction (0, 0) raises ValueError. Equal radii are accepted: such an ellipse is a circle, but not a Circle object.

See also

EllipticalGeometry2D

→ Its base-class, the base-class of the round 2-dimensional shapes around a center.

Circle

→ The round shape with one radius.

Path

A Path is a chain of connected segments, like SVG’s <path>: line segments, arcs and Bezier curves, in any mix. It is built from a sequence of segments, each starting at the end point of the previous one - the coordinates must be equal, else the constructor raises ValueError. With closed=True, the last segment must end at the start point of the first one; no closing line is added.

The path copies the given segments as its edges, the way a polygon builds its edges from its corners: consecutive edges share their joint, and the path owns every point - the joints in Points and the points defining the curves, like a Bezier curve’s control points. Segments holds the edges, IsClosed tells whether the path is closed. The given segments stay as they are; an edge references its segment’s Value and holds a shallow copy of its key-value-pairs.

from pyTooling.Cartesian2D        import Point2D, LineSegment2D, ArcSegment2D, QuadraticBezierSegment2D, \
                                         SweepDirection
from pyTooling.Cartesian2D.Shapes import Path

drop = Path((
  LineSegment2D(Point2D(0, 0), Point2D(4, 0)),
  ArcSegment2D(Point2D(4, 0), Point2D(4, 4), center=Point2D(4, 2), direction=SweepDirection.Counterclockwise),
  QuadraticBezierSegment2D(Point2D(4, 4), Point2D(2, 6), Point2D(0, 4)),
  LineSegment2D(Point2D(0, 4), Point2D(0, 0))
), closed=True)

line, arc, curve, back = drop.Segments
len(drop.Points)                   # 4 joints
line.End is arc.Start              # True
drop.BoundingBox                   # BoundingBox2D(0, 0, 6.0, 5.0)

The BoundingBox is the union of the edges’ exact bounding boxes. Length sums the edges’ lengths; a Bezier curve has none, so a path with one raises InvalidOperationError. Moving or mirroring a path moves or mirrors each joint once and the edges’ other points; a mirrored arc reverses its direction. A copy shares its copied joints between its edges, like the original.

An open path grows at both ends: AppendSegment() takes a segment starting at the path’s end point, PrependSegment() one ending at its start point. The path copies the segment as a new edge sharing that joint, like the initializer does. A closed path doesn’t get opened: both raise InvalidOperationError.

wire = Path((LineSegment2D(Point2D(0, 0), Point2D(4, 0)), ))
wire.AppendSegment(ArcSegment2D(Point2D(4, 0), Point2D(4, 4), height=-2))
wire.PrependSegment(LineSegment2D(Point2D(-2, 0), Point2D(0, 0)))

len(wire.Segments)                 # 3
wire.Points[-1]                    # (4, 4)
wire.BoundingBox                   # BoundingBox2D(-2, 0, 6.0, 4)

See also

Shape

→ Its base-class, the base-class of the 2-dimensional shapes.

Polygon

→ The closed shape of straight edges, built from its corners.

Polyline

A Polyline is a path of line segments, like SVG’s <polyline>. It is built from two or more points instead of segments, given as one iterable of Point2D in drawing order: every edge is a LineSegment2D between two consecutive points, and consecutive edges share their point. With closed=True, an edge connects the last point back to the first one; a closed polyline needs three or more points.

It inherits the path’s members: Points, Segments, IsClosed, Length, the bounding box, moving and mirroring. The points are copied by default; copyPoints=False keeps the given points instead, as for a polygon.

from pyTooling.Cartesian2D        import Point2D
from pyTooling.Cartesian2D.Shapes import Polyline

zigzag = Polyline((Point2D(0, 0), Point2D(3, 4), Point2D(6, 0)))
str(zigzag)                        # ((0, 0) → (3, 4) → (6, 0))
zigzag.Length                      # 10.0

wedge = Polyline((Point2D(0, 0), Point2D(3, 4), Point2D(6, 0)), closed=True)
len(wedge.Segments)                # 3
wedge.Length                       # 16.0

A point that isn’t a Point2D raises TypeError, fewer than two points - three for a closed polyline - raise NotEnoughCornersError. Unlike a polygon’s, a polyline’s edges may cross each other.

Paths, polylines and polygons convert into each other, where no approximation is needed. Each conversion copies the points, references the same value and copies the key-value-pairs shallowly:

  • ToPolyline() - a path of line segments becomes a polyline, open or closed like the path; a polyline inherits it, a copy;

  • ToPolygon() - a closed path of line segments, a closed polyline too, becomes a polygon, which checks its edges for crossings;

  • ToPolyline() - a polygon, and so a trapezium, rectangle or square, becomes a closed polyline.

An open path or polyline encloses no area: ToPolygon() raises InvalidOperationError. So do both path conversions for a path with an arc or a Bezier curve, as a curve can’t be converted without an approximation.

See also

Path

→ Its base-class, the chain of segments of any kind.

Polygon

→ The closed shape of straight edges, which encloses an area.

Groups

The groups live in pyTooling.Cartesian2D.Groups. A group places shapes, segments and other groups at an offset, so a drawing is built from parts that are positioned as a whole.

Element2D

Element2D is the abstract base-class of everything a group or canvas can hold: shapes, segments and groups. An element belongs to at most one group or canvas, its Parent, and offers:

  • BoundingBox in its frame and AbsoluteBoundingBox relative to the origin;

  • Move(), shifting it in its frame;

  • MirrorX() and MirrorY(), mirroring it vertically at a vertical axis (the x-coordinates flip) or horizontally at a horizontal axis (the y-coordinates flip), through an edge or the center of its bounding box, named by an Alignment2D and optionally shifted by an offset;

  • MirrorAt(), mirroring it at a line in absolute coordinates, which several elements can share;

  • MoveTo(), moving it into another group - by default keeping its absolute position, so its local coordinates change;

  • AlignTo(), moving it so an edge or the center of its bounding box aligns with another element’s - see Alignment2D. Both elements may be in different groups.

Assigning Parent moves an element into a group too, but keeps its local coordinates. A group can’t become its own descendant: that raises GroupCycleError. An edge of a shape belongs to its shape and can’t be moved into a group.

Every shape, segment and group takes a keyword-only parameter parent: a group or canvas given there registers the new element, as AddChild() does. The element registers when it is fully initialized, so a failing check leaves nothing in the group or canvas. A segment with a shape as parent is an edge of that shape instead. A Point2D with a parent is different, it isn’t registered: see Ownership and Frames.

An element carries an optional Value and arbitrary key-value-pairs (KeyValuePairsMixin), given by the keyword-only parameters value and keyValuePairs. The value is any object, for example the model object a shape draws. The key-value-pairs are read and written with the dictionary syntax; in, len(element) and iterating an element address the key-value-pairs, not its points or children. Copy() and copy.copy() keep the value (the same object) and copy the key-value-pairs shallowly; copy.deepcopy() copies both deeply. Points carry neither, to stay small. A copy isn’t in the group or canvas of the original.

from pyTooling.Cartesian2D        import Point2D
from pyTooling.Cartesian2D.Shapes import Circle

pin = Circle(Point2D(10, 5), 2, value="clock", keyValuePairs={"layer": 1})

pin["inverted"] = True

pin.Value                          # 'clock'
len(pin)                           # 2
pin.Copy()["layer"]                # 1

Group2D

A Group2D has an Offset and Children. The coordinates of all children are relative to the group, and a group without a parent is relative to the origin. So moving a group changes its offset only - the coordinates of its children stay as they are.

from pyTooling.Cartesian2D        import Point2D, Offset2D
from pyTooling.Cartesian2D.Shapes import Circle
from pyTooling.Cartesian2D.Groups import Group2D

wheel = Circle(Point2D(1, 1), 1)
axle =  Group2D(Offset2D(10, 0), (wheel, ))
car =   Group2D(Offset2D(0, 5), (axle, ))

wheel.Center                       # (1, 1), relative to axle
wheel.Center.Absolute              # (11, 6)
car.BoundingBox                    # BoundingBox2D(10, 5, 12, 7)
car.Move(Offset2D(100, 0))         # wheel.Center is still (1, 1)

Children are given to the constructor or added by AddChild(), and removed by RemoveChild(). A child of another element raises AlreadyOwnedError - use MoveTo() to move it.

A group’s BoundingBox is the union of its children’s bounding boxes, shifted by its offset, so it is in the group’s own frame. A group without children has no bounding box and raises EmptyGroupError. DistributeX() and DistributeY() move the children along that axis, so the gaps between them are equal: either at a given distance after the first child, or evenly between the first and the last child.

The frame of a point, segment or shape inside a group is that group. Subtracting points of different groups raises NotInSameFrameError; subtract their absolute positions instead.

Copy() and copy.copy() copy a group or a canvas with a copy of each child, made by the child’s Copy(); copy.deepcopy() uses deep copies of the children. The copy of a group has no parent.

See also

Ownership and Frames

→ How points belong to shapes and segments.

Canvas2D

A Canvas2D is the root of a drawing. It holds shapes, segments and groups like a group does, but has no offset and no parent: its children are relative to the origin. It knows the Direction of its y-axis, a YAxisDirection:

  • Up - y-coordinates grow upwards, as in mathematics (the default);

  • Down - y-coordinates grow downwards, as on a screen or in SVG.

A canvas has an optional Size, a Size2D starting at the origin. Without one, it is open in every direction.

Mirror() flips the canvas to the other direction, so the drawing looks the same afterwards. Every level of the drawing negates its y-coordinates - a group’s offset relative to its parent, every point relative to its group. On a canvas with a size, the top level uses height - y instead, so the drawing stays on the canvas.

from pyTooling.Cartesian2D        import Point2D, Offset2D, Size2D
from pyTooling.Cartesian2D.Shapes import Circle
from pyTooling.Cartesian2D.Groups import Canvas2D, Group2D, YAxisDirection

wheel =  Circle(Point2D(1, 2), 1)
axle =   Group2D(Offset2D(10, 20), (wheel, ))
canvas = Canvas2D(YAxisDirection.Down, Size2D(400, 300), (axle, ))

canvas.Top(wheel)                  # 21
canvas.Mirror()                    # canvas.Direction is YAxisDirection.Up
axle.Offset                        # (10, 280)
wheel.Center                       # (1, -2)
canvas.Top(wheel)                  # 279

A bounding box keeps its minimum and maximum, whatever the direction. Which of them is the top edge depends on the canvas: Top() and Bottom() return the absolute y-coordinate of an element’s top and bottom edge, and TopAlignment and BottomAlignment the Alignment2D to pass to AlignTo(). An element of another canvas raises NotInSameFrameError.

See also

Group2D

→ The nested container, placed at an offset.

Drawing

The styles live in pyTooling.Cartesian2D.Drawing. They are values: their properties are read-only, they compare with == and != by value and are hashable, so any number of elements can share one style object. A style of another type in a comparison raises TypeError.

from pyTooling.Cartesian2D.Drawing import Fill, Font, LineStyle, RGBAColor, Stroke

black =   RGBAColor(0, 0, 0)
outline = Stroke(1.5, black, LineStyle.Dashed)
shade =   Fill(RGBAColor.Parse("#ff800080"))
label =   Font("monospace", 10, bold=True)

shade.Color.Alpha                  # 0.50196...
shade.Color.ToHex()                # '#ff800080'
outline == Stroke(1.5, black, LineStyle.Dashed)   # True

Color

Color is the abstract base-class of all colors. A color is given in a color system, and every color converts to red, green and blue components and an opacity with ToRGBA(), so a renderer handles any color. A stroke or fill takes a color of any color system. A further color system, like HSL or CMYK, is a class derived from Color implementing ToRGBA().

An RGBAColor has a Red, Green and Blue component, integers from 0 to 255, and an opacity Alpha from 0.0 (transparent) to 1.0 (opaque, the default). A component out of range raises ValueError. Its ToRGBA() returns the color itself.

Parse() reads the hexadecimal form of CSS and SVG, #rrggbb or #rrggbbaa, and ToHex() and str() return it. The opacity is stored as given, but written with two hexadecimal digits, so ToHex() rounds it to a multiple of 1/255.

Stroke and Fill

A Stroke is the style of a line or an outline: a positive Width in the unit of the coordinates, a Color and a LineStyle: Solid (the default), Dashed or Dotted. A Fill is the style of an area, its Color.

Font

A Font has a Family and a Size in points. The family is a font name or a generic family, as in CSS - "DejaVu Sans Mono" or "monospace"; a renderer maps it to its format.

Its style has three independent parts, as in CSS: the font is Bold or not, has SmallCaps or not, and its letters have a posture, the FontStyle: a FontStyle of Upright (the default), Italic or Slanted.

Drawable Shapes

A drawable shape is a geometry class with a style, and has the same name: pyTooling.Cartesian2D.Drawing.Rectangle is a pyTooling.Cartesian2D.Shapes.Rectangle, so it is checked, measured, moved, mirrored and grouped like one. Drawing has a drawable Polygon, Trapezium, Rectangle, Square, Circle, Ellipse, Path and Polyline, and the drawable segments LineSegment2D, ArcSegment2D, QuadraticBezierSegment2D and CubicBezierSegment2D.

The constructors take the geometry’s parameters, the keyword-only parameters value and keyValuePairs of every element, and the keyword-only parameters stroke and fill:

  • Stroke - the outline, or None for no outline;

  • Fill - the area, or None for a transparent area.

Both are properties with a setter; a value of another type raises TypeError. A segment has no area, so it has a stroke only (StrokeMixin); the shapes have both (StyleMixin). A path or polyline encloses an area only when it is closed: a fill for an open one raises ValueError, given to the constructor or assigned. Copy() and copy.copy() return a drawable shape of the same class with copied points, the same style objects and value, and a shallow copy of the key-value-pairs. copy.deepcopy() keeps the same style objects too, and copies the value and the key-value-pairs deeply.

from pyTooling.Cartesian2D         import Point2D
from pyTooling.Cartesian2D.Drawing import Circle, Fill, Polyline, Rectangle, RGBAColor, Stroke
from pyTooling.Cartesian2D.Groups  import Canvas2D, YAxisDirection

outline = Stroke(1, RGBAColor(0, 0, 0))
canvas =  Canvas2D(YAxisDirection.Down)
box =     Rectangle(Point2D(0, 0), Point2D(40, 0), Point2D(40, 20), Point2D(0, 20), stroke=outline, parent=canvas)
dot =     Circle(Point2D(40, 10), 3, stroke=outline, fill=Fill(RGBAColor(255, 255, 255)), parent=canvas)
wire =    Polyline((Point2D(40, 10), Point2D(60, 10), Point2D(60, 30)), stroke=outline, parent=canvas)

box.Fill = Fill(RGBAColor.Parse("#ffffe0"))
dot.BoundingBox                    # BoundingBox2D(37, 7, 43, 13)
len(canvas.Children)               # 3
wire.IsClosed                      # False

A drawable shape checks its parent first and registers in it when its geometry and styles are checked, so a failing check leaves nothing in the group or canvas, and points given with copyPoints=False stay free. A drawable segment is no edge of a shape: its parent is a group or canvas. So a path can’t be built from drawable segments, a Path raises TypeError for one: build the path from geometric segments and draw it as a Path.

Text

A Text is an element of one or more lines at a Position, so it groups, moves and aligns like a shape. Newlines in its Content separate the lines, which Lines returns; a text without a newline has one line. The glyphs are drawn with the text’s fill, black (Fill(RGBAColor(0, 0, 0))) by default; fill=None makes them transparent. An outline around them is drawn with its stroke, none by default. The text owns its position point, like a circle owns its center. Like every element, it takes a value and keyValuePairs.

  • Alignment - a TextAlignment: every line starts (Left, the default), is centered (Center) or ends (Right) at the position’s x-coordinate.

  • Anchor - a VerticalAnchor: the point of the text at the position’s y-coordinate - the Top of the text, its Middle, the Baseline of its first line (the default, as in SVG) or its Bottom.

  • LineHeight - the distance of two baselines as a factor of the font size, 1.2 by default.

Content, Font, Alignment, Anchor and LineHeight are properties with a setter, like Stroke and Fill. An assigned value is checked like the constructor’s parameter: None raises ValueError, a value of another type TypeError, a line height that isn’t positive ValueError. The lines, the line count and the bounding box follow every assignment.

Top and bottom are those of the canvas holding the text: with the y-axis pointing down, the top of a text has its smallest y-coordinate. Without a canvas, the y-axis points up. Mirror() therefore keeps a text where it is drawn. Mirroring a text with MirrorX, MirrorY or MirrorAt moves its position only: the glyphs aren’t mirrored, and its alignment and anchor stay.

from pyTooling.Cartesian2D         import Point2D
from pyTooling.Cartesian2D.Drawing import Font, Text, TextAlignment, VerticalAnchor
from pyTooling.Cartesian2D.Groups  import Canvas2D, YAxisDirection

canvas = Canvas2D(YAxisDirection.Down)
label =  Text(
    Point2D(20, 10), "clock\nreset",
    font=Font("monospace", 10),
    alignment=TextAlignment.Center,
    anchor=VerticalAnchor.Top,
    parent=canvas
)

label.Lines                        # ('clock', 'reset')
label.BoundingBox                  # BoundingBox2D(5.0, 10, 35.0, 32.0)
label.Fill                         # Fill(RGBAColor(0, 0, 0, 1.0))

label.Content = "clock\nreset\nenable"
label.Anchor =  VerticalAnchor.Bottom
label.LineCount                    # 3
label.BoundingBox                  # BoundingBox2D(2.0, -24.0, 38.0, 10.0)

Attention

The BoundingBox of a text is an estimate: the longest line times the font’s CharacterWidth (0.6 of the size by default, the advance width of common monospace fonts) is its width; a line is one font size high, 0.8 of it above the baseline, and each further line adds the line height. A renderer measuring the glyphs gets other values, for a proportional font especially.

3D Cartesian

The pyTooling.Cartesian3D package is pyTooling.Cartesian2D with a third axis: the same classes, the same arithmetic, and a z coordinate throughout. Everything above applies, so this section names only what differs.

See also

2D Cartesian

→ The 2-dimensional classes this package extends, where the arithmetic is written out.

Basic Classes

Point3D

A Point3D has X, Y and Z, and the same arithmetic against a Vector3D or an Offset3D. A plain 3-tuple is accepted wherever an offset is.

See also

Point2D

→ Its counterpart, the 2-dimensional point.

Origin3D

Origin3D is a Point3D fixed at (0, 0, 0).

See also

Point3D

→ Its base-class, the point with a z coordinate.

Origin2D

→ Its counterpart, the 2-dimensional origin.

Offset3D

Offset3D holds XOffset, YOffset and ZOffset, and negates, adds, subtracts and compares as its 2D counterpart does.

The vector operations of BaseVector3D are the same too, except for the cross product: VectorProduct() returns a new vector of the left operand’s class, perpendicular to both vectors. The vectors and their cross product form a right-handed system, and the cross product of parallel vectors is a zero vector.

from pyTooling.Cartesian3D import Offset3D

xAxis = Offset3D(1, 0, 0)
yAxis = Offset3D(0, 1, 0)

xAxis.VectorProduct(yAxis)                 # Offset3D(0, 0, 1)
yAxis.VectorProduct(xAxis)                 # Offset3D(0, 0, -1)
Offset3D(2, 3, 6).Length                   # 7.0
Offset3D(2, 3, 6).ScalarProduct(xAxis)     # 2

See also

Vector3D

→ Its sibling, the immutable vector.

Offset2D

→ Its counterpart, the 2-dimensional offset.

Vector3D

A Vector3D is the immutable, hashable sibling of Offset3D, as Vector2D is in 2D. Both derive from the abstract BaseVector3D, and an operator returns a vector of its left operand’s class.

Inheritance diagram:

Inheritance diagram of pyTooling.Cartesian3D.Vector3D, pyTooling.Cartesian3D.Offset3D

from pyTooling.Cartesian3D import Vector3D, Offset3D

xAxis = Vector3D(1, 0, 0)

xAxis.VectorProduct(Offset3D(0, 1, 0))     # Vector3D(0, 0, 1)
Offset3D(0, 1, 0).VectorProduct(xAxis)     # Offset3D(0, 0, -1)
{xAxis: "east"}[Vector3D(1.0, 0.0, 0.0)]   # 'east'

See also

Offset3D

→ Its sibling, the mutable offset.

Vector2D

→ Its counterpart, the 2-dimensional vector.

Size3D

Size3D is Width, Height and Depth.

See also

Size2D

→ Its counterpart, the 2-dimensional size.

BoundingBox3D

A BoundingBox3D is the smallest axis-aligned cuboid enclosing something: the read-only properties MinX to MaxZ, and from them Width, Height, Depth and Center. Its constructor takes the smallest coordinates first, then the largest: BoundingBox3D(minX, minY, minZ, maxX, maxY, maxZ).

It unites, overlaps, contains points, shifts by an Offset3D, compares and copies as its 2D counterpart does. HasOverlap() compares the inner volumes, so boxes touching at a face, an edge or a corner don’t overlap. There is no conversion to a volume yet.

from pyTooling.Cartesian3D import Point3D, LineSegment3D

line = LineSegment3D(Point3D(4, 1, -2), Point3D(-1, 3, 5))

line.BoundingBox                      # BoundingBox3D(-1, 1, -2, 4, 3, 5)
Point3D(0, 2, 0) in line.BoundingBox  # True

See also

BoundingBox2D

→ Its counterpart, the 2-dimensional bounding box.

Plane3D

A Plane3D is a plane, infinite in all directions within it. Like a 2D line, it is stored in Hesse normal form: a Normal (nx, ny, nz) of length 1, whose first non-zero component is positive, and the signed Distance d from the origin; the plane is all points (x, y, z) with nx * x + ny * y + nz * z = d. Plane3D(normal, distance) normalizes the normal and scales the distance the same way as Line2D(normal, distance), and stores the normal as a Vector3D.

Classmethod

Plane

FromPoints(first, second, third)

Through three points; the normal is the cross product of the offsets from the first point to the others. Three points on one line raise ValueError.

FromPointAndSpanningVectors(point, firstSpanningVector, secondSpanningVector)

Through a point, spanned by two vectors: the plane point + s * firstSpanningVector + t * secondSpanningVector. Parallel spanning vectors raise ValueError.

FromPointAndNormal(point, normal)

Through a point, perpendicular to a normal; a zero normal raises ValueError.

SignedDistance(), Project(), Intersects() and Intersection() (with a Ray3D) work as for a line, in absolute coordinates.

from pyTooling.Cartesian3D import Point3D, Offset3D, Plane3D, Ray3D

plane = Plane3D.FromPointAndSpanningVectors(Point3D(0, 0, 2), Offset3D(1, 0, 0), Offset3D(0, 3, -4))

str(plane)                                         # '0.0 * x + 0.8 * y + 0.6 * z = 1.2'
plane.SignedDistance(Point3D(0, 0, 0))             # -1.2
plane.Project(Point3D(0, 0, 0))                    # (0.0, 0.96, 0.72)
plane.Intersection(Ray3D(Point3D(0, 0, 0), Offset3D(0, 0, 1)))   # (0.0, 0.0, 2.0)

Three planes are derived classes with a fixed normal, each named after the coordinate plane it is parallel to; they keep their coordinate as given:

  • YZPlane3D - the plane x = c, normal (1, 0, 0); all its points have the same X;

  • XZPlane3D - the plane y = c, normal (0, 1, 0), with Y;

  • XYPlane3D - the plane z = c, normal (0, 0, 1), with Z.

Called on one of them, the classmethods FromPoints, FromPointAndSpanningVectors and FromPointAndNormal return that class and keep the coordinate as given; input that doesn’t fit the plane raises ValueError: YZPlane3D.FromPointAndNormal(Point3D(5, 1, 2), Vector3D(-1, 0, 0)) is YZPlane3D(5).

Like the 2D line, a plane is a value in absolute coordinates, compared geometrically and hashable: MirrorAt() mirrors an element of any group at any plane - a plane parallel to a coordinate plane flips one coordinate exactly, an oblique plane nx * x + ny * y + nz * z = d moves every absolute point by -2 * (nx * x + ny * y + nz * z - d) times the normal. FromElement creates the plane through a face or the center of an element’s absolute bounding box, named by an Alignment3D of the plane’s axis.

from pyTooling.Cartesian3D        import Point3D, Offset3D, Alignment3D, XYPlane3D, LineSegment3D
from pyTooling.Cartesian3D.Groups import Group3D

rod =   LineSegment3D(Point3D(0, 0, 0), Point3D(0, 0, 2))
part =  Group3D(Offset3D(10, 0, 1), (rod, ))
floor = XYPlane3D(0)

str(floor)                         # 'z = 0'
rod.MirrorAt(floor)                # rod.End is (0, 0, -4), relative to part
rod.End.Absolute                   # (10, 0, -3)

XYPlane3D.FromElement(rod, Alignment3D.MinZ)   # XYPlane3D(-3)

See also

Line2D

→ Its counterpart, the line in Hesse normal form.

Ray3D

A Ray3D is the 3D counterpart of a Ray2D: a half-line from a start point (Start) in a direction (Direction, a Vector3D), a value in absolute coordinates. PointAt() returns the point at a distance from the start point. Two rays are equal, if they start at the same point and their directions point the same way: all three components of the cross product are zero and the dot product is positive.

from pyTooling.Cartesian3D import Point3D, Offset3D, Ray3D

ray = Ray3D(Point3D(1, 2, 3), Offset3D(0, 3, 4))

str(ray)                           # '(1, 2, 3) + t * (0, 3, 4), t >= 0'
ray.PointAt(5)                     # (1.0, 5.0, 7.0)

Segment3D

Segment3D is a Start and an End point, copied on construction unless copyPoints=False. Either way, the segment owns its points; an assigned start or end point is referenced and owned too. It is abstract: a straight segment is a LineSegment3D.

BoundingBox encloses both points, and Move() shifts both by an Offset3D, in place.

See also

Segment2D

→ Its counterpart, the 2-dimensional segment.

LineSegment3D

LineSegment3D adds the measurements - length in three dimensions, the angle to another line segment, and conversion to an offset or a vector.

See also

Segment3D

→ Its base-class, the segment between two 3D points.

LineSegment2D

→ Its counterpart, the 2-dimensional line segment.

Ownership and Frames

Points belong to segments as in 2D: a segment owns its two points, a point owned by another segment raises AlreadyOwnedError, and the same point object given twice raises DuplicatePointError. Point3D(x, y, z, parent=segment) sets the owner without registering the point.

Points, segments and groups derive from the abstract Base3D, which holds the parent and returns the Frame; segments and groups also from Element3D. Subtracting points or measuring the angle between line segments of different frames raises NotInSameFrameError. The errors are the classes of pyTooling.Cartesian2D.

See also

Ownership and Frames

→ Its counterpart, where ownership and frames are explained.

Volumes

Attention

The volumes in pyTooling.Cartesian3D.Volumes are declared but not implemented. All three classes are empty: no corners, no constructor, no validation. They are not the 3D counterpart of the 2D shapes yet, and are listed here because they exist and can be subclassed, not because they can be used.

Volume

Volume is the empty base-class of every 3D volume, matching Shape.

See also

Shape

→ Its counterpart, the base-class of the 2-dimensional shapes.

Cuboid

Cuboid is intended as a volume bounded by six rectangles - the counterpart of Rectangle.

See also

Volume

→ Its base-class, the base-class of the 3-dimensional volumes.

Rectangle

→ Its counterpart, the trapezium with four right angles.

Cube

Cube is intended as a cuboid of six equally sized squares - the counterpart of Square.

See also

Cuboid

→ Its base-class, the volume a rectangle corresponds to.

Square

→ Its counterpart, the rectangle with four equal sides.

Groups

The groups live in pyTooling.Cartesian3D.Groups and work as in 2D: a group places segments and other groups at an offset, and the coordinates of its children are relative to the group. There is no 3D canvas yet, so a group without a parent is the root of a drawing, relative to the origin.

See also

Groups

→ Its counterpart, the 2D groups and the canvas.

Element3D

Element3D is the abstract base-class of everything a group can hold: segments and groups. An element belongs to at most one group, its Parent, and offers:

  • BoundingBox in its frame and AbsoluteBoundingBox relative to the origin;

  • Move(), shifting it in its frame;

  • MirrorX(), MirrorY() and MirrorZ(), mirroring it at a plane parallel to the yz-, xz- or xy-plane (the x-, y- or z-coordinates flip), through an edge or the center of its bounding box, named by an Alignment3D and optionally shifted by an offset;

  • MirrorAt(), mirroring it at an plane in absolute coordinates, which several elements can share;

  • MoveTo(), moving it into another group - by default keeping its absolute position, so its local coordinates change;

  • AlignTo(), moving it so an edge or the center of its bounding box aligns with another element’s - see Alignment3D. Both elements may be in different groups.

Assigning Parent moves an element into a group too, but keeps its local coordinates. A group can’t become its own descendant: that raises GroupCycleError. Every segment and group takes a keyword-only parameter parent: a group given there registers the new element, as AddChild() does.

An element carries an optional Value and arbitrary key-value-pairs (KeyValuePairsMixin), given by the keyword-only parameters value and keyValuePairs, as in 2D. Copy() and copy.copy() keep the value (the same object) and copy the key-value-pairs shallowly; copy.deepcopy() copies both deeply. Points carry neither. A copy isn’t in the group of the original.

from pyTooling.Cartesian3D import Point3D, LineSegment3D

wire = LineSegment3D(Point3D(0, 0, 0), Point3D(4, 0, 2), value="clock", keyValuePairs={"layer": 1})

wire["shielded"] = True

wire.Value                         # 'clock'
len(wire)                          # 2
wire.Copy()["layer"]               # 1

See also

Element2D

→ Its counterpart, the 2-dimensional element.

Group3D

A Group3D has an Offset and Children. Moving a group changes its offset only - the coordinates of its children stay as they are.

from pyTooling.Cartesian3D        import Point3D, Offset3D, LineSegment3D
from pyTooling.Cartesian3D.Groups import Group3D

rod =      LineSegment3D(Point3D(0, 0, 0), Point3D(0, 0, 2))
part =     Group3D(Offset3D(10, 0, 0), (rod, ))
assembly = Group3D(Offset3D(0, 5, 1), (part, ))

rod.End                            # (0, 0, 2), relative to part
rod.End.Absolute                   # (10, 5, 3)
assembly.BoundingBox               # BoundingBox3D(10, 5, 1, 10, 5, 3)
assembly.Move(Offset3D(100, 0, 0)) # rod.End is still (0, 0, 2)

Children are given to the constructor or added by AddChild(), and removed by RemoveChild(); a child of another group raises AlreadyOwnedError - use MoveTo() to move it -, removing an element of another group NotOwnedError. A group without children has no bounding box and raises EmptyGroupError. DistributeX(), DistributeY() and DistributeZ() move the children along that axis, so the gaps between them are equal. Subtracting points of different groups raises NotInSameFrameError; subtract their absolute positions instead. Copy() and copy.copy() copy a group with a copy of each child, copy.deepcopy() with deep copies of its children; the copy has no parent.

See also

Group2D

→ Its counterpart, the 2-dimensional group.