Path
Mojo struct 🡭
Path
@memory_only
struct PathBuild with move_to/line_to/quad_curve_to/cubic_curve_to/arc_to/ close, or a whole shape at once with rect/round_rect/ellipse/ regular_polygon/arrow_head, then hand to fill_path/stroke_path/ stroke_path_aa. No chaining: each call is mut self returning nothing, like Canvas’s push_clip/set_pixel.
transformed maps a whole path into a new one, and extend folds
that new path into another – how a shape built at a convenient
size or orientation joins a larger path as its own sub-path,
sharing one fill with the rest rather than seaming against it.
All coordinates are absolute. There are no relative-to-current- point variants (SVG/Cairo’s rel_line_to and friends).
commands is the recorded sequence, one PathCommand per builder
call, readable by anything that wants to inspect or replay a path.
Treat it as read-only: the builder methods keep the current point
and sub-path start in step with it, and an appended command would
not.
Fields
- commands (
List[PathCommand])
Implemented traits
AnyType, Copyable, Deinitable, Movable
Methods
__init__
fn def __init__(out self)Args:
- self (
Self)
Returns:
Self
move_to
fn def move_to(mut self, x: Float64, y: Float64)Start a new sub-path at (x, y). Ends whatever sub-path was being built before (if any) without closing it – call close() first if a closed shape was intended.
Args:
- self (
Self) - x (
Float64): New sub-path’s starting point x. - y (
Float64): New sub-path’s starting point y.
line_to
fn def line_to(mut self, x: Float64, y: Float64)A straight segment from the current point to (x, y).
Args:
- self (
Self) - x (
Float64): Endpoint x. - y (
Float64): Endpoint y.
Raises:
Error: No move_to() has been called yet on this path.
quad_curve_to
fn def quad_curve_to(mut self, cx: Float64, cy: Float64, x: Float64, y: Float64)A quadratic Bezier from the current point to (x, y), pulled toward control point (cx, cy).
Args:
- self (
Self) - cx (
Float64): Control point x. - cy (
Float64): Control point y. - x (
Float64): Endpoint x. - y (
Float64): Endpoint y.
Raises:
Error: No move_to() has been called yet on this path.
cubic_curve_to
fn def cubic_curve_to(mut self, c1x: Float64, c1y: Float64, c2x: Float64, c2y: Float64, x: Float64, y: Float64)A cubic Bezier from the current point to (x, y), pulled toward control points (c1x, c1y) and (c2x, c2y).
Args:
- self (
Self) - c1x (
Float64): First control point x. - c1y (
Float64): First control point y. - c2x (
Float64): Second control point x. - c2y (
Float64): Second control point y. - x (
Float64): Endpoint x. - y (
Float64): Endpoint y.
Raises:
Error: No move_to() has been called yet on this path.
arc_to
fn def arc_to(mut self, cx: Float64, cy: Float64, radius: Float64, start_angle: Float64, end_angle: Float64)A circular arc segment, center (cx, cy), swept from start_angle to end_angle in radians – the same angle convention as draw_arc/fill_arc/fill_ring_sector, flattened through that family’s _arc_fpoints. An end_angle below start_angle sweeps the other way round, which is what a counter-clockwise corner or a reflected shape needs.
Unlike Cairo’s arc(), this inserts no connecting line from the
current point to the arc’s start. To join without a seam, call
move_to(cx + radiuscos(start_angle), cy +
radiussin(start_angle)) first, or end the previous segment
exactly there.
Args:
- self (
Self) - cx (
Float64): Arc’s center x. - cy (
Float64): Arc’s center y. - radius (
Float64): Arc’s radius in pixels. - start_angle (
Float64): Sweep start, radians, 0 pointing along +x. - end_angle (
Float64): Sweep end, radians. Belowstart_anglesweeps in decreasing angle.
Raises:
Error: No move_to() has been called yet on this path.
rect
fn def rect(mut self, x: Float64, y: Float64, width: Float64, height: Float64)Add a closed rectangular sub-path, clockwise from its top-left corner.
fill_rect is the primitive for an axis-aligned rectangle on
its own; this is for one that has to be part of a path. A
degenerate rectangle (zero or negative width or height) adds
nothing.
This describes the geometric rectangle [x, x+width] x
[y, y+height], and fill_path’s X-fill between a row’s crossings
is inclusive, so filling it covers column x+width – one more
than fill_rect(x, y, width, height), which stops at
x+width-1.
Args:
- self (
Self) - x (
Float64): Left edge. - y (
Float64): Top edge. - width (
Float64): Width in pixels. - height (
Float64): Height in pixels.
Raises:
Error: Never in practice – the internal line_to calls always follow this method’s own move_to.
round_rect
fn def round_rect(mut self, x: Float64, y: Float64, width: Float64, height: Float64, radius: Float64)Add a closed rectangular sub-path with rounded corners.
Corners are circular quarter-arcs through arc_to.
radius is clamped to half the shorter side. Past that the
corners overlap and the shape self-intersects, which under
EVEN_ODD punches holes in its own corners. At exactly half the
shorter side the shape is a stadium, or a circle when the
rectangle is square.
Args:
- self (
Self) - x (
Float64): Left edge. - y (
Float64): Top edge. - width (
Float64): Width in pixels. - height (
Float64): Height in pixels. - radius (
Float64): Corner radius, clamped to half the shorter side.
Raises:
Error: Never in practice – every internal call follows this method’s own move_to.
ellipse
fn def ellipse(mut self, cx: Float64, cy: Float64, rx: Float64, ry: Float64)Add a closed elliptical sub-path.
Four cubic Beziers, one per quadrant, with control points at the
standard kappa = 4/3 * (sqrt(2) - 1) offset – an approximation
with a maximum radial error about 0.027% of the radius, where
fill_ellipse_aa is exact. Use this for an ellipse that has to
be part of a path, and fill_ellipse_aa for a plain filled
one. arc_to cannot build it: it takes a single radius.
Args:
- self (
Self) - cx (
Float64): Center x. - cy (
Float64): Center y. - rx (
Float64): Horizontal radius in pixels. - ry (
Float64): Vertical radius in pixels.
Raises:
Error: Never in practice – every internal call follows this method’s own move_to.
regular_polygon
fn def regular_polygon(mut self, cx: Float64, cy: Float64, radius: Float64, sides: Int, rotation: Float64 = 0)Add a closed regular polygon: sides vertices on the circle of radius about (cx, cy), vertex k at angle rotation + 2 * pi * k / sides, in this package’s angle convention (0 along +x, increasing clockwise on screen because y grows downward). rotation = 0.0 puts the first vertex at three o’clock; -pi / 2 puts it at twelve, which makes a pointy-top hexagon or an upright triangle, and pi / 4 turns a square into a diamond.
Wound the same way as rect and ellipse, so a polygon and a
hole cut from it behave under FillRule.NONZERO as those do.
A hexagon that is regular in data space and drawn through two
different axis scales is not regular in pixels: build it at
unit radius and map it with transformed and a Transform2D
carrying the two scales.
That advice assumes the mapping is affine, which a
Transform2D always is but a caller’s own data-to-pixel scale
might not be – a logarithmic axis, for one. A Transform2D
built from such a scale’s slope and intercept places this
polygon’s vertices on the straight line the scale is not,
correct only where the two happen to agree. Check for that
case rather than assuming a scale is affine because most are.
Args:
- self (
Self) - cx (
Float64): Center x. - cy (
Float64): Center y. - radius (
Float64): Distance from the center to each vertex, in pixels. - sides (
Int): Vertex count, at least 3. - rotation (
Float64): Angle of the first vertex, radians.
Raises:
Error: sides is less than 3.
arrow_head
fn def arrow_head(mut self, tip_x: Float64, tip_y: Float64, direction_x: Float64, direction_y: Float64, length: Float64, half_width: Float64)Add a closed arrowhead: the triangle whose tip is at (tip_x, tip_y), pointing along (direction_x, direction_y), with its base length back from the tip and 2 * half_width wide. The direction is normalized here, so a caller passes the shaft’s own vector.
One path filled once rather than three strokes, because an anti-aliased edge is half-covered and pieces that meet along one show a pale seam. The same applies where the shaft meets the head: stroke the shaft to the tip, under the head, not to the base, so the head covers the join and the seam has nowhere to show.
A builder rather than a stroke cap so it serializes the same
way on every backend; an SVG <marker> would be one backend’s
feature.
Args:
- self (
Self) - tip_x (
Float64): The tip’s x. - tip_y (
Float64): The tip’s y. - direction_x (
Float64): The x of the direction the arrow points in; any length. - direction_y (
Float64): The y of that direction. - length (
Float64): Tip-to-base distance in pixels. - half_width (
Float64): Half the base width in pixels.
Raises:
Error: The direction is the zero vector.
curve_through
fn def curve_through(mut self, points: List[FPoint], tension: Float64 = 1)Add a smooth open sub-path through points: a move_to to the first, then one cubic Bezier per consecutive pair with Catmull-Rom tangents, so the curve passes through every point.
Each segment’s control points are the endpoint plus or minus
(next - previous) / 6 * tension, the first and last points
taking a one-sided tangent. tension scales the tangents
directly: 1.0 is the textbook Catmull-Rom curve, 0.5 bows half
as far, and 0.0 (or less) takes an explicit line_to branch, so
a polyline built this way is command-for-command the one
line_to builds rather than a flattened straight cubic, which
samples at different intermediate points.
curve_to_through is the same curve continued from the
current point, for a shape with more than one smoothed edge on
one sub-path.
Args:
- self (
Self) - points (
List[FPoint]): Points to pass through, in order. Empty adds nothing; a single point adds only the move_to. - tension (
Float64): Tangent scale, 1.0 for Catmull-Rom.
Raises:
Error: Never in practice – every internal call follows this method’s own move_to.
curve_to_through
fn def curve_to_through(mut self, points: List[FPoint], tension: Float64 = 1)Continue the current sub-path with a smooth curve from the current point through points, with no move_to: the continuation form of curve_through, whose tangents and tension it shares. line_to is to move_to as this is to curve_through.
For a closed shape with more than one smoothed edge, a band
between two series for instance: curve_through the top edge,
curve_to_through the bottom one back the other way, close.
One sub-path, so the fill is one shape and the two edges meet
without a seam.
The current point is the curve’s first point, so the tangent
at the join is one-sided there, as curve_through’s is at its
first point. The commands are exactly those curve_through
would add for the current point followed by points, minus
its move_to.
Args:
- self (
Self) - points (
List[FPoint]): Points to pass through after the current point, in order. Empty adds nothing. - tension (
Float64): Tangent scale, 1.0 for Catmull-Rom.
Raises:
Error: No move_to() has been called yet on this path.
transformed
fn def transformed(self, transform: Transform2D) -> SelfThis path mapped through transform, as a new path.
Bezier control points map directly, since an affine transform of
a Bezier is the Bezier of the transformed control points.
arc_to describes a circular arc by center, radius and angles,
and only a transform that maps circles to circles folds back into
those five numbers:
- Equal scale magnitudes: exact. The center maps, the radius
scales, and the angles shift by the transform’s rotation. A
negative scale mirrors the angles first – a y-flip maps an
angle a to -a, an x-flip to pi - a, and both together to
a + pi – so a reflected arc comes out swept the other way,
which
arc_toexpresses as a decreasing angle. - Unequal magnitudes: flattened. The arc becomes an elliptical
arc, which
arc_tocannot express and no primitive here draws.
Flattening goes through the same _arc_fpoints the renderer
would have used, so the drawn result is unchanged; what is lost
is the ability to transform the result again exactly.
Args:
- self (
Self) - transform (
Transform2D): Mapping applied to every point.
Returns:
Self: A new path in the transformed space.
Raises:
Error: Never in practice – every call below follows this path’s own commands, which were already well-formed.
fn def transformed(self, matrix: Matrix2D) -> SelfThis path mapped through matrix, as a new path – the general-affine counterpart of the Transform2D overload, and what every drawing call applies to a path under a canvas transform.
Bezier control points map directly. An arc_to stays an arc
when the matrix keeps circles circular (a rotation, a uniform
scale, a translation, or a mirror of those): its center maps,
its radius scales, its start angle is read off the mapped start
point, and its sweep keeps its size, reversing under a mirror.
Any other matrix turns the arc into an ellipse, which arc_to
cannot hold, so it is flattened through the same _arc_fpoints
the renderer would use and its points are mapped.
Args:
- self (
Self) - matrix (
Matrix2D): Mapping applied to every point.
Returns:
Self: A new path in the transformed space.
Raises:
Error: Never in practice – every call below follows this path’s own commands, which were already well-formed.
bounds
fn def bounds(self, curve_steps: Int = Int(0)) -> Tuple[Float64, Float64, Float64, Float64]The axis-aligned box the path’s flattened outline spans, as (min_x, min_y, max_x, max_y) in canvas coordinates.
Measured on the same flattening the fills draw, so a curve’s box follows the curve and not its control points, which can lie well outside it. An empty path returns all zeros.
Args:
- self (
Self) - curve_steps (
Int): Straight-line segments per quad/cubic Bezier; 0 (the default) chooses per segment, as the fills do.
Returns:
Tuple[Float64, Float64, Float64, Float64]: (min_x, min_y, max_x, max_y).
stroke_bounds
fn def stroke_bounds(self, width: Float64 = 1, curve_steps: Int = Int(0), dashes: List[Float64] = List(), dash_offset: Float64 = 0, cap: LineCap = LineCap.ROUND, join: LineJoin = LineJoin.ROUND, miter_limit: Float64 = 4) -> Tuple[Float64, Float64, Float64, Float64]The axis-aligned box stroke_path_aa paints with the same arguments, as (min_x, min_y, max_x, max_y) in canvas coordinates.
Measured on the stroke’s own outline, so it includes what
bounds() padded by half the width would miss or overstate: a
SQUARE or ROUND cap’s overhang past an endpoint, a MITER
join’s spike past a corner, and a dash pattern that ends
short of the path. A round cap or join is polygonized the way
the stroke draws it, so its extent is within a hundredth of a
pixel of the true disk’s. A single-point sub-path
contributes its point; an empty path returns all zeros.
Args:
- self (
Self) - width (
Float64): Stroke width in pixels. - curve_steps (
Int): Straight-line segments per quad/cubic Bezier; 0 (the default) chooses per segment, as the strokes do. - dashes (
List[Float64]): On/off segment lengths in pixels, cycled along the stroke. Empty (default) measures a solid stroke. - dash_offset (
Float64): Distance into the dash pattern the stroke starts at. - cap (
LineCap): How an open sub-path’s two ends are finished – see LineCap. - join (
LineJoin): How corners are turned – see LineJoin. - miter_limit (
Float64): Ratio past which a MITER join falls back to BEVEL, as a multiple of half the stroke width.
Returns:
Tuple[Float64, Float64, Float64, Float64]: (min_x, min_y, max_x, max_y).
in_fill
fn def in_fill(self, x: Float64, y: Float64, fill_rule: FillRule = FillRule.EVEN_ODD, curve_steps: Int = Int(0)) -> BoolWhether (x, y) lies inside the region fill_path_aa paints under fill_rule: the point’s winding number over every sub-path, each treated as closed, on the same flattening the fills use.
The boundary is half-open, as the fills are: a point on a sub-path’s left or top edge is inside, one on its right or bottom edge is not.
Args:
- self (
Self) - x (
Float64): Point x, in the path’s coordinates. - y (
Float64): Point y. - fill_rule (
FillRule): EVEN_ODD (the default) or NONZERO, as for the fills. - curve_steps (
Int): Straight-line segments per quad/cubic Bezier; 0 (the default) chooses per segment, as the fills do.
Returns:
Bool: True when the point is inside.
in_stroke
fn def in_stroke(self, x: Float64, y: Float64, width: Float64 = 1, curve_steps: Int = Int(0), dashes: List[Float64] = List(), dash_offset: Float64 = 0, cap: LineCap = LineCap.ROUND, join: LineJoin = LineJoin.ROUND, miter_limit: Float64 = 4) -> BoolWhether (x, y) lies inside the stroke stroke_path_aa paints with the same arguments: the nonzero winding number of the point over the stroke’s outline, caps, joins and dashes included, built the way the stroke draws it. A single-point sub-path is the one pixel the stroke sets there.
Args:
- self (
Self) - x (
Float64): Point x, in the path’s coordinates. - y (
Float64): Point y. - width (
Float64): Stroke width in pixels. - curve_steps (
Int): Straight-line segments per quad/cubic Bezier; 0 (the default) chooses per segment, as the strokes do. - dashes (
List[Float64]): On/off segment lengths in pixels, cycled along the stroke. Empty (default) tests a solid stroke. - dash_offset (
Float64): Distance into the dash pattern the stroke starts at. - cap (
LineCap): How an open sub-path’s two ends are finished – see LineCap. - join (
LineJoin): How corners are turned – see LineJoin. - miter_limit (
Float64): Ratio past which a MITER join falls back to BEVEL, as a multiple of half the stroke width.
Returns:
Bool: True when the point is inside the stroke.
extend
fn def extend(mut self, other: Self)Append other’s commands onto this path, and adopt its current point and sub-path start so the builder state stays in step with commands – the thing a caller cannot do from outside, since those fields are private.
No implicit connection between the two: other begins with
its own move_to, so it becomes its own sub-path rather than
continuing this one, and a close() afterward closes
other’s sub-path, not whatever this path held before.
The case this exists for is transformed, which returns a
new path: a shape built at a convenient size or orientation,
mapped into place, then folded into a larger path so it shares
one fill with its neighbors and leaves no seam between them
(see regular_polygon’s docstring for a worked case, and
arrow_head’s for why a shared fill matters at all).
Extending by an empty other (no move_to called on it) is a
no-op: this path’s current point and sub-path start are left
alone.
Args:
- self (
Self) - other (
Self): The path whose commands to append. Left unchanged; nothing here mutates it.
Raises:
Error: Never in practice – other’s commands are already
well-formed, having been built through this same API.
close
fn def close(mut self)Draw a straight segment back to this sub-path’s move_to and mark it closed. stroke_path/stroke_path_aa then draw it as a polygon rather than an open polyline. No effect on fill_path, which treats every sub-path as implicitly closed.
Args:
- self (
Self)
Raises: