Skip to content
Path

Path

Mojo struct 🡭

Path

@memory_only
struct Path

Build 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. Below start_angle sweeps 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) -> Self

This 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_to expresses as a decreasing angle.
  • Unequal magnitudes: flattened. The arc becomes an elliptical arc, which arc_to cannot 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) -> Self

This 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)) -> Bool

Whether (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) -> Bool

Whether (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: