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
intorfloat.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
ArcSegment2Daround a center, reporting its radius, angles and length, and quadratic and cubic Bezier curves, with exact bounding boxes.Paths - a
Pathchains line segments, arcs and Bezier curves, open or closed, like SVG’s<path>; aPolylineis 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
Polygonwith any number of corners, the four-corneredTrapeziumnarrowed down to aSquare, aCircleand anEllipsealong 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
BoundingBox2Denclosing 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
Line2Dfrom 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 aVerticalLine2Dor aHorizontalLine2D, 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
Group2Dholds 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
Canvas2Dis 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()andcopy.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 -
ToTuplehands 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 |
|---|---|
|
A new |
|
A new |
|
An |
|
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
zcoordinate.
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).
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 |
|---|---|
|
The Euclidean length, a |
|
The dot product |
|
The cross product |
|
A new offset of length 1 in the same direction, with |
|
A new offset, scaled by the 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.
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:
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; |
|
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()).
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 |
|---|---|
|
A new |
|
|
|
|
|
A new |
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 |
|---|---|
Through two points; two points at the same position raise |
|
Through a point in a direction; a zero direction raises |
|
Through a point, perpendicular to a normal; a zero normal raises |
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 |
|---|---|
|
|
A new point, the foot of the perpendicular from the point to the line. |
|
|
|
A new point, where the ray hits the line. A ray parallel to the line, or pointing away from it, raises
|
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 linex = c, normal(1, 0); all its points have the sameX;HorizontalLine2D- the liney = c, normal(0, 1); all its points have the sameY.
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
Line2Dwith 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, afloateven for integer coordinates;AngleTo()- the angle to another line segment’s direction, from 0 tomath.pi; a line segment of length 0 has no direction and raisesValueError;ToOffset()- the segment as the displacement from start to end;ToVector()- the same as an immutableVector2D.
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:
centeranddirectionThe 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.arcPointA 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.heightand optionallychordPositionThe arc’s distance from its chord, the line segment from the start to the end point, measured perpendicular at a root point.
chordPositionplaces the root point on the chord, as a fraction from0.0at the start point to1.0at the end point; the default0.5is 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)
StartAngleandEndAngle- the angles of both points seen from the center, in radians, from -math.pitomath.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 newLineSegment2Dfrom 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.
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:
a
QuadraticBezierSegment2Dhas oneControlPoint-(start, controlPoint, end);a
CubicBezierSegment2Dhas two,ControlPoint1next to the start point andControlPoint2next to the end point -(start, controlPoint1, controlPoint2, end).
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 anOffset2D, 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.
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.
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), afloat;
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 signedheightover the chord, measured atchordPosition(default0.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 immutableVector2Dof 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. Asdand-ddescribe 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:
Perimeter- the length of its outline (circumference), afloat. 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)
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.
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:
BoundingBoxin its frame andAbsoluteBoundingBoxrelative to the origin;Move(), shifting it in its frame;MirrorX()andMirrorY(), 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 anAlignment2Dand 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 - seeAlignment2D. 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:
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- aTextAlignment: every line starts (Left, the default), is centered (Center) or ends (Right) at the position’s x-coordinate.Anchor- aVerticalAnchor: the point of the text at the position’s y-coordinate - theTopof the text, itsMiddle, theBaselineof its first line (the default, as in SVG) or itsBottom.LineHeight- the distance of two baselines as a factor of the font size,1.2by 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
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
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:
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'
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 |
|---|---|
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 |
|
|
Through a point, spanned by two vectors: the plane
|
Through a point, perpendicular to a normal; a zero normal raises |
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:
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.
Cube
Cube is intended as a cuboid of six equally sized squares - the counterpart
of Square.
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:
BoundingBoxin its frame andAbsoluteBoundingBoxrelative to the origin;Move(), shifting it in its frame;MirrorX(),MirrorY()andMirrorZ(), 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 anAlignment3Dand 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 - seeAlignment3D. 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.