Skip to content

Geometry

momapy.geometry

Geometric primitives and transformations for momapy.

This module provides geometric classes and functions for working with points, lines, segments, curves, and transformations. It includes support for Bezier curves, elliptical arcs, and various geometric operations.

Examples:

from momapy.geometry import Point, Line, Segment, Rotation, Translation

# Create points
p1 = Point(0, 0)
p2 = Point(10, 10)

# Create a line
line = Line(p1, p2)
line.slope()

# Create a segment
segment = Segment(p1, p2)
segment.length()

# Apply transformations
rotated = p1.transformed(Rotation(math.pi / 2, p2))

Classes:

Name Description
Bbox

Represents a bounding box.

CubicBezierCurve

Represents a cubic Bezier curve.

EllipticalArc

Represents an elliptical arc.

GeometryObject

Abstract base class for all geometry objects.

Line

Represents an infinite line defined by two points.

MatrixTransformation

Represents a transformation as a 3x3 matrix.

Point

Represents a 2D point with x and y coordinates.

QuadraticBezierCurve

Represents a quadratic Bezier curve.

Rotation

Represents a rotation transformation.

Scaling

Represents a scaling transformation.

Segment

Represents a line segment between two points.

Transformation

Abstract base class for geometric transformations.

Translation

Represents a translation transformation.

Functions:

Name Description
get_normalized_angle

Normalize an angle to [0, 2*pi).

get_primitives_anchor_point

Get an anchor point of geometry primitives.

get_primitives_angle

Get the border point at a given angle.

get_primitives_border

Get the border point of geometry primitives in a given direction.

get_transformation_for_frame

Get transformation for a frame defined by origin and axes.

Bbox dataclass

Bbox(position: Point, width: float, height: float)

Bases: object

Represents a bounding box.

Attributes:

Name Type Description
position Point

Center point.

width float

Width of the box.

height float

Height of the box.

Examples:

bbox = Bbox(Point(5, 5), 10, 10)
bbox.north_west()
bbox.south_east()

Methods:

Name Description
anchor_point

Get a named anchor point.

around_points

Create a minimal Bbox enclosing all given points.

center

Get the center point.

east

Get the east (right center) point.

east_north_east

Get the east-north-east point.

east_south_east

Get the east-south-east point.

isnan

Check if the position has NaN coordinates.

north

Get the north (top center) point.

north_east

Get the north-east corner.

north_north_east

Get the north-north-east point.

north_north_west

Get the north-north-west point.

north_west

Get the north-west corner.

size

Get the size as (width, height).

south

Get the south (bottom center) point.

south_east

Get the south-east corner.

south_south_east

Get the south-south-east point.

south_south_west

Get the south-south-west point.

south_west

Get the south-west corner.

union

Create a Bbox enclosing all given bounding boxes.

west

Get the west (left center) point.

west_north_west

Get the west-north-west point.

west_south_west

Get the west-south-west point.

anchor_point

anchor_point(anchor_point: str) -> Point

Get a named anchor point.

Parameters:

Name Type Description Default
anchor_point str

Name like 'north', 'south_east', 'center', etc.

required

Returns:

Type Description
Point

The anchor point.

Source code in src/momapy/geometry.py
def anchor_point(self, anchor_point: str) -> Point:
    """Get a named anchor point.

    Args:
        anchor_point: Name like 'north', 'south_east', 'center', etc.

    Returns:
        The anchor point.
    """
    return getattr(self, anchor_point)()

around_points classmethod

around_points(points: Iterable[Point]) -> Bbox

Create a minimal Bbox enclosing all given points.

Parameters:

Name Type Description Default
points Iterable[Point]

An iterable of Point objects.

required

Returns:

Type Description
Bbox

A new Bbox that tightly encloses all input points.

Raises:

Type Description
ValueError

If the iterable is empty.

Source code in src/momapy/geometry.py
@classmethod
def around_points(cls, points: collections.abc.Iterable[Point]) -> "Bbox":
    """Create a minimal Bbox enclosing all given points.

    Args:
        points: An iterable of Point objects.

    Returns:
        A new Bbox that tightly encloses all input points.

    Raises:
        ValueError: If the iterable is empty.
    """
    it = iter(points)
    try:
        first = next(it)
    except StopIteration:
        raise ValueError("points must contain at least one point")
    min_x = max_x = first.x
    min_y = max_y = first.y
    for p in it:
        if p.x < min_x:
            min_x = p.x
        elif p.x > max_x:
            max_x = p.x
        if p.y < min_y:
            min_y = p.y
        elif p.y > max_y:
            max_y = p.y
    return cls(
        Point((min_x + max_x) / 2, (min_y + max_y) / 2),
        max_x - min_x,
        max_y - min_y,
    )

center

center() -> Point

Get the center point.

Source code in src/momapy/geometry.py
def center(self) -> Point:
    """Get the center point."""
    return Point(self.x, self.y)

east

east() -> Point

Get the east (right center) point.

Source code in src/momapy/geometry.py
def east(self) -> Point:
    """Get the east (right center) point."""
    return Point(self.x + self.width / 2, self.y)

east_north_east

east_north_east() -> Point

Get the east-north-east point.

Source code in src/momapy/geometry.py
def east_north_east(self) -> Point:
    """Get the east-north-east point."""
    return Point(self.x + self.width / 2, self.y - self.height / 4)

east_south_east

east_south_east() -> Point

Get the east-south-east point.

Source code in src/momapy/geometry.py
def east_south_east(self) -> Point:
    """Get the east-south-east point."""
    return Point(self.x + self.width / 2, self.y + self.height / 4)

isnan

isnan() -> bool

Check if the position has NaN coordinates.

Returns:

Type Description
bool

True if position has NaN.

Source code in src/momapy/geometry.py
def isnan(self) -> bool:
    """Check if the position has NaN coordinates.

    Returns:
        True if position has NaN.
    """
    return self.position.isnan()

north

north() -> Point

Get the north (top center) point.

Source code in src/momapy/geometry.py
def north(self) -> Point:
    """Get the north (top center) point."""
    return Point(self.x, self.y - self.height / 2)

north_east

north_east() -> Point

Get the north-east corner.

Source code in src/momapy/geometry.py
def north_east(self) -> Point:
    """Get the north-east corner."""
    return Point(self.x + self.width / 2, self.y - self.height / 2)

north_north_east

north_north_east() -> Point

Get the north-north-east point.

Source code in src/momapy/geometry.py
def north_north_east(self) -> Point:
    """Get the north-north-east point."""
    return Point(self.x + self.width / 4, self.y - self.height / 2)

north_north_west

north_north_west() -> Point

Get the north-north-west point.

Source code in src/momapy/geometry.py
def north_north_west(self) -> Point:
    """Get the north-north-west point."""
    return Point(self.x - self.width / 4, self.y - self.height / 2)

north_west

north_west() -> Point

Get the north-west corner.

Source code in src/momapy/geometry.py
def north_west(self) -> Point:
    """Get the north-west corner."""
    return Point(self.x - self.width / 2, self.y - self.height / 2)

size

size() -> tuple[float, float]

Get the size as (width, height).

Returns:

Type Description
tuple[float, float]

Tuple of (width, height).

Source code in src/momapy/geometry.py
def size(self) -> tuple[float, float]:
    """Get the size as (width, height).

    Returns:
        Tuple of (width, height).
    """
    return (self.width, self.height)

south

south() -> Point

Get the south (bottom center) point.

Source code in src/momapy/geometry.py
def south(self) -> Point:
    """Get the south (bottom center) point."""
    return Point(self.x, self.y + self.height / 2)

south_east

south_east() -> Point

Get the south-east corner.

Source code in src/momapy/geometry.py
def south_east(self) -> Point:
    """Get the south-east corner."""
    return Point(self.x + self.width / 2, self.y + self.height / 2)

south_south_east

south_south_east() -> Point

Get the south-south-east point.

Source code in src/momapy/geometry.py
def south_south_east(self) -> Point:
    """Get the south-south-east point."""
    return Point(self.x + self.width / 4, self.y + self.height / 2)

south_south_west

south_south_west() -> Point

Get the south-south-west point.

Source code in src/momapy/geometry.py
def south_south_west(self) -> Point:
    """Get the south-south-west point."""
    return Point(self.x - self.width / 4, self.y + self.height / 2)

south_west

south_west() -> Point

Get the south-west corner.

Source code in src/momapy/geometry.py
def south_west(self) -> Point:
    """Get the south-west corner."""
    return Point(self.x - self.width / 2, self.y + self.height / 2)

union classmethod

union(bboxes: list[Bbox]) -> Bbox

Create a Bbox enclosing all given bounding boxes.

Parameters:

Name Type Description Default
bboxes list[Bbox]

List of Bbox objects to merge.

required

Returns:

Type Description
Bbox

A new Bbox enclosing all input bboxes.

Source code in src/momapy/geometry.py
@classmethod
def union(cls, bboxes: list["Bbox"]) -> "Bbox":
    """Create a Bbox enclosing all given bounding boxes.

    Args:
        bboxes: List of Bbox objects to merge.

    Returns:
        A new Bbox enclosing all input bboxes.
    """
    if not bboxes:
        return cls(Point(0, 0), 0, 0)
    corners = []
    for bbox in bboxes:
        corners.append(bbox.north_west())
        corners.append(bbox.south_east())
    return cls.around_points(corners)

west

west() -> Point

Get the west (left center) point.

Source code in src/momapy/geometry.py
def west(self) -> Point:
    """Get the west (left center) point."""
    return Point(self.x - self.width / 2, self.y)

west_north_west

west_north_west() -> Point

Get the west-north-west point.

Source code in src/momapy/geometry.py
def west_north_west(self) -> Point:
    """Get the west-north-west point."""
    return Point(self.x - self.width / 2, self.y - self.height / 4)

west_south_west

west_south_west() -> Point

Get the west-south-west point.

Source code in src/momapy/geometry.py
def west_south_west(self) -> Point:
    """Get the west-south-west point."""
    return Point(self.x - self.width / 2, self.y + self.height / 4)

x property

x: float

The x coordinate of the center.

y property

y: float

The y coordinate of the center.

CubicBezierCurve dataclass

CubicBezierCurve(p1: Point, p2: Point, control_point1: Point, control_point2: Point)

Bases: GeometryObject

Represents a cubic Bezier curve.

Attributes:

Name Type Description
p1 Point

Start point.

p2 Point

End point.

control_point1 Point

First control point.

control_point2 Point

Second control point.

Examples:

curve = CubicBezierCurve(
    Point(0, 0),
    Point(10, 0),
    Point(3, 5),
    Point(7, 5)
)

Methods:

Name Description
bbox

Get the bounding box of the curve.

derivative

Compute the derivative at parameter t.

evaluate

Evaluate the curve at parameter t.

evaluate_multi

Evaluate the curve at multiple parameters.

get_angle_at_fraction

Get angle at a fraction of the arc length.

get_intersection_with_line

Get intersection with a line.

get_position_and_angle_at_fraction

Get both position and angle at a fraction of the arc length.

get_position_at_fraction

Get point at a fraction of the arc length.

length

Calculate the arc length of the curve.

reversed

Return a reversed copy of the curve.

shortened

Return a shortened copy of the curve.

split

Split the curve at parameter t using De Casteljau subdivision.

transformed

Apply a transformation to this curve.

bbox

bbox() -> Bbox

Get the bounding box of the curve.

Returns:

Type Description
Bbox

A Bbox enclosing the curve.

Source code in src/momapy/geometry.py
def bbox(self) -> "Bbox":
    """Get the bounding box of the curve.

    Returns:
        A Bbox enclosing the curve.
    """
    xs = [self.p1.x, self.p2.x]
    ys = [self.p1.y, self.p2.y]
    for values, candidates in [
        (
            (self.p1.x, self.control_point1.x, self.control_point2.x, self.p2.x),
            xs,
        ),
        (
            (self.p1.y, self.control_point1.y, self.control_point2.y, self.p2.y),
            ys,
        ),
    ]:
        p0, p1, p2, p3 = values
        a = -3 * p0 + 9 * p1 - 9 * p2 + 3 * p3
        b = 6 * p0 - 12 * p1 + 6 * p2
        c = -3 * p0 + 3 * p1
        if abs(a) < ZERO_TOLERANCE:
            if abs(b) > ZERO_TOLERANCE:
                t = -c / b
                if 0 < t < 1:
                    point = self.evaluate(t)
                    candidates.append(point.x if candidates is xs else point.y)
        else:
            disc = b * b - 4 * a * c
            if disc >= 0:
                sqrt_disc = math.sqrt(disc)
                for t in [(-b + sqrt_disc) / (2 * a), (-b - sqrt_disc) / (2 * a)]:
                    if 0 < t < 1:
                        point = self.evaluate(t)
                        candidates.append(point.x if candidates is xs else point.y)
    min_x, max_x = min(xs), max(xs)
    min_y, max_y = min(ys), max(ys)
    return Bbox(
        Point(min_x, min_y),
        max_x - min_x,
        max_y - min_y,
    )

derivative

derivative(t: float) -> tuple[float, float]

Compute the derivative at parameter t.

Parameters:

Name Type Description Default
t float

Parameter value from 0 to 1.

required

Returns:

Type Description
tuple[float, float]

Tuple of (dx/dt, dy/dt).

Source code in src/momapy/geometry.py
def derivative(self, t: float) -> tuple[float, float]:
    """Compute the derivative at parameter t.

    Args:
        t: Parameter value from 0 to 1.

    Returns:
        Tuple of (dx/dt, dy/dt).
    """
    u = 1 - t
    dx = (
        3 * u * u * (self.control_point1.x - self.p1.x)
        + 6 * u * t * (self.control_point2.x - self.control_point1.x)
        + 3 * t * t * (self.p2.x - self.control_point2.x)
    )
    dy = (
        3 * u * u * (self.control_point1.y - self.p1.y)
        + 6 * u * t * (self.control_point2.y - self.control_point1.y)
        + 3 * t * t * (self.p2.y - self.control_point2.y)
    )
    return (dx, dy)

evaluate

evaluate(t: float) -> Point

Evaluate the curve at parameter t.

Parameters:

Name Type Description Default
t float

Parameter value from 0 to 1.

required

Returns:

Type Description
Point

The point at parameter t.

Source code in src/momapy/geometry.py
def evaluate(self, t: float) -> Point:
    """Evaluate the curve at parameter t.

    Args:
        t: Parameter value from 0 to 1.

    Returns:
        The point at parameter t.
    """
    return self.evaluate_multi([t])[0]

evaluate_multi

evaluate_multi(t_sequence: Sequence[float]) -> list[Point]

Evaluate the curve at multiple parameters.

Parameters:

Name Type Description Default
t_sequence Sequence[float]

Sequence of parameter values.

required

Returns:

Type Description
list[Point]

List of points.

Source code in src/momapy/geometry.py
def evaluate_multi(
    self, t_sequence: collections.abc.Sequence[float]
) -> list[Point]:
    """Evaluate the curve at multiple parameters.

    Args:
        t_sequence: Sequence of parameter values.

    Returns:
        List of points.
    """
    t = numpy.asarray(t_sequence, dtype="double")
    u = 1 - t
    u2 = u * u
    t2 = t * t
    x = (
        u2 * u * self.p1.x
        + 3 * u2 * t * self.control_point1.x
        + 3 * u * t2 * self.control_point2.x
        + t2 * t * self.p2.x
    )
    y = (
        u2 * u * self.p1.y
        + 3 * u2 * t * self.control_point1.y
        + 3 * u * t2 * self.control_point2.y
        + t2 * t * self.p2.y
    )
    return [Point(float(xi), float(yi)) for xi, yi in zip(x, y)]

get_angle_at_fraction

get_angle_at_fraction(fraction: float) -> float

Get angle at a fraction of the arc length.

Parameters:

Name Type Description Default
fraction float

Fraction from 0 to 1.

required

Returns:

Type Description
float

Angle in radians.

Source code in src/momapy/geometry.py
def get_angle_at_fraction(self, fraction: float) -> float:
    """Get angle at a fraction of the arc length.

    Args:
        fraction: Fraction from 0 to 1.

    Returns:
        Angle in radians.
    """
    total = self.length()
    if fraction <= 0:
        t = 0.0
    elif fraction >= 1:
        t = 1.0
    else:
        t = _find_t_at_arc_length_fraction(self.derivative, total, fraction)
    dx, dy = self.derivative(t)
    return math.atan2(dy, dx)

get_intersection_with_line

get_intersection_with_line(line: Line) -> list[Point] | list[Segment]

Get intersection with a line.

Parameters:

Name Type Description Default
line Line

The line to intersect with.

required

Returns:

Type Description
list[Point] | list[Segment]

List of intersection points or segments.

Source code in src/momapy/geometry.py
def get_intersection_with_line(self, line: Line) -> list[Point] | list[Segment]:
    """Get intersection with a line.

    Args:
        line: The line to intersect with.

    Returns:
        List of intersection points or segments.
    """
    a = line.p2.y - line.p1.y
    b = line.p1.x - line.p2.x
    c = line.p2.x * line.p1.y - line.p1.x * line.p2.y
    p0x, p0y = self.p1.x, self.p1.y
    p1x, p1y = self.control_point1.x, self.control_point1.y
    p2x, p2y = self.control_point2.x, self.control_point2.y
    p3x, p3y = self.p2.x, self.p2.y
    c3x = -p0x + 3 * p1x - 3 * p2x + p3x
    c2x = 3 * p0x - 6 * p1x + 3 * p2x
    c1x = -3 * p0x + 3 * p1x
    c0x = p0x
    c3y = -p0y + 3 * p1y - 3 * p2y + p3y
    c2y = 3 * p0y - 6 * p1y + 3 * p2y
    c1y = -3 * p0y + 3 * p1y
    c0y = p0y
    coeff3 = a * c3x + b * c3y
    coeff2 = a * c2x + b * c2y
    coeff1 = a * c1x + b * c1y
    coeff0 = a * c0x + b * c0y + c
    roots = numpy.roots([coeff3, coeff2, coeff1, coeff0])
    result = []
    for root in roots:
        if abs(root.imag) < CONVERGENCE_TOLERANCE:
            t = root.real
            if -PARAMETER_TOLERANCE <= t <= 1 + PARAMETER_TOLERANCE:
                t = max(0.0, min(1.0, t))
                result.append(self.evaluate(t))
    return result

get_position_and_angle_at_fraction

get_position_and_angle_at_fraction(fraction: float) -> tuple[Point, float]

Get both position and angle at a fraction of the arc length.

Parameters:

Name Type Description Default
fraction float

Fraction from 0 to 1.

required

Returns:

Type Description
tuple[Point, float]

Tuple of (point, angle_in_radians).

Source code in src/momapy/geometry.py
def get_position_and_angle_at_fraction(
    self, fraction: float
) -> tuple[Point, float]:
    """Get both position and angle at a fraction of the arc length.

    Args:
        fraction: Fraction from 0 to 1.

    Returns:
        Tuple of (point, angle_in_radians).
    """
    total = self.length()
    if fraction <= 0:
        t = 0.0
    elif fraction >= 1:
        t = 1.0
    else:
        t = _find_t_at_arc_length_fraction(self.derivative, total, fraction)
    pos = self.evaluate(t)
    dx, dy = self.derivative(t)
    angle = math.atan2(dy, dx)
    return (pos, angle)

get_position_at_fraction

get_position_at_fraction(fraction: float) -> Point

Get point at a fraction of the arc length.

Parameters:

Name Type Description Default
fraction float

Fraction from 0 to 1.

required

Returns:

Type Description
Point

The point at that fraction.

Source code in src/momapy/geometry.py
def get_position_at_fraction(self, fraction: float) -> Point:
    """Get point at a fraction of the arc length.

    Args:
        fraction: Fraction from 0 to 1.

    Returns:
        The point at that fraction.
    """
    if fraction <= 0:
        return Point(self.p1.x, self.p1.y)
    if fraction >= 1:
        return Point(self.p2.x, self.p2.y)
    total = self.length()
    t = _find_t_at_arc_length_fraction(self.derivative, total, fraction)
    return self.evaluate(t)

length

length() -> float

Calculate the arc length of the curve.

Returns:

Type Description
float

The arc length.

Source code in src/momapy/geometry.py
def length(self) -> float:
    """Calculate the arc length of the curve.

    Returns:
        The arc length.
    """
    return _arc_length(self.derivative, 0.0, 1.0)

reversed

reversed() -> CubicBezierCurve

Return a reversed copy of the curve.

Returns:

Type Description
CubicBezierCurve

A new CubicBezierCurve going in reverse direction.

Source code in src/momapy/geometry.py
def reversed(self) -> "CubicBezierCurve":
    """Return a reversed copy of the curve.

    Returns:
        A new CubicBezierCurve going in reverse direction.
    """
    return CubicBezierCurve(
        self.p2, self.p1, self.control_point2, self.control_point1
    )

shortened

shortened(length: float, start_or_end: Literal['start', 'end'] = 'end') -> CubicBezierCurve

Return a shortened copy of the curve.

Parameters:

Name Type Description Default
length float

Amount to shorten by.

required
start_or_end Literal['start', 'end']

Which end to shorten from.

'end'

Returns:

Type Description
CubicBezierCurve

A new shortened CubicBezierCurve.

Source code in src/momapy/geometry.py
def shortened(
    self, length: float, start_or_end: typing.Literal["start", "end"] = "end"
) -> "CubicBezierCurve":
    """Return a shortened copy of the curve.

    Args:
        length: Amount to shorten by.
        start_or_end: Which end to shorten from.

    Returns:
        A new shortened CubicBezierCurve.
    """
    if length == 0 or self.length() == 0:
        return copy.deepcopy(self)
    if start_or_end == "start":
        return self.reversed().shortened(length).reversed()
    total_length = self.length()
    if length > total_length:
        length = total_length
    fraction = 1 - length / total_length
    t = _find_t_at_arc_length_fraction(self.derivative, total_length, fraction)
    left, _ = self.split(t)
    return left

split

split(t: float) -> tuple[CubicBezierCurve, CubicBezierCurve]

Split the curve at parameter t using De Casteljau subdivision.

Parameters:

Name Type Description Default
t float

Parameter value from 0 to 1.

required

Returns:

Type Description
tuple[CubicBezierCurve, CubicBezierCurve]

Tuple of two CubicBezierCurves.

Source code in src/momapy/geometry.py
def split(self, t: float) -> tuple["CubicBezierCurve", "CubicBezierCurve"]:
    """Split the curve at parameter t using De Casteljau subdivision.

    Args:
        t: Parameter value from 0 to 1.

    Returns:
        Tuple of two CubicBezierCurves.
    """
    u = 1 - t
    # Level 1
    m0x = u * self.p1.x + t * self.control_point1.x
    m0y = u * self.p1.y + t * self.control_point1.y
    m1x = u * self.control_point1.x + t * self.control_point2.x
    m1y = u * self.control_point1.y + t * self.control_point2.y
    m2x = u * self.control_point2.x + t * self.p2.x
    m2y = u * self.control_point2.y + t * self.p2.y
    # Level 2
    n0x = u * m0x + t * m1x
    n0y = u * m0y + t * m1y
    n1x = u * m1x + t * m2x
    n1y = u * m1y + t * m2y
    # Level 3 (point on curve)
    px = u * n0x + t * n1x
    py = u * n0y + t * n1y
    mid = Point(px, py)
    left = CubicBezierCurve(self.p1, mid, Point(m0x, m0y), Point(n0x, n0y))
    right = CubicBezierCurve(mid, self.p2, Point(n1x, n1y), Point(m2x, m2y))
    return (left, right)

transformed

transformed(transformation) -> CubicBezierCurve

Apply a transformation to this curve.

Parameters:

Name Type Description Default
transformation

The transformation to apply.

required

Returns:

Type Description
CubicBezierCurve

A new transformed CubicBezierCurve.

Source code in src/momapy/geometry.py
def transformed(self, transformation) -> "CubicBezierCurve":
    """Apply a transformation to this curve.

    Args:
        transformation: The transformation to apply.

    Returns:
        A new transformed CubicBezierCurve.
    """
    return CubicBezierCurve(
        self.p1.transformed(transformation),
        self.p2.transformed(transformation),
        self.control_point1.transformed(transformation),
        self.control_point2.transformed(transformation),
    )

EllipticalArc dataclass

EllipticalArc(p1: Point, p2: Point, rx: float, ry: float, x_axis_rotation: float, arc_flag: int, sweep_flag: int)

Bases: GeometryObject

Represents an elliptical arc.

Attributes:

Name Type Description
p1 Point

Start point.

p2 Point

End point.

rx float

X-radius of the ellipse.

ry float

Y-radius of the ellipse.

x_axis_rotation float

Rotation of the x-axis in radians.

arc_flag int

Large arc flag (0 or 1).

sweep_flag int

Sweep flag (0 or 1).

Examples:

arc = EllipticalArc(
    Point(0, 0), Point(10, 0), 5, 5, 0, 0, 1
)

Methods:

Name Description
bbox

Get the bounding box of the arc.

derivative

Compute the derivative of the arc at parameter t.

evaluate

Evaluate the arc at parameter t using center parameterization.

get_angle_at_fraction

Get angle at a fraction along the arc.

get_center

Get the center point of the ellipse.

get_center_parameterization

Get the center parameterization of the arc.

get_intersection_with_line

Get intersection with a line.

get_position_and_angle_at_fraction

Get both position and angle at a fraction.

get_position_at_fraction

Get point at a fraction along the arc.

length

Calculate the length of the arc.

reversed

Return a reversed copy of the arc.

shortened

Return a shortened copy of the arc.

transformed

Apply a transformation to this arc.

bbox

bbox() -> Bbox

Get the bounding box of the arc.

Returns:

Type Description
Bbox

A Bbox enclosing the arc.

Source code in src/momapy/geometry.py
def bbox(self) -> "Bbox":
    """Get the bounding box of the arc.

    Returns:
        A Bbox enclosing the arc.
    """
    cx, cy, rx, ry, sigma, theta1, theta2, delta_theta = (
        self.get_center_parameterization()
    )
    cos_sigma = math.cos(sigma)
    sin_sigma = math.sin(sigma)
    # Extrema of x(theta): dx/dtheta = 0
    # -rx*sin(theta)*cos(sigma) - ry*cos(theta)*sin(sigma) = 0
    # tan(theta) = -(ry*sin(sigma)) / (rx*cos(sigma))
    theta_x = math.atan2(-ry * sin_sigma, rx * cos_sigma)
    # Extrema of y(theta): dy/dtheta = 0
    # -rx*sin(theta)*sin(sigma) + ry*cos(theta)*cos(sigma) = 0
    # tan(theta) = (ry*cos(sigma)) / (rx*sin(sigma))
    theta_y = math.atan2(ry * cos_sigma, rx * sin_sigma)
    # Collect candidate points: endpoints + extrema within arc range
    points = [self.p1, self.p2]
    if delta_theta > 0:
        theta_min = theta1
        theta_max = theta1 + delta_theta
    else:
        theta_min = theta1 + delta_theta
        theta_max = theta1
    for theta_base in (theta_x, theta_y):
        # Check both solutions (theta_base and theta_base + pi)
        for candidate in (theta_base, theta_base + math.pi):
            # Normalize candidate into [theta_min - 2pi, theta_max + 2pi]
            # and check if it falls within range
            normalized = candidate
            while normalized < theta_min - 2 * math.pi:
                normalized += 2 * math.pi
            while normalized > theta_max + 2 * math.pi:
                normalized -= 2 * math.pi
            # Try multiple periods
            for offset in (
                -2 * math.pi,
                0,
                2 * math.pi,
            ):
                theta_c = normalized + offset
                if (
                    theta_min - ZERO_TOLERANCE
                    <= theta_c
                    <= theta_max + ZERO_TOLERANCE
                ):
                    t = (theta_c - theta1) / delta_theta
                    if -ZERO_TOLERANCE <= t <= 1 + ZERO_TOLERANCE:
                        points.append(self.evaluate(max(0, min(1, t))))
    return Bbox.around_points(points)

derivative

derivative(t: float) -> tuple[float, float]

Compute the derivative of the arc at parameter t.

Parameters:

Name Type Description Default
t float

Parameter value from 0 to 1.

required

Returns:

Type Description
tuple[float, float]

Tuple of (dx, dy) derivatives.

Source code in src/momapy/geometry.py
def derivative(self, t: float) -> tuple[float, float]:
    """Compute the derivative of the arc at parameter t.

    Args:
        t: Parameter value from 0 to 1.

    Returns:
        Tuple of (dx, dy) derivatives.
    """
    cx, cy, rx, ry, sigma, theta1, theta2, delta_theta = (
        self.get_center_parameterization()
    )
    theta = theta1 + t * delta_theta
    cos_sigma = math.cos(sigma)
    sin_sigma = math.sin(sigma)
    cos_theta = math.cos(theta)
    sin_theta = math.sin(theta)
    dx = delta_theta * (-rx * sin_theta * cos_sigma - ry * cos_theta * sin_sigma)
    dy = delta_theta * (-rx * sin_theta * sin_sigma + ry * cos_theta * cos_sigma)
    return (dx, dy)

evaluate

evaluate(t: float) -> Point

Evaluate the arc at parameter t using center parameterization.

Parameters:

Name Type Description Default
t float

Parameter value from 0 to 1.

required

Returns:

Type Description
Point

The point at parameter t.

Source code in src/momapy/geometry.py
def evaluate(self, t: float) -> Point:
    """Evaluate the arc at parameter t using center parameterization.

    Args:
        t: Parameter value from 0 to 1.

    Returns:
        The point at parameter t.
    """
    cx, cy, rx, ry, sigma, theta1, theta2, delta_theta = (
        self.get_center_parameterization()
    )
    theta = theta1 + t * delta_theta
    cos_sigma = math.cos(sigma)
    sin_sigma = math.sin(sigma)
    cos_theta = math.cos(theta)
    sin_theta = math.sin(theta)
    x = cx + rx * cos_theta * cos_sigma - ry * sin_theta * sin_sigma
    y = cy + rx * cos_theta * sin_sigma + ry * sin_theta * cos_sigma
    return Point(x, y)

get_angle_at_fraction

get_angle_at_fraction(fraction: float) -> float

Get angle at a fraction along the arc.

Parameters:

Name Type Description Default
fraction float

Fraction from 0 to 1.

required

Returns:

Type Description
float

Angle in radians.

Source code in src/momapy/geometry.py
def get_angle_at_fraction(self, fraction: float) -> float:
    """Get angle at a fraction along the arc.

    Args:
        fraction: Fraction from 0 to 1.

    Returns:
        Angle in radians.
    """
    total = self.length()
    if total == 0:
        return 0.0
    if fraction <= 0:
        t = 0.0
    elif fraction >= 1:
        t = 1.0
    else:
        t = _find_t_at_arc_length_fraction(self.derivative, total, fraction)
    dx, dy = self.derivative(t)
    return math.atan2(dy, dx)

get_center

get_center() -> Point

Get the center point of the ellipse.

Returns:

Type Description
Point

The center point.

Source code in src/momapy/geometry.py
def get_center(self) -> Point:
    """Get the center point of the ellipse.

    Returns:
        The center point.
    """
    cx, cy, *_ = self.get_center_parameterization()
    return Point(cx, cy)

get_center_parameterization

get_center_parameterization() -> tuple[float, float, float, float, float, float, float, float]

Get the center parameterization of the arc.

Returns:

Type Description
tuple[float, float, float, float, float, float, float, float]

Tuple of (cx, cy, rx, ry, sigma, theta1, theta2, delta_theta).

Source code in src/momapy/geometry.py
def get_center_parameterization(
    self,
) -> tuple[float, float, float, float, float, float, float, float]:
    """Get the center parameterization of the arc.

    Returns:
        Tuple of (cx, cy, rx, ry, sigma, theta1, theta2, delta_theta).
    """
    x1, y1 = self.p1.x, self.p1.y
    sigma = self.x_axis_rotation
    x2, y2 = self.p2.x, self.p2.y
    rx = self.rx
    ry = self.ry
    fa = self.arc_flag
    fs = self.sweep_flag
    x1p = math.cos(sigma) * ((x1 - x2) / 2) + math.sin(sigma) * ((y1 - y2) / 2)
    y1p = -math.sin(sigma) * ((x1 - x2) / 2) + math.cos(sigma) * ((y1 - y2) / 2)
    l = x1p**2 / rx**2 + y1p**2 / ry**2
    if l > 1:
        rx = math.sqrt(l) * rx
        ry = math.sqrt(l) * ry
    r = rx**2 * ry**2 - rx**2 * y1p**2 - ry**2 * x1p**2
    if r < 0:
        r = 0
    a = math.sqrt(r / (rx**2 * y1p**2 + ry**2 * x1p**2))
    if fa == fs:
        a = -a
    cxp = a * rx * y1p / ry
    cyp = -a * ry * x1p / rx
    cx = math.cos(sigma) * cxp - math.sin(sigma) * cyp + (x1 + x2) / 2
    cy = math.sin(sigma) * cxp + math.cos(sigma) * cyp + (y1 + y2) / 2
    theta1 = _get_angle_between_segments(
        Segment(Point(0, 0), Point(1, 0)),
        Segment(Point(0, 0), Point((x1p - cxp) / rx, (y1p - cyp) / ry)),
    )
    delta_theta = _get_angle_between_segments(
        Segment(Point(0, 0), Point((x1p - cxp) / rx, (y1p - cyp) / ry)),
        Segment(Point(0, 0), Point(-(x1p + cxp) / rx, -(y1p + cyp) / ry)),
    )
    if fs == 0 and delta_theta > 0:
        delta_theta -= 2 * math.pi
    elif fs == 1 and delta_theta < 0:
        delta_theta += 2 * math.pi
    theta2 = theta1 + delta_theta
    return cx, cy, rx, ry, sigma, theta1, theta2, delta_theta

get_intersection_with_line

get_intersection_with_line(line: Line) -> list[Point]

Get intersection with a line.

Uses analytical ellipse-line intersection by solving Acos(theta) + Bsin(theta) + C = 0 via atan2.

Parameters:

Name Type Description Default
line Line

The line to intersect with.

required

Returns:

Type Description
list[Point]

List of intersection points.

Source code in src/momapy/geometry.py
def get_intersection_with_line(self, line: Line) -> list[Point]:
    """Get intersection with a line.

    Uses analytical ellipse-line intersection by solving
    A*cos(theta) + B*sin(theta) + C = 0 via atan2.

    Args:
        line: The line to intersect with.

    Returns:
        List of intersection points.
    """
    cx, cy, rx, ry, sigma, theta1, theta2, delta_theta = (
        self.get_center_parameterization()
    )
    # Line in implicit form: a*x + b*y + c = 0
    dx = line.p2.x - line.p1.x
    dy = line.p2.y - line.p1.y
    a = -dy
    b = dx
    c = -(a * line.p1.x + b * line.p1.y)
    cos_sigma = math.cos(sigma)
    sin_sigma = math.sin(sigma)
    # Substitute parametric arc equations into line equation
    A = a * rx * cos_sigma + b * rx * sin_sigma
    B = a * (-ry * sin_sigma) + b * ry * cos_sigma
    C = a * cx + b * cy + c
    R = math.sqrt(A * A + B * B)
    if R < ZERO_TOLERANCE:
        return []
    ratio = -C / R
    if abs(ratio) > 1 + ZERO_TOLERANCE:
        return []
    ratio = max(-1.0, min(1.0, ratio))
    phi = math.atan2(B, A)
    acos_val = math.acos(ratio)
    candidates = [phi + acos_val, phi - acos_val]
    if delta_theta > 0:
        theta_min = theta1
        theta_max = theta1 + delta_theta
    else:
        theta_min = theta1 + delta_theta
        theta_max = theta1
    result = []
    for theta_candidate in candidates:
        # Normalize into range by trying multiple periods
        for k in range(-3, 4):
            theta_c = theta_candidate + k * 2 * math.pi
            if theta_min - ZERO_TOLERANCE <= theta_c <= theta_max + ZERO_TOLERANCE:
                t = (theta_c - theta1) / delta_theta
                if -ZERO_TOLERANCE <= t <= 1 + ZERO_TOLERANCE:
                    t = max(0.0, min(1.0, t))
                    point = self.evaluate(t)
                    # Avoid duplicates
                    is_dup = False
                    for existing in result:
                        if (
                            abs(existing.x - point.x) < ROUNDING_TOLERANCE
                            and abs(existing.y - point.y) < ROUNDING_TOLERANCE
                        ):
                            is_dup = True
                            break
                    if not is_dup:
                        result.append(point)
    return result

get_position_and_angle_at_fraction

get_position_and_angle_at_fraction(fraction: float) -> tuple[Point, float]

Get both position and angle at a fraction.

Parameters:

Name Type Description Default
fraction float

Fraction from 0 to 1.

required

Returns:

Type Description
tuple[Point, float]

Tuple of (point, angle).

Source code in src/momapy/geometry.py
def get_position_and_angle_at_fraction(
    self, fraction: float
) -> tuple[Point, float]:
    """Get both position and angle at a fraction.

    Args:
        fraction: Fraction from 0 to 1.

    Returns:
        Tuple of (point, angle).
    """
    total = self.length()
    if total == 0:
        return (Point(self.p1.x, self.p1.y), 0.0)
    if fraction <= 0:
        t = 0.0
    elif fraction >= 1:
        t = 1.0
    else:
        t = _find_t_at_arc_length_fraction(self.derivative, total, fraction)
    pos = self.evaluate(t)
    dx, dy = self.derivative(t)
    angle = math.atan2(dy, dx)
    return (pos, angle)

get_position_at_fraction

get_position_at_fraction(fraction: float) -> Point

Get point at a fraction along the arc.

Parameters:

Name Type Description Default
fraction float

Fraction from 0 to 1.

required

Returns:

Type Description
Point

The point at that fraction.

Source code in src/momapy/geometry.py
def get_position_at_fraction(self, fraction: float) -> Point:
    """Get point at a fraction along the arc.

    Args:
        fraction: Fraction from 0 to 1.

    Returns:
        The point at that fraction.
    """
    if fraction <= 0:
        return Point(self.p1.x, self.p1.y)
    if fraction >= 1:
        return Point(self.p2.x, self.p2.y)
    total = self.length()
    if total == 0:
        return Point(self.p1.x, self.p1.y)
    t = _find_t_at_arc_length_fraction(self.derivative, total, fraction)
    return self.evaluate(t)

length

length() -> float

Calculate the length of the arc.

Returns:

Type Description
float

The arc length.

Source code in src/momapy/geometry.py
def length(self) -> float:
    """Calculate the length of the arc.

    Returns:
        The arc length.
    """
    return _arc_length(self.derivative, 0.0, 1.0)

reversed

reversed() -> EllipticalArc

Return a reversed copy of the arc.

Returns:

Type Description
EllipticalArc

A new EllipticalArc going in reverse direction.

Source code in src/momapy/geometry.py
def reversed(self) -> "EllipticalArc":
    """Return a reversed copy of the arc.

    Returns:
        A new EllipticalArc going in reverse direction.
    """
    return EllipticalArc(
        self.p2,
        self.p1,
        self.rx,
        self.ry,
        self.x_axis_rotation,
        self.arc_flag,
        abs(self.sweep_flag - 1),
    )

shortened

shortened(length: float, start_or_end: Literal['start', 'end'] = 'end') -> EllipticalArc

Return a shortened copy of the arc.

Parameters:

Name Type Description Default
length float

Amount to shorten by.

required
start_or_end Literal['start', 'end']

Which end to shorten from.

'end'

Returns:

Type Description
EllipticalArc

A new shortened EllipticalArc.

Source code in src/momapy/geometry.py
def shortened(
    self,
    length: float,
    start_or_end: typing.Literal["start", "end"] = "end",
) -> "EllipticalArc":
    """Return a shortened copy of the arc.

    Args:
        length: Amount to shorten by.
        start_or_end: Which end to shorten from.

    Returns:
        A new shortened EllipticalArc.
    """
    if length == 0 or self.length() == 0:
        return copy.deepcopy(self)
    if start_or_end == "start":
        return self.reversed().shortened(length).reversed()
    fraction = 1 - length / self.length()
    point = self.get_position_at_fraction(fraction)
    return dataclasses.replace(self, p2=point)

transformed

transformed(transformation: Transformation) -> EllipticalArc

Apply a transformation to this arc.

Parameters:

Name Type Description Default
transformation Transformation

The transformation to apply.

required

Returns:

Type Description
EllipticalArc

A new transformed EllipticalArc.

Source code in src/momapy/geometry.py
def transformed(self, transformation: "Transformation") -> "EllipticalArc":
    """Apply a transformation to this arc.

    Args:
        transformation: The transformation to apply.

    Returns:
        A new transformed EllipticalArc.
    """
    east = Point(
        math.cos(self.x_axis_rotation) * self.rx,
        math.sin(self.x_axis_rotation) * self.rx,
    )
    north = Point(
        math.cos(self.x_axis_rotation) * self.ry,
        math.sin(self.x_axis_rotation) * self.ry,
    )
    new_center = Point(0, 0).transformed(transformation)
    new_east = east.transformed(transformation)
    new_north = north.transformed(transformation)
    new_rx = Segment(new_center, new_east).length()
    new_ry = Segment(new_center, new_north).length()
    new_start_point = self.p1.transformed(transformation)
    new_end_point = self.p2.transformed(transformation)
    new_x_axis_rotation = math.degrees(
        Line(new_center, new_east).get_angle_to_horizontal()
    )
    return EllipticalArc(
        p1=new_start_point,
        p2=new_end_point,
        rx=new_rx,
        ry=new_ry,
        x_axis_rotation=new_x_axis_rotation,
        arc_flag=self.arc_flag,
        sweep_flag=self.sweep_flag,
    )

GeometryObject dataclass

GeometryObject()

Bases: ABC

Abstract base class for all geometry objects.

Line dataclass

Line(p1: Point, p2: Point)

Bases: GeometryObject

Represents an infinite line defined by two points.

Attributes:

Name Type Description
p1 Point

First point on the line.

p2 Point

Second point on the line.

Examples:

line = Line(Point(0, 0), Point(10, 10))
line.slope()
line.intercept()

Methods:

Name Description
get_angle_to_horizontal

Get the angle of the line relative to horizontal.

get_distance_to_point

Get perpendicular distance from a point to this line.

get_intersection_with_line

Get intersection with another line.

has_point

Check if a point lies on this line.

intercept

Calculate the y-intercept of the line.

is_coincident_to_line

Check if this line is coincident with another.

is_parallel_to_line

Check if this line is parallel to another.

reversed

Return a reversed copy of the line.

slope

Calculate the slope of the line.

transformed

Apply a transformation to this line.

get_angle_to_horizontal

get_angle_to_horizontal() -> float

Get the angle of the line relative to horizontal.

Returns:

Type Description
float

Angle in radians.

Source code in src/momapy/geometry.py
def get_angle_to_horizontal(self) -> float:
    """Get the angle of the line relative to horizontal.

    Returns:
        Angle in radians.
    """
    angle = math.atan2(self.p2.y - self.p1.y, self.p2.x - self.p1.x)
    return get_normalized_angle(angle)

get_distance_to_point

get_distance_to_point(point: Point) -> float

Get perpendicular distance from a point to this line.

Parameters:

Name Type Description Default
point Point

The point to measure from.

required

Returns:

Type Description
float

The perpendicular distance.

Source code in src/momapy/geometry.py
def get_distance_to_point(self, point: Point) -> float:
    """Get perpendicular distance from a point to this line.

    Args:
        point: The point to measure from.

    Returns:
        The perpendicular distance.
    """
    return abs(
        (self.p2.x - self.p1.x) * (self.p1.y - point.y)
        - (self.p1.x - point.x) * (self.p2.y - self.p1.y)
    ) / math.sqrt((self.p2.x - self.p1.x) ** 2 + (self.p2.y - self.p1.y) ** 2)

get_intersection_with_line

get_intersection_with_line(line: Line) -> list[Line] | list[Point]

Get intersection with another line.

Parameters:

Name Type Description Default
line Line

The other line.

required

Returns:

Type Description
list[Line] | list[Point]

List containing intersection point(s) or the coincident line.

Source code in src/momapy/geometry.py
def get_intersection_with_line(self, line: "Line") -> list["Line"] | list[Point]:
    """Get intersection with another line.

    Args:
        line: The other line.

    Returns:
        List containing intersection point(s) or the coincident line.
    """
    slope1 = self.slope()
    intercept1 = self.intercept()
    slope2 = line.slope()
    intercept2 = line.intercept()
    if self.is_coincident_to_line(line):
        intersection = [copy.deepcopy(self)]
    elif self.is_parallel_to_line(line):
        intersection = []
    elif math.isnan(slope1):
        intersection = [Point(self.p1.x, slope2 * self.p1.x + intercept2)]
    elif math.isnan(slope2):
        intersection = [Point(line.p1.x, slope1 * line.p1.x + intercept1)]
    else:
        d = (self.p1.x - self.p2.x) * (line.p1.y - line.p2.y) - (
            self.p1.y - self.p2.y
        ) * (line.p1.x - line.p2.x)
        px = (
            (self.p1.x * self.p2.y - self.p1.y * self.p2.x)
            * (line.p1.x - line.p2.x)
            - (self.p1.x - self.p2.x)
            * (line.p1.x * line.p2.y - line.p1.y * line.p2.x)
        ) / d
        py = (
            (self.p1.x * self.p2.y - self.p1.y * self.p2.x)
            * (line.p1.y - line.p2.y)
            - (self.p1.y - self.p2.y)
            * (line.p1.x * line.p2.y - line.p1.y * line.p2.x)
        ) / d
        intersection = [Point(px, py)]
    return intersection

has_point

has_point(point: Point, max_distance: float = ROUNDING_TOLERANCE) -> bool

Check if a point lies on this line.

Parameters:

Name Type Description Default
point Point

The point to check.

required
max_distance float

Maximum allowed distance from line.

ROUNDING_TOLERANCE

Returns:

Type Description
bool

True if point is on the line within tolerance.

Source code in src/momapy/geometry.py
def has_point(self, point: Point, max_distance: float = ROUNDING_TOLERANCE) -> bool:
    """Check if a point lies on this line.

    Args:
        point: The point to check.
        max_distance: Maximum allowed distance from line.

    Returns:
        True if point is on the line within tolerance.
    """
    return self.get_distance_to_point(point) <= max_distance

intercept

intercept() -> float

Calculate the y-intercept of the line.

Returns:

Type Description
float

The y-intercept, or NaN if vertical.

Source code in src/momapy/geometry.py
def intercept(self) -> float:
    """Calculate the y-intercept of the line.

    Returns:
        The y-intercept, or NaN if vertical.
    """
    slope = self.slope()
    if not math.isnan(slope):
        return round(self.p1.y - slope * self.p1.x, ROUNDING)
    return float("NAN")

is_coincident_to_line

is_coincident_to_line(line: Line) -> bool

Check if this line is coincident with another.

Parameters:

Name Type Description Default
line Line

The other line.

required

Returns:

Type Description
bool

True if coincident (same infinite line).

Source code in src/momapy/geometry.py
def is_coincident_to_line(self, line: "Line") -> bool:
    """Check if this line is coincident with another.

    Args:
        line: The other line.

    Returns:
        True if coincident (same infinite line).
    """
    slope1 = self.slope()
    intercept1 = self.intercept()
    slope2 = line.slope()
    intercept2 = line.intercept()
    return (
        math.isnan(slope1)
        and math.isnan(slope2)
        and self.p1.x == line.p1.x
        or slope1 == slope2
        and intercept1 == intercept2
    )

is_parallel_to_line

is_parallel_to_line(line: Line) -> bool

Check if this line is parallel to another.

Parameters:

Name Type Description Default
line Line

The other line.

required

Returns:

Type Description
bool

True if parallel.

Source code in src/momapy/geometry.py
def is_parallel_to_line(self, line: "Line") -> bool:
    """Check if this line is parallel to another.

    Args:
        line: The other line.

    Returns:
        True if parallel.
    """
    slope1 = self.slope()
    slope2 = line.slope()
    if math.isnan(slope1) and math.isnan(slope2):
        return True
    return slope1 == slope2

reversed

reversed() -> Line

Return a reversed copy of the line.

Returns:

Type Description
Line

A new Line with p1 and p2 swapped.

Source code in src/momapy/geometry.py
def reversed(self) -> "Line":
    """Return a reversed copy of the line.

    Returns:
        A new Line with p1 and p2 swapped.
    """
    return Line(self.p2, self.p1)

slope

slope() -> float

Calculate the slope of the line.

Returns:

Type Description
float

The slope, or NaN if vertical.

Source code in src/momapy/geometry.py
def slope(self) -> float:
    """Calculate the slope of the line.

    Returns:
        The slope, or NaN if vertical.
    """
    if self.p1.x != self.p2.x:
        return round((self.p2.y - self.p1.y) / (self.p2.x - self.p1.x), ROUNDING)
    return float("NAN")

transformed

transformed(transformation: Transformation) -> Line

Apply a transformation to this line.

Parameters:

Name Type Description Default
transformation Transformation

The transformation to apply.

required

Returns:

Type Description
Line

A new transformed Line.

Source code in src/momapy/geometry.py
def transformed(self, transformation: "Transformation") -> "Line":
    """Apply a transformation to this line.

    Args:
        transformation: The transformation to apply.

    Returns:
        A new transformed Line.
    """
    return Line(
        self.p1.transformed(transformation),
        self.p2.transformed(transformation),
    )

MatrixTransformation dataclass

MatrixTransformation(m: NDArray)

Bases: Transformation

Represents a transformation as a 3x3 matrix.

Attributes:

Name Type Description
m NDArray

The 3x3 transformation matrix.

Methods:

Name Description
inverted

Get the inverse transformation.

to_matrix

Get the matrix representation.

inverted

inverted() -> Transformation

Get the inverse transformation.

Returns:

Type Description
Transformation

The inverse matrix transformation.

Source code in src/momapy/geometry.py
def inverted(self) -> Transformation:
    """Get the inverse transformation.

    Returns:
        The inverse matrix transformation.
    """
    return MatrixTransformation(numpy.linalg.inv(self.m))

to_matrix

to_matrix() -> NDArray

Get the matrix representation.

Returns:

Type Description
NDArray

The 3x3 matrix.

Source code in src/momapy/geometry.py
def to_matrix(self) -> numpy.typing.NDArray:
    """Get the matrix representation.

    Returns:
        The 3x3 matrix.
    """
    return self.m

Point dataclass

Point(x: float, y: float)

Bases: GeometryObject

Represents a 2D point with x and y coordinates.

Attributes:

Name Type Description
x float

The x-coordinate.

y float

The y-coordinate.

Examples:

p = Point(10, 20)
p.x
p + (5, 5)

Methods:

Name Description
bbox

Get the bounding box of this point.

from_tuple

Create a Point from a tuple.

get_angle_to_horizontal

Get the angle from origin to this point relative to horizontal.

get_intersection_with_line

Get intersection points with a line.

isnan

Check if either coordinate is NaN.

reversed

Return a reversed copy (identity for points).

round

Round coordinates to specified digits.

to_matrix

Convert to a 3x1 numpy matrix for transformation operations.

to_tuple

Convert to a tuple.

transformed

Apply a transformation to this point.

bbox

bbox() -> Bbox

Get the bounding box of this point.

Returns:

Type Description
Bbox

A Bbox with zero width and height.

Source code in src/momapy/geometry.py
def bbox(self) -> "Bbox":
    """Get the bounding box of this point.

    Returns:
        A Bbox with zero width and height.
    """
    return Bbox(copy.deepcopy(self), 0, 0)

from_tuple classmethod

from_tuple(t: tuple[float, float]) -> Self

Create a Point from a tuple.

Parameters:

Name Type Description Default
t tuple[float, float]

Tuple (x, y).

required

Returns:

Type Description
Self

A new Point.

Source code in src/momapy/geometry.py
@classmethod
def from_tuple(cls, t: tuple[float, float]) -> typing_extensions.Self:
    """Create a Point from a tuple.

    Args:
        t: Tuple (x, y).

    Returns:
        A new Point.
    """
    return cls(t[0], t[1])

get_angle_to_horizontal

get_angle_to_horizontal() -> float

Get the angle from origin to this point relative to horizontal.

Returns:

Type Description
float

Angle in radians.

Source code in src/momapy/geometry.py
def get_angle_to_horizontal(self) -> float:
    """Get the angle from origin to this point relative to horizontal.

    Returns:
        Angle in radians.
    """
    angle = math.atan2(self.y, self.x)
    return get_normalized_angle(angle)

get_intersection_with_line

get_intersection_with_line(line: Line) -> list[Point]

Get intersection points with a line.

Parameters:

Name Type Description Default
line Line

The line to intersect with.

required

Returns:

Type Description
list[Point]

List of intersection points (empty if no intersection).

Source code in src/momapy/geometry.py
def get_intersection_with_line(self, line: "Line") -> list["Point"]:
    """Get intersection points with a line.

    Args:
        line: The line to intersect with.

    Returns:
        List of intersection points (empty if no intersection).
    """
    if line.has_point(self):
        return [self]
    return []

isnan

isnan() -> bool

Check if either coordinate is NaN.

Returns:

Type Description
bool

True if x or y is NaN.

Source code in src/momapy/geometry.py
def isnan(self) -> bool:
    """Check if either coordinate is NaN.

    Returns:
        True if x or y is NaN.
    """
    return math.isnan(self.x) or math.isnan(self.y)

reversed

reversed() -> Point

Return a reversed copy (identity for points).

Returns:

Type Description
Point

A copy of the point.

Source code in src/momapy/geometry.py
def reversed(self) -> "Point":
    """Return a reversed copy (identity for points).

    Returns:
        A copy of the point.
    """
    return Point(self.x, self.y)

round

round(ndigits=None)

Round coordinates to specified digits.

Parameters:

Name Type Description Default
ndigits

Number of decimal places.

None

Returns:

Type Description

A new Point with rounded coordinates.

Source code in src/momapy/geometry.py
def round(self, ndigits=None):
    """Round coordinates to specified digits.

    Args:
        ndigits: Number of decimal places.

    Returns:
        A new Point with rounded coordinates.
    """
    return Point(round(self.x, ndigits), round(self.y, ndigits))

to_matrix

to_matrix() -> ndarray

Convert to a 3x1 numpy matrix for transformation operations.

Returns:

Type Description
ndarray

A numpy array [[x], [y], [1]].

Source code in src/momapy/geometry.py
def to_matrix(self) -> numpy.ndarray:
    """Convert to a 3x1 numpy matrix for transformation operations.

    Returns:
        A numpy array [[x], [y], [1]].
    """
    m = numpy.array([[self.x], [self.y], [1]], dtype=float)
    return m

to_tuple

to_tuple() -> tuple[float, float]

Convert to a tuple.

Returns:

Type Description
tuple[float, float]

Tuple (x, y).

Source code in src/momapy/geometry.py
def to_tuple(self) -> tuple[float, float]:
    """Convert to a tuple.

    Returns:
        Tuple (x, y).
    """
    return (
        self.x,
        self.y,
    )

transformed

transformed(transformation: Transformation) -> Point

Apply a transformation to this point.

Parameters:

Name Type Description Default
transformation Transformation

The transformation to apply.

required

Returns:

Type Description
Point

A new transformed Point.

Source code in src/momapy/geometry.py
def transformed(self, transformation: "Transformation") -> "Point":
    """Apply a transformation to this point.

    Args:
        transformation: The transformation to apply.

    Returns:
        A new transformed Point.
    """
    m = numpy.matmul(transformation.to_matrix(), self.to_matrix())
    return Point(m[0][0], m[1][0])

QuadraticBezierCurve dataclass

QuadraticBezierCurve(p1: Point, p2: Point, control_point: Point)

Bases: GeometryObject

Represents a quadratic Bezier curve.

Attributes:

Name Type Description
p1 Point

Start point.

p2 Point

End point.

control_point Point

The single control point.

Examples:

curve = QuadraticBezierCurve(
    Point(0, 0),
    Point(10, 0),
    Point(5, 5)
)

Methods:

Name Description
bbox

Get the bounding box of the curve.

derivative

Compute the derivative at parameter t.

evaluate

Evaluate the curve at parameter t.

evaluate_multi

Evaluate the curve at multiple parameters.

get_angle_at_fraction

Get angle at a fraction of the arc length.

get_intersection_with_line

Get intersection with a line.

get_position_and_angle_at_fraction

Get both position and angle at a fraction of the arc length.

get_position_at_fraction

Get point at a fraction of the arc length.

length

Calculate the arc length of the curve.

reversed

Return a reversed copy of the curve.

shortened

Return a shortened copy of the curve.

split

Split the curve at parameter t using De Casteljau subdivision.

transformed

Apply a transformation to this curve.

bbox

bbox() -> Bbox

Get the bounding box of the curve.

Returns:

Type Description
Bbox

A Bbox enclosing the curve.

Source code in src/momapy/geometry.py
def bbox(self) -> "Bbox":
    """Get the bounding box of the curve.

    Returns:
        A Bbox enclosing the curve.
    """
    # Extrema candidates: endpoints + roots of derivative
    xs = [self.p1.x, self.p2.x]
    ys = [self.p1.y, self.p2.y]
    # dx/dt = 2(1-t)(cp-p1) + 2t(p2-cp) = 0 => t = (p1-cp)/(p1-2cp+p2)
    for values, lst in [
        ((self.p1.x, self.control_point.x, self.p2.x), xs),
        ((self.p1.y, self.control_point.y, self.p2.y), ys),
    ]:
        a, b, c = values
        denominator = a - 2 * b + c
        if abs(denominator) > ZERO_TOLERANCE:
            t = (a - b) / denominator
            if 0 < t < 1:
                point = self.evaluate(t)
                lst.append(point.x if lst is xs else point.y)
    min_x, max_x = min(xs), max(xs)
    min_y, max_y = min(ys), max(ys)
    return Bbox(
        Point(min_x, min_y),
        max_x - min_x,
        max_y - min_y,
    )

derivative

derivative(t: float) -> tuple[float, float]

Compute the derivative at parameter t.

Parameters:

Name Type Description Default
t float

Parameter value from 0 to 1.

required

Returns:

Type Description
tuple[float, float]

Tuple of (dx/dt, dy/dt).

Source code in src/momapy/geometry.py
def derivative(self, t: float) -> tuple[float, float]:
    """Compute the derivative at parameter t.

    Args:
        t: Parameter value from 0 to 1.

    Returns:
        Tuple of (dx/dt, dy/dt).
    """
    u = 1 - t
    dx = 2 * u * (self.control_point.x - self.p1.x) + 2 * t * (
        self.p2.x - self.control_point.x
    )
    dy = 2 * u * (self.control_point.y - self.p1.y) + 2 * t * (
        self.p2.y - self.control_point.y
    )
    return (dx, dy)

evaluate

evaluate(t: float) -> Point

Evaluate the curve at parameter t.

Parameters:

Name Type Description Default
t float

Parameter value from 0 to 1.

required

Returns:

Type Description
Point

The point at parameter t.

Source code in src/momapy/geometry.py
def evaluate(self, t: float) -> Point:
    """Evaluate the curve at parameter t.

    Args:
        t: Parameter value from 0 to 1.

    Returns:
        The point at parameter t.
    """
    return self.evaluate_multi([t])[0]

evaluate_multi

evaluate_multi(t_sequence: Sequence[float]) -> list[Point]

Evaluate the curve at multiple parameters.

Parameters:

Name Type Description Default
t_sequence Sequence[float]

Sequence of parameter values.

required

Returns:

Type Description
list[Point]

List of points.

Source code in src/momapy/geometry.py
def evaluate_multi(
    self, t_sequence: collections.abc.Sequence[float]
) -> list[Point]:
    """Evaluate the curve at multiple parameters.

    Args:
        t_sequence: Sequence of parameter values.

    Returns:
        List of points.
    """
    t = numpy.asarray(t_sequence, dtype="double")
    u = 1 - t
    x = u * u * self.p1.x + 2 * u * t * self.control_point.x + t * t * self.p2.x
    y = u * u * self.p1.y + 2 * u * t * self.control_point.y + t * t * self.p2.y
    return [Point(float(xi), float(yi)) for xi, yi in zip(x, y)]

get_angle_at_fraction

get_angle_at_fraction(fraction: float) -> float

Get angle at a fraction of the arc length.

Parameters:

Name Type Description Default
fraction float

Fraction from 0 to 1.

required

Returns:

Type Description
float

Angle in radians.

Source code in src/momapy/geometry.py
def get_angle_at_fraction(self, fraction: float) -> float:
    """Get angle at a fraction of the arc length.

    Args:
        fraction: Fraction from 0 to 1.

    Returns:
        Angle in radians.
    """
    total = self.length()
    if fraction <= 0:
        t = 0.0
    elif fraction >= 1:
        t = 1.0
    else:
        t = _find_t_at_arc_length_fraction(self.derivative, total, fraction)
    dx, dy = self.derivative(t)
    return math.atan2(dy, dx)

get_intersection_with_line

get_intersection_with_line(line: Line) -> list[Point] | list[Segment]

Get intersection with a line.

Parameters:

Name Type Description Default
line Line

The line to intersect with.

required

Returns:

Type Description
list[Point] | list[Segment]

List of intersection points or segments.

Source code in src/momapy/geometry.py
def get_intersection_with_line(self, line: Line) -> list[Point] | list[Segment]:
    """Get intersection with a line.

    Args:
        line: The line to intersect with.

    Returns:
        List of intersection points or segments.
    """
    # Line equation: a*x + b*y + c = 0
    a = line.p2.y - line.p1.y
    b = line.p1.x - line.p2.x
    c = line.p2.x * line.p1.y - line.p1.x * line.p2.y
    # Bezier: B(t) = (1-t)^2 * p0 + 2(1-t)t * p1 + t^2 * p2
    p0x, p0y = self.p1.x, self.p1.y
    p1x, p1y = self.control_point.x, self.control_point.y
    p2x, p2y = self.p2.x, self.p2.y
    # Substitute into line equation: A*t^2 + B*t + C = 0
    ax_coeff = a * (p0x - 2 * p1x + p2x) + b * (p0y - 2 * p1y + p2y)
    bx_coeff = a * (-2 * p0x + 2 * p1x) + b * (-2 * p0y + 2 * p1y)
    cx_coeff = a * p0x + b * p0y + c
    roots = []
    if abs(ax_coeff) < ZERO_TOLERANCE:
        if abs(bx_coeff) > ZERO_TOLERANCE:
            roots.append(-cx_coeff / bx_coeff)
    else:
        disc = bx_coeff * bx_coeff - 4 * ax_coeff * cx_coeff
        if disc >= 0:
            sqrt_disc = math.sqrt(disc)
            roots.append((-bx_coeff + sqrt_disc) / (2 * ax_coeff))
            roots.append((-bx_coeff - sqrt_disc) / (2 * ax_coeff))
    result = []
    for t in roots:
        if -PARAMETER_TOLERANCE <= t <= 1 + PARAMETER_TOLERANCE:
            t = max(0.0, min(1.0, t))
            result.append(self.evaluate(t))
    return result

get_position_and_angle_at_fraction

get_position_and_angle_at_fraction(fraction: float) -> tuple[Point, float]

Get both position and angle at a fraction of the arc length.

Parameters:

Name Type Description Default
fraction float

Fraction from 0 to 1.

required

Returns:

Type Description
tuple[Point, float]

Tuple of (point, angle_in_radians).

Source code in src/momapy/geometry.py
def get_position_and_angle_at_fraction(
    self, fraction: float
) -> tuple[Point, float]:
    """Get both position and angle at a fraction of the arc length.

    Args:
        fraction: Fraction from 0 to 1.

    Returns:
        Tuple of (point, angle_in_radians).
    """
    total = self.length()
    if fraction <= 0:
        t = 0.0
    elif fraction >= 1:
        t = 1.0
    else:
        t = _find_t_at_arc_length_fraction(self.derivative, total, fraction)
    pos = self.evaluate(t)
    dx, dy = self.derivative(t)
    angle = math.atan2(dy, dx)
    return (pos, angle)

get_position_at_fraction

get_position_at_fraction(fraction: float) -> Point

Get point at a fraction of the arc length.

Parameters:

Name Type Description Default
fraction float

Fraction from 0 to 1.

required

Returns:

Type Description
Point

The point at that fraction.

Source code in src/momapy/geometry.py
def get_position_at_fraction(self, fraction: float) -> Point:
    """Get point at a fraction of the arc length.

    Args:
        fraction: Fraction from 0 to 1.

    Returns:
        The point at that fraction.
    """
    if fraction <= 0:
        return Point(self.p1.x, self.p1.y)
    if fraction >= 1:
        return Point(self.p2.x, self.p2.y)
    total = self.length()
    t = _find_t_at_arc_length_fraction(self.derivative, total, fraction)
    return self.evaluate(t)

length

length() -> float

Calculate the arc length of the curve.

Returns:

Type Description
float

The arc length.

Source code in src/momapy/geometry.py
def length(self) -> float:
    """Calculate the arc length of the curve.

    Returns:
        The arc length.
    """
    return _arc_length(self.derivative, 0.0, 1.0)

reversed

reversed() -> QuadraticBezierCurve

Return a reversed copy of the curve.

Returns:

Type Description
QuadraticBezierCurve

A new QuadraticBezierCurve going in reverse direction.

Source code in src/momapy/geometry.py
def reversed(self) -> "QuadraticBezierCurve":
    """Return a reversed copy of the curve.

    Returns:
        A new QuadraticBezierCurve going in reverse direction.
    """
    return QuadraticBezierCurve(self.p2, self.p1, self.control_point)

shortened

shortened(length: float, start_or_end: Literal['start', 'end'] = 'end') -> QuadraticBezierCurve

Return a shortened copy of the curve.

Parameters:

Name Type Description Default
length float

Amount to shorten by.

required
start_or_end Literal['start', 'end']

Which end to shorten from.

'end'

Returns:

Type Description
QuadraticBezierCurve

A new shortened QuadraticBezierCurve.

Source code in src/momapy/geometry.py
def shortened(
    self, length: float, start_or_end: typing.Literal["start", "end"] = "end"
) -> "QuadraticBezierCurve":
    """Return a shortened copy of the curve.

    Args:
        length: Amount to shorten by.
        start_or_end: Which end to shorten from.

    Returns:
        A new shortened QuadraticBezierCurve.
    """
    if length == 0 or self.length() == 0:
        return copy.deepcopy(self)
    if start_or_end == "start":
        return self.reversed().shortened(length).reversed()
    total_length = self.length()
    if length > total_length:
        length = total_length
    fraction = 1 - length / total_length
    t = _find_t_at_arc_length_fraction(self.derivative, total_length, fraction)
    left, _ = self.split(t)
    return left

split

split(t: float) -> tuple[QuadraticBezierCurve, QuadraticBezierCurve]

Split the curve at parameter t using De Casteljau subdivision.

Parameters:

Name Type Description Default
t float

Parameter value from 0 to 1.

required

Returns:

Type Description
tuple[QuadraticBezierCurve, QuadraticBezierCurve]

Tuple of two QuadraticBezierCurves.

Source code in src/momapy/geometry.py
def split(self, t: float) -> tuple["QuadraticBezierCurve", "QuadraticBezierCurve"]:
    """Split the curve at parameter t using De Casteljau subdivision.

    Args:
        t: Parameter value from 0 to 1.

    Returns:
        Tuple of two QuadraticBezierCurves.
    """
    u = 1 - t
    m0x = u * self.p1.x + t * self.control_point.x
    m0y = u * self.p1.y + t * self.control_point.y
    m1x = u * self.control_point.x + t * self.p2.x
    m1y = u * self.control_point.y + t * self.p2.y
    mx = u * m0x + t * m1x
    my = u * m0y + t * m1y
    mid = Point(mx, my)
    left = QuadraticBezierCurve(self.p1, mid, Point(m0x, m0y))
    right = QuadraticBezierCurve(mid, self.p2, Point(m1x, m1y))
    return (left, right)

transformed

transformed(transformation) -> QuadraticBezierCurve

Apply a transformation to this curve.

Parameters:

Name Type Description Default
transformation

The transformation to apply.

required

Returns:

Type Description
QuadraticBezierCurve

A new transformed QuadraticBezierCurve.

Source code in src/momapy/geometry.py
def transformed(self, transformation) -> "QuadraticBezierCurve":
    """Apply a transformation to this curve.

    Args:
        transformation: The transformation to apply.

    Returns:
        A new transformed QuadraticBezierCurve.
    """
    return QuadraticBezierCurve(
        self.p1.transformed(transformation),
        self.p2.transformed(transformation),
        self.control_point.transformed(transformation),
    )

Rotation dataclass

Rotation(angle: float, point: Point | None = None)

Bases: Transformation

Represents a rotation transformation.

Attributes:

Name Type Description
angle float

Rotation angle in radians.

point Point | None

Optional center of rotation (defaults to origin).

Examples:

rot = Rotation(math.pi / 2, Point(5, 5))

Methods:

Name Description
inverted

Get the inverse rotation.

to_matrix

Convert to a rotation matrix.

inverted

inverted() -> Transformation

Get the inverse rotation.

Returns:

Type Description
Transformation

A rotation by the negative angle.

Source code in src/momapy/geometry.py
def inverted(self) -> Transformation:
    """Get the inverse rotation.

    Returns:
        A rotation by the negative angle.
    """
    return Rotation(-self.angle, self.point)

to_matrix

to_matrix() -> NDArray

Convert to a rotation matrix.

Returns:

Type Description
NDArray

A 3x3 rotation matrix.

Source code in src/momapy/geometry.py
def to_matrix(self) -> numpy.typing.NDArray:
    """Convert to a rotation matrix.

    Returns:
        A 3x3 rotation matrix.
    """
    m = numpy.array(
        [
            [math.cos(self.angle), -math.sin(self.angle), 0],
            [math.sin(self.angle), math.cos(self.angle), 0],
            [0, 0, 1],
        ],
        dtype=float,
    )
    if self.point is not None:
        translation = Translation(self.point.x, self.point.y)
        m = numpy.matmul(
            numpy.matmul(translation.to_matrix(), m),
            translation.inverted().to_matrix(),
        )
    return m

Scaling dataclass

Scaling(sx: float, sy: float)

Bases: Transformation

Represents a scaling transformation.

Attributes:

Name Type Description
sx float

Scale factor in x direction.

sy float

Scale factor in y direction.

Examples:

scale = Scaling(2, 2)  # Double size

Methods:

Name Description
inverted

Get the inverse scaling.

to_matrix

Convert to a scaling matrix.

inverted

inverted() -> Transformation

Get the inverse scaling.

Returns:

Type Description
Transformation

Scaling by (1/sx, 1/sy).

Source code in src/momapy/geometry.py
def inverted(self) -> Transformation:
    """Get the inverse scaling.

    Returns:
        Scaling by (1/sx, 1/sy).
    """
    return Scaling(1 / self.sx, 1 / self.sy)

to_matrix

to_matrix() -> NDArray

Convert to a scaling matrix.

Returns:

Type Description
NDArray

A 3x3 scaling matrix.

Source code in src/momapy/geometry.py
def to_matrix(self) -> numpy.typing.NDArray:
    """Convert to a scaling matrix.

    Returns:
        A 3x3 scaling matrix.
    """
    m = numpy.array(
        [
            [self.sx, 0, 0],
            [0, self.sy, 0],
            [0, 0, 1],
        ],
        dtype=float,
    )
    return m

Segment dataclass

Segment(p1: Point, p2: Point)

Bases: GeometryObject

Represents a line segment between two points.

Attributes:

Name Type Description
p1 Point

Start point.

p2 Point

End point.

Examples:

seg = Segment(Point(0, 0), Point(10, 10))
seg.length()
seg.get_position_at_fraction(0.5)

Methods:

Name Description
bbox

Get the bounding box of the segment.

get_angle_at_fraction

Get angle at a fraction along the segment.

get_angle_to_horizontal

Get the angle of the segment relative to horizontal.

get_distance_to_point

Get shortest distance from a point to this segment.

get_intersection_with_line

Get intersection with a line.

get_position_and_angle_at_fraction

Get both position and angle at a fraction.

get_position_at_fraction

Get point at a fraction along the segment.

has_point

Check if a point lies on this segment.

length

Calculate the length of the segment.

reversed

Return a reversed copy of the segment.

shortened

Return a shortened copy of the segment.

transformed

Apply a transformation to this segment.

bbox

bbox() -> Bbox

Get the bounding box of the segment.

Returns:

Type Description
Bbox

A Bbox enclosing the segment.

Source code in src/momapy/geometry.py
def bbox(self) -> "Bbox":
    """Get the bounding box of the segment.

    Returns:
        A Bbox enclosing the segment.
    """
    return Bbox.around_points([self.p1, self.p2])

get_angle_at_fraction

get_angle_at_fraction(fraction: float) -> float

Get angle at a fraction along the segment.

Parameters:

Name Type Description Default
fraction float

Fraction from 0 to 1.

required

Returns:

Type Description
float

Angle in radians.

Source code in src/momapy/geometry.py
def get_angle_at_fraction(self, fraction: float) -> float:
    """Get angle at a fraction along the segment.

    Args:
        fraction: Fraction from 0 to 1.

    Returns:
        Angle in radians.
    """
    return self.get_angle_to_horizontal()

get_angle_to_horizontal

get_angle_to_horizontal() -> float

Get the angle of the segment relative to horizontal.

Returns:

Type Description
float

Angle in radians.

Source code in src/momapy/geometry.py
def get_angle_to_horizontal(self) -> float:
    """Get the angle of the segment relative to horizontal.

    Returns:
        Angle in radians.
    """
    angle = math.atan2(self.p2.y - self.p1.y, self.p2.x - self.p1.x)
    return get_normalized_angle(angle)

get_distance_to_point

get_distance_to_point(point: Point) -> float

Get shortest distance from a point to this segment.

Parameters:

Name Type Description Default
point Point

The point to measure from.

required

Returns:

Type Description
float

The shortest distance.

Source code in src/momapy/geometry.py
def get_distance_to_point(self, point: Point) -> float:
    """Get shortest distance from a point to this segment.

    Args:
        point: The point to measure from.

    Returns:
        The shortest distance.
    """
    a = point.x - self.p1.x
    b = point.y - self.p1.y
    c = self.p2.x - self.p1.x
    d = self.p2.y - self.p1.y
    dot = a * c + b * d
    len_sq = c**2 + d**2
    if len_sq != 0:
        param = dot / len_sq
    else:
        param = -1
    if param < 0:
        xx = self.p1.x
        yy = self.p1.y
    elif param > 1:
        xx = self.p2.x
        yy = self.p2.y
    else:
        xx = self.p1.x + param * c
        yy = self.p1.y + param * d
    dx = point.x - xx
    dy = point.y - yy
    return math.sqrt(dx**2 + dy**2)

get_intersection_with_line

get_intersection_with_line(line: Line) -> list[Point] | list[Segment]

Get intersection with a line.

Parameters:

Name Type Description Default
line Line

The line to intersect with.

required

Returns:

Type Description
list[Point] | list[Segment]

List of intersection points or segment if coincident.

Source code in src/momapy/geometry.py
def get_intersection_with_line(self, line: Line) -> list[Point] | list["Segment"]:
    """Get intersection with a line.

    Args:
        line: The line to intersect with.

    Returns:
        List of intersection points or segment if coincident.
    """
    line2 = Line(self.p1, self.p2)
    line_intersection = line.get_intersection_with_line(line2)
    result: list[Point] | list[Segment] = []
    if len(line_intersection) > 0 and isinstance(line_intersection[0], Point):
        sorted_xs = sorted([self.p1.x, self.p2.x])
        sorted_ys = sorted([self.p1.y, self.p2.y])
        if (
            line_intersection[0].x >= sorted_xs[0]
            and line_intersection[0].x <= sorted_xs[-1]
            and line_intersection[0].y >= sorted_ys[0]
            and line_intersection[0].y <= sorted_ys[-1]
        ):
            result = [line_intersection[0]]
    elif len(line_intersection) > 0:
        result = [self]
    return result

get_position_and_angle_at_fraction

get_position_and_angle_at_fraction(fraction: float) -> tuple[Point, float]

Get both position and angle at a fraction.

Parameters:

Name Type Description Default
fraction float

Fraction from 0 to 1.

required

Returns:

Type Description
tuple[Point, float]

Tuple of (point, angle in radians).

Source code in src/momapy/geometry.py
def get_position_and_angle_at_fraction(
    self, fraction: float
) -> tuple[Point, float]:
    """Get both position and angle at a fraction.

    Args:
        fraction: Fraction from 0 to 1.

    Returns:
        Tuple of (point, angle in radians).
    """
    return (
        self.get_position_at_fraction(fraction),
        self.get_angle_at_fraction(fraction),
    )

get_position_at_fraction

get_position_at_fraction(fraction: float) -> Point

Get point at a fraction along the segment.

Parameters:

Name Type Description Default
fraction float

Fraction from 0 to 1 (0 = start, 1 = end).

required

Returns:

Type Description
Point

The point at that fraction.

Source code in src/momapy/geometry.py
def get_position_at_fraction(self, fraction: float) -> Point:
    """Get point at a fraction along the segment.

    Args:
        fraction: Fraction from 0 to 1 (0 = start, 1 = end).

    Returns:
        The point at that fraction.
    """
    x = self.p1.x + fraction * (self.p2.x - self.p1.x)
    y = self.p1.y + fraction * (self.p2.y - self.p1.y)
    return Point(x, y)

has_point

has_point(point: Point, max_distance: float = ROUNDING_TOLERANCE) -> bool

Check if a point lies on this segment.

Parameters:

Name Type Description Default
point Point

The point to check.

required
max_distance float

Maximum allowed distance.

ROUNDING_TOLERANCE

Returns:

Type Description
bool

True if point is on the segment within tolerance.

Source code in src/momapy/geometry.py
def has_point(self, point: Point, max_distance: float = ROUNDING_TOLERANCE) -> bool:
    """Check if a point lies on this segment.

    Args:
        point: The point to check.
        max_distance: Maximum allowed distance.

    Returns:
        True if point is on the segment within tolerance.
    """
    return self.get_distance_to_point(point) <= max_distance

length

length() -> float

Calculate the length of the segment.

Returns:

Type Description
float

The Euclidean length.

Source code in src/momapy/geometry.py
def length(self) -> float:
    """Calculate the length of the segment.

    Returns:
        The Euclidean length.
    """
    return math.sqrt((self.p2.x - self.p1.x) ** 2 + (self.p2.y - self.p1.y) ** 2)

reversed

reversed() -> Segment

Return a reversed copy of the segment.

Returns:

Type Description
Segment

A new Segment with p1 and p2 swapped.

Source code in src/momapy/geometry.py
def reversed(self) -> "Segment":
    """Return a reversed copy of the segment.

    Returns:
        A new Segment with p1 and p2 swapped.
    """
    return Segment(self.p2, self.p1)

shortened

shortened(length: float, start_or_end: Literal['start', 'end'] = 'end') -> Segment

Return a shortened copy of the segment.

Parameters:

Name Type Description Default
length float

Amount to shorten by.

required
start_or_end Literal['start', 'end']

Which end to shorten from.

'end'

Returns:

Type Description
Segment

A new shortened Segment.

Source code in src/momapy/geometry.py
def shortened(
    self,
    length: float,
    start_or_end: typing.Literal["start", "end"] = "end",
) -> "Segment":
    """Return a shortened copy of the segment.

    Args:
        length: Amount to shorten by.
        start_or_end: Which end to shorten from.

    Returns:
        A new shortened Segment.
    """
    if length == 0 or self.length() == 0:
        return copy.deepcopy(self)
    if start_or_end == "start":
        return self.reversed().shortened(length).reversed()
    fraction = 1 - length / self.length()
    point = self.get_position_at_fraction(fraction)
    return Segment(self.p1, point)

transformed

transformed(transformation: Transformation) -> Segment

Apply a transformation to this segment.

Parameters:

Name Type Description Default
transformation Transformation

The transformation to apply.

required

Returns:

Type Description
Segment

A new transformed Segment.

Source code in src/momapy/geometry.py
def transformed(self, transformation: "Transformation") -> "Segment":
    """Apply a transformation to this segment.

    Args:
        transformation: The transformation to apply.

    Returns:
        A new transformed Segment.
    """
    return Segment(
        self.p1.transformed(transformation),
        self.p2.transformed(transformation),
    )

Transformation dataclass

Transformation()

Bases: ABC

Abstract base class for geometric transformations.

Methods:

Name Description
inverted

Get the inverse transformation.

to_matrix

Convert to a 3x3 transformation matrix.

inverted abstractmethod

inverted() -> Transformation

Get the inverse transformation.

Returns:

Type Description
Transformation

The inverse transformation.

Source code in src/momapy/geometry.py
@abc.abstractmethod
def inverted(self) -> "Transformation":
    """Get the inverse transformation.

    Returns:
        The inverse transformation.
    """
    pass

to_matrix abstractmethod

to_matrix() -> NDArray

Convert to a 3x3 transformation matrix.

Returns:

Type Description
NDArray

A 3x3 numpy array.

Source code in src/momapy/geometry.py
@abc.abstractmethod
def to_matrix(self) -> numpy.typing.NDArray:
    """Convert to a 3x3 transformation matrix.

    Returns:
        A 3x3 numpy array.
    """
    pass

Translation dataclass

Translation(tx: float, ty: float)

Bases: Transformation

Represents a translation transformation.

Attributes:

Name Type Description
tx float

Translation in x direction.

ty float

Translation in y direction.

Examples:

trans = Translation(10, 20)

Methods:

Name Description
inverted

Get the inverse translation.

to_matrix

Convert to a translation matrix.

inverted

inverted() -> Transformation

Get the inverse translation.

Returns:

Type Description
Transformation

A translation by (-tx, -ty).

Source code in src/momapy/geometry.py
def inverted(self) -> Transformation:
    """Get the inverse translation.

    Returns:
        A translation by (-tx, -ty).
    """
    return Translation(-self.tx, -self.ty)

to_matrix

to_matrix() -> NDArray

Convert to a translation matrix.

Returns:

Type Description
NDArray

A 3x3 translation matrix.

Source code in src/momapy/geometry.py
def to_matrix(self) -> numpy.typing.NDArray:
    """Convert to a translation matrix.

    Returns:
        A 3x3 translation matrix.
    """
    m = numpy.array([[1, 0, self.tx], [0, 1, self.ty], [0, 0, 1]], dtype=float)
    return m

get_normalized_angle

get_normalized_angle(angle: float) -> float

Normalize an angle to [0, 2*pi).

Parameters:

Name Type Description Default
angle float

Angle in radians.

required

Returns:

Type Description
float

Normalized angle.

Source code in src/momapy/geometry.py
def get_normalized_angle(angle: float) -> float:
    """Normalize an angle to [0, 2*pi).

    Args:
        angle: Angle in radians.

    Returns:
        Normalized angle.
    """
    return angle - (angle // (2 * math.pi) * (2 * math.pi))

get_primitives_anchor_point

get_primitives_anchor_point(primitives: list[Segment | QuadraticBezierCurve | CubicBezierCurve | EllipticalArc], anchor_point: str, center: Point | None = None) -> Point | None

Get an anchor point of geometry primitives.

Parameters:

Name Type Description Default
primitives list[Segment | QuadraticBezierCurve | CubicBezierCurve | EllipticalArc]

List of geometry primitives.

required
anchor_point str

Name of anchor point.

required
center Point | None

Optional center point.

None

Returns:

Type Description
Point | None

The anchor point or None.

Source code in src/momapy/geometry.py
def get_primitives_anchor_point(
    primitives: list[
        "Segment | QuadraticBezierCurve | CubicBezierCurve | EllipticalArc"
    ],
    anchor_point: str,
    center: Point | None = None,
) -> Point | None:
    """Get an anchor point of geometry primitives.

    Args:
        primitives: List of geometry primitives.
        anchor_point: Name of anchor point.
        center: Optional center point.

    Returns:
        The anchor point or None.
    """
    bboxes = [p.bbox() for p in primitives]
    bbox = Bbox.union(bboxes)
    if center is None:
        center = bbox.center()
    if center.isnan():
        return None
    point = bbox.anchor_point(anchor_point)
    return get_primitives_border(primitives, point, center)

get_primitives_angle

get_primitives_angle(primitives: list[Segment | QuadraticBezierCurve | CubicBezierCurve | EllipticalArc], angle: float, unit: Literal['degrees', 'radians'] = 'degrees', center: Point | None = None) -> Point | None

Get the border point at a given angle.

Parameters:

Name Type Description Default
primitives list[Segment | QuadraticBezierCurve | CubicBezierCurve | EllipticalArc]

List of geometry primitives.

required
angle float

The angle.

required
unit Literal['degrees', 'radians']

Unit of angle ('degrees' or 'radians').

'degrees'
center Point | None

Optional center point.

None

Returns:

Type Description
Point | None

The border point or None.

Source code in src/momapy/geometry.py
def get_primitives_angle(
    primitives: list[Segment | QuadraticBezierCurve | CubicBezierCurve | EllipticalArc],
    angle: float,
    unit: typing.Literal["degrees", "radians"] = "degrees",
    center: Point | None = None,
) -> Point | None:
    """Get the border point at a given angle.

    Args:
        primitives: List of geometry primitives.
        angle: The angle.
        unit: Unit of angle ('degrees' or 'radians').
        center: Optional center point.

    Returns:
        The border point or None.
    """
    if unit == "degrees":
        angle = math.radians(angle)
    angle = -angle
    d = 100
    if center is None:
        bboxes = [p.bbox() for p in primitives]
        bbox = Bbox.union(bboxes)
        center = bbox.center()
        if center.isnan():
            return None
    point = center + (d * math.cos(angle), d * math.sin(angle))
    return get_primitives_border(primitives, point, center)

get_primitives_border

get_primitives_border(primitives: list[Segment | QuadraticBezierCurve | CubicBezierCurve | EllipticalArc], point: Point, center: Point | None = None) -> Point | None

Get the border point of geometry primitives in a given direction.

Parameters:

Name Type Description Default
primitives list[Segment | QuadraticBezierCurve | CubicBezierCurve | EllipticalArc]

List of geometry primitives.

required
point Point

Direction point.

required
center Point | None

Optional center point. Defaults to bbox center.

None

Returns:

Type Description
Point | None

The border point or None.

Source code in src/momapy/geometry.py
def get_primitives_border(
    primitives: list[Segment | QuadraticBezierCurve | CubicBezierCurve | EllipticalArc],
    point: Point,
    center: Point | None = None,
) -> Point | None:
    """Get the border point of geometry primitives in a given direction.

    Args:
        primitives: List of geometry primitives.
        point: Direction point.
        center: Optional center point. Defaults to bbox center.

    Returns:
        The border point or None.
    """
    if not primitives:
        return None
    if center is None:
        bboxes = [p.bbox() for p in primitives]
        bbox = Bbox.union(bboxes)
        center = bbox.center()
    if center.isnan():
        return None
    line = Line(center, point)
    candidate_points = []
    for primitive in primitives:
        candidate_points.extend(_intersect_line_with_primitive(line, primitive))
    intersection_point = None
    max_d = -1
    ok_direction_exists = False
    d1 = Segment(point, center).length()
    for candidate_point in candidate_points:
        d2 = Segment(candidate_point, point).length()
        d3 = Segment(candidate_point, center).length()
        candidate_ok_direction = not d2 > d1 or d2 < d3
        if candidate_ok_direction or not ok_direction_exists:
            if candidate_ok_direction and not ok_direction_exists:
                ok_direction_exists = True
                max_d = -1
            if d3 > max_d:
                max_d = d3
                intersection_point = candidate_point
    return intersection_point

get_transformation_for_frame

get_transformation_for_frame(origin: Point, unit_x: Point, unit_y: Point) -> MatrixTransformation

Get transformation for a frame defined by origin and axes.

Given a frame F defined by its origin, unit x axis vector, and unit y axis vector, returns the transformation that converts points from F coordinates to reference frame coordinates.

Parameters:

Name Type Description Default
origin Point

Origin of the frame.

required
unit_x Point

Unit x-axis vector.

required
unit_y Point

Unit y-axis vector.

required

Returns:

Type Description
MatrixTransformation

The transformation matrix.

Source code in src/momapy/geometry.py
def get_transformation_for_frame(
    origin: Point, unit_x: Point, unit_y: Point
) -> MatrixTransformation:
    """Get transformation for a frame defined by origin and axes.

    Given a frame F defined by its origin, unit x axis vector, and unit y axis
    vector, returns the transformation that converts points from F coordinates
    to reference frame coordinates.

    Args:
        origin: Origin of the frame.
        unit_x: Unit x-axis vector.
        unit_y: Unit y-axis vector.

    Returns:
        The transformation matrix.
    """
    m = numpy.array(
        [
            [
                unit_x.x - origin.x,
                unit_y.x - origin.x,
                origin.x,
            ],
            [
                unit_x.y - origin.y,
                unit_y.y - origin.y,
                origin.y,
            ],
            [0, 0, 1],
        ],
        dtype=float,
    )
    return MatrixTransformation(m)