from __future__ import annotations
from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union
from collections.abc import Callable, Generator
from fontParts.base.errors import FontPartsError
from fontParts.base.base import (
BaseObject,
TransformationMixin,
InterpolationMixin,
SelectionMixin,
dynamicProperty,
reference,
)
from fontParts.base import normalizers
from fontParts.base.deprecated import DeprecatedSegment, RemovedSegment
from fontParts.base.compatibility import SegmentCompatibilityReporter
from fontParts.base.annotations import (
AffineTransformationLike,
CollectionType,
IntFloatType,
)
if TYPE_CHECKING:
from fontParts.base.point import BasePoint
from fontParts.base.contour import BaseContour
from fontParts.base.glyph import BaseGlyph
from fontParts.base.layer import BaseLayer
from fontParts.base.font import BaseFont
[docs]
class BaseSegment(
BaseObject,
TransformationMixin,
InterpolationMixin,
SelectionMixin,
DeprecatedSegment,
RemovedSegment,
):
"""Represent the basis for a segment object."""
def _setPoints(self, points: CollectionType[BasePoint]) -> None:
if hasattr(self, "_points"):
raise AssertionError("segment has points")
self._points = points
def _reprContents(self) -> list[str]:
contents = [f"{self.type}"]
if self.index is not None:
contents.append(f"index='{self.index!r}'")
return contents
# this class should not be used in hashable
# collections since it is dynamically generated.
__hash__ = None # type: ignore[assignment]
# -------
# Parents
# -------
# Contour
_contour: Callable[[], BaseContour] | None = None
contour: dynamicProperty = dynamicProperty(
"contour",
"""Get or set the segment's parent contour object.
The value must be a :class:`BaseContour` instance or :obj:`None`.
:return: The :class:`BaseContour` instance containing the segment
or :obj:`None`.
:raises AssertionError: If attempting to set the contour when it
has already been set.
Example::
>>> contour = segment.contour
""",
)
def _get_contour(self) -> BaseContour | None:
if self._contour is None:
return None
return self._contour()
def _set_contour(
self, contour: BaseContour | Callable[[], BaseContour] | None
) -> None:
if self._contour is not None:
raise AssertionError("contour for segment already set")
if contour is not None:
contour = reference(contour)
self._contour = contour
# Glyph
glyph: dynamicProperty = dynamicProperty(
"glyph",
"""Get the segment's parent glyph object.
This property is read-only.
The value must be a :class:`BaseGlyph` instance or :obj:`None`.
:return: The :class:`BaseGlyph` instance containing the segment
or :obj:`None`.
Example::
>>> glyph = segment.glyph
""",
)
def _get_glyph(self) -> BaseGlyph | None:
if self._contour is None:
return None
return self.contour.glyph
# Layer
layer: dynamicProperty = dynamicProperty(
"layer",
"""Get the segment's parent layer object.
This property is read-only.
:return: The :class:`BaseLayer` instance containing the segemnt
or :obj:`None`.
Example::
>>> layer = segment.layer
""",
)
def _get_layer(self) -> BaseLayer | None:
if self._contour is None:
return None
return self.glyph.layer
# Font
font: dynamicProperty = dynamicProperty(
"font",
"""Get the segment's parent font object.
This property is read-only.
:return: The :class:`BaseFont` instance containing the segment
or :obj:`None`.
Example::
>>> font = segment.font
""",
)
def _get_font(self) -> BaseFont | None:
if self._contour is None:
return None
return self.glyph.font
# --------
# Equality
# --------
def __eq__(self, other: object) -> bool:
"""Check for equality with another segment.
The :meth:`BaseObject.__eq__` method can't be used here
because the :class:`BaseContour` implementation contructs
segment objects without assigning an underlying `naked`
object. Therefore, comparisons will always fail. This
method overrides the base method and compares the
:class:`BasePoint` contained by the segment.
:param other: The segment to compare with.
:return: :obj:`True` if the segments are equal, :obj:`False` otherwise.
.. note::
Subclasses may override this method.
"""
if isinstance(other, self.__class__):
return self.points == other.points
return NotImplemented
# --------------
# Identification
# --------------
index: dynamicProperty = dynamicProperty(
"base_index",
"""Get the index of the segment.
This property is read-only.
:return: An :class:`int` representing the segment's index within an
ordered list of the parent contour's segments, or :obj:`None` if the
segment does not belong to a contour.
Example::
>>> segment.index
0
""",
)
def _get_base_index(self) -> int | None:
if self.contour is None:
return None
value = self._get_index()
normalizedValue = normalizers.normalizeIndex(value)
return normalizedValue
[docs]
def _get_index(self) -> int:
"""Get the index of the native segment.
This is the environment implementation of the :attr:`BaseSegment.index`
property getter.
:return: An :class:`int` representing the segment's index within an
ordered list of the parent contour's segments, or :obj:`None` if the
segment does not belong to a contour.
.. note::
Subclasses may override this method.
"""
contour = self.contour
value = contour.segments.index(self)
return value
# ----------
# Attributes
# ----------
type: dynamicProperty = dynamicProperty(
"base_type",
"""Get or set the segment's type.
The value must be a :class:`str` containing one of the following
alternatives:
+----------------+---------------------------------+
| Type | Description |
+----------------+---------------------------------+
| ``'move'`` | An on-curve move to. |
| ``'line'`` | An on-curve line to. |
| ``'curve'`` | An on-curve cubic curve to. |
| ``'qcurve'`` | An on-curve quadratic curve to. |
+----------------+---------------------------------+
:return: A :class:`str` representing the type of the segment.
""",
)
def _get_base_type(self) -> str:
value = self._get_type()
value = normalizers.normalizeSegmentType(value)
return value
def _set_base_type(self, value: str) -> None:
value = normalizers.normalizeSegmentType(value)
self._set_type(value)
[docs]
def _get_type(self) -> str:
"""Get the native segment's type.
This is the environment implementation of the :attr:`BaseSegment.type`
property getter.
:return: A :class:`str` representing the type of the segment. The value
will have been normalized with :func:`normalizers.normalizeSegmentType`.
.. note::
Subclasses may override this method.
"""
onCurve = self.onCurve
if onCurve is None:
return "qcurve"
return onCurve.type
[docs]
def _set_type(self, newType: str) -> None:
"""Set the native segment's type.
This is the environment implementation of the :attr:`BaseSegment.type`
property setter.
:param newType: The segment type definition as a :class:`str`. The value
will have been normalized with :func:`normalizers.normalizeSegmentType`.
:raises FontPartsError: If the segment does not belong to a contour.
.. note::
Subclasses may override this method.
"""
oldType = self.type
if oldType == newType:
return
if self.onCurve is None:
# special case with a single qcurve segment
# and only offcurves, don't convert
return
contour = self.contour
if contour is None:
raise FontPartsError("The segment does not belong to a contour.")
# converting line <-> move
if newType in ("move", "line") and oldType in ("move", "line"):
pass
# converting to a move or line
elif newType not in ("curve", "qcurve"):
offCurves = self.offCurve
for point in offCurves:
contour.removePoint(point)
# converting a line/move to a curve/qcurve
else:
segments = contour.segments
i = segments.index(self)
prev = segments[i - 1].onCurve
on = self.onCurve
x = on.x
y = on.y
points = contour.points
i = points.index(on)
contour.insertPoint(i, (x, y), "offcurve")
off2 = contour.points[i]
contour.insertPoint(i, (prev.x, prev.y), "offcurve")
off1 = contour.points[i]
del self._points
self._setPoints((off1, off2, on))
self.onCurve.type = newType
smooth: dynamicProperty = dynamicProperty(
"base_smooth",
"""Get or set the segment's smooth state.
The value must be a :class:`bool` indicating the segment's smooth state.
:return: :obj:`True` if the segment is smooth, :obj:`False` if it is sharp.
Example::
>>> segment.smooth
False
>>> segment.smooth = True
""",
)
def _get_base_smooth(self) -> bool:
value = self._get_smooth()
value = normalizers.normalizeBoolean(value)
return value
def _set_base_smooth(self, value: bool) -> None:
value = normalizers.normalizeBoolean(value)
self._set_smooth(value)
[docs]
def _get_smooth(self) -> bool:
"""Get the native segment's smooth state.
This is the environment implementation of the :attr:`BaseSegment.smooth`
property getter.
:return: :obj:`True` if the segment is smooth, :obj:`False` if it is
sharp. The value will have been normalized
with :func:`normalizers.normalizeBoolean`.
.. note::
Subclasses may override this method.
"""
onCurve = self.onCurve
if onCurve is None:
return True
return onCurve.smooth
[docs]
def _set_smooth(self, value: bool) -> None:
"""Set the native segment's smooth state.
This is the environment implementation of the :attr:`BaseSegment.smooth`
property setter.
:param value: The point's smooth state as a :class:`bool`. The value
will have been normalized with :func:`normalizers.normalizeBoolean`.
.. note::
Subclasses may override this method.
"""
onCurve = self.onCurve
if onCurve is not None:
self.onCurve.smooth = value
# ------
# Points
# ------
def __getitem__(self, index: int) -> BasePoint:
"""Get the point at the specified index.
:param index: The zero-based index of the point to retrieve as
an :class:`int`.
:return: The :class:`BasePoint` instance located at the specified `index`.
:raises IndexError: If the specified `index` is out of range.
"""
return self._getItem(index)
[docs]
def _getItem(self, index: int) -> BasePoint:
"""Get the native point at the specified index.
This is the environment implementation of :meth:`BaseSegment.__getitem__`.
:param index: The zero-based index of the point to retrieve as
an :class:`int`.
:return: The :class:`BasePoint` instance located at the specified `index`.
:raises IndexError: If the specified `index` is out of range.
.. note::
Subclasses may override this method.
"""
return self.points[index]
def __iter__(self):
"""Return an iterator over the points in the segment.
:return: An iterator over the :class:`BasePoint` instances belonging to
the segment.
"""
return self._iterPoints()
[docs]
def _iterPoints(self, **kwargs: Any) -> Generator[BasePoint]:
"""Return an iterator over the points in the native segment.
This is the environment implementation of :meth:`BaseSegment.__iter__`.
:return: An iterator over the :class:`BasePoint` subclass instances
belonging to the segment.
.. note::
Subclasses may override this method.
"""
points = self.points
count = len(points)
index = 0
while count:
yield points[index]
count -= 1
index += 1
def __len__(self) -> int:
"""Return the number of points in the segment.
:return: An :class:`int` representing the number of :class:`BasePoint`
instances belonging to the segment.
"""
return self._len()
[docs]
def _len(self, **kwargs: Any) -> int:
"""Return the number of points in the native segment.
This is the environment implementation of :meth:`BaseSegment.__len__`.
:return: An :class:`int` representing the number of :class:`BasePoint`
subclass instances belonging to the segment.
.. note::
Subclasses may override this method.
"""
return len(self.points)
points: dynamicProperty = dynamicProperty(
"base_points",
"""Get a list of all points in the segment.
This attribute is read-only.
:return: A :class:`tuple` of :class`BasePoint` instances.
""",
)
def _get_base_points(self) -> tuple[BasePoint, ...]:
return self._get_points()
[docs]
def _get_points(self) -> tuple[BasePoint, ...]:
"""Get a list of all points in the native segment.
This is the environment implementation of the :attr:`BaseSegment.points`
property getter.
:return: A :class:`tuple` of :class`BasePoints`.
.. note::
Subclasses may override this method.
"""
if not hasattr(self, "_points"):
return ()
return tuple(self._points)
onCurve: dynamicProperty = dynamicProperty(
"base_onCurve",
"""Get the on-curve point in the segment.
This property is read-only.
:return: An on-curve :class:`BasePoint` instance or :obj:`None`.
""",
)
def _get_base_onCurve(self) -> BasePoint | None:
return self._get_onCurve()
[docs]
def _get_onCurve(self) -> BasePoint | None:
"""Get the on-curve point in the native segment.
This is the environment implementation of
the :attr:`BaseSegment.onCurve` property getter.
:return: An on-curve :class:`BasePoint` instance or :obj:`None`.
.. note::
Subclasses may override this method.
"""
value = self.points[-1]
if value.type == "offcurve":
return None
return value
offCurve: dynamicProperty = dynamicProperty(
"base_offCurve",
"""Get the off-curve points in the segment.
This property is read-only.
:return: An off-curve :class:`BasePoint` instance or :obj:`None`.
""",
)
[docs]
def _get_base_offCurve(self) -> tuple[BasePoint, ...]:
return self._get_offCurve()
[docs]
def _get_offCurve(self) -> tuple[BasePoint, ...]:
"""Get the off-curve points in the native segment.
This is the environment implementation of
the :attr:`BaseSegment.offCurve` property getter.
:return: An off-curve :class:`BasePoint` instance or :obj:`None`.
.. note::
Subclasses may override this method.
"""
if self.points and self.points[-1].type == "offcurve":
return self.points
return self.points[:-1]
# --------------
# Transformation
# --------------
# -------------
# Interpolation
# -------------
compatibilityReporterClass = SegmentCompatibilityReporter
def isCompatible(
self, other: BaseSegment, cls=None
) -> tuple[bool, SegmentCompatibilityReporter]:
"""Evaluate interpolation compatibility with another segment.
This method will return a :class:`bool` indicating if the segment is
compatible for interpolation with `other`, and a :class:`str`
containing compatibility notes.
:param other: The other :class:`BaseSegment` instance to check
compatibility with.
:return: A :class:`tuple` where the first element is a :class:`bool`
indicating compatibility, and the second element is
a :class:`fontParts.base.compatibility.SegmentCompatibilityReporter`
instance.
Example::
>>> compatible, report = self.isCompatible(otherSegment)
>>> compatible
False
>>> compatible
[Fatal] Segment: [0] + [0]
[Fatal] Segment: [0] is line | [0] is move
[Fatal] Segment: [1] + [1]
[Fatal] Segment: [1] is line | [1] is qcurve
"""
return super().isCompatible(other, BaseSegment)
def _isCompatible(
self, other: BaseSegment, reporter: SegmentCompatibilityReporter
) -> None:
"""Evaluate interpolation compatibility with another native segment.
This is the environment implementation of :meth:`BaseSegment.isCompatible`.
:param other: The other :class:`BaseSegment` instance to check
compatibility with.
:param reporter: An object used to report compatibility issues.
.. note::
Subclasses may override this method.
"""
segment1 = self
segment2 = other
# type
if segment1.type != segment2.type:
# line <-> curve can be converted
if {segment1.type, segment2.type} != {"curve", "line"}:
reporter.typeDifference = True
reporter.fatal = True
# ----
# Misc
# ----
[docs]
def round(self) -> None:
"""Round coordinates in all the segment's points."""
for point in self.points:
point.round()