Source code for fontParts.base.contour

from __future__ import annotations
from typing import TYPE_CHECKING, cast, Any, List, Optional, Tuple, Type, TypeVar, Union
from collections.abc import Callable, Iterator

from fontParts.base.errors import FontPartsError
from fontParts.base.base import (
    BaseObject,
    TransformationMixin,
    InterpolationMixin,
    SelectionMixin,
    IdentifierMixin,
    dynamicProperty,
    reference,
)
from fontParts.base import normalizers
from fontParts.base.compatibility import ContourCompatibilityReporter
from fontParts.base.deprecated import DeprecatedContour, RemovedContour
from fontParts.base.annotations import (
    AffineTransformationLike,
    BoundingBox,
    Coordinate,
    CoordinateLike,
    CollectionType,
    IntFloatType,
    PenType,
    PointPenType,
)

if TYPE_CHECKING:
    from fontParts.base.point import BasePoint
    from fontParts.base.bPoint import BaseBPoint
    from fontParts.base.segment import BaseSegment
    from fontParts.base.glyph import BaseGlyph
    from fontParts.base.layer import BaseLayer
    from fontParts.base.font import BaseFont

BaseContourType = TypeVar("BaseContourType", bound="BaseContour")
PointCollectionType = CollectionType[CoordinateLike]


[docs] class BaseContour( BaseObject, TransformationMixin, InterpolationMixin, SelectionMixin, IdentifierMixin, DeprecatedContour, RemovedContour, ): """Represent the basis for a contour object. :cvar segmentClass: A class representing contour segments. This will usually be a :class:`BaseSegment` subclass. :cvar bPointClass: A class representing contour bPoints. This will usually be a :class:`BaseBPoint` subclass. """ segmentClass: type[BaseSegment] | None = None bPointClass: type[BaseBPoint] | None = None def _reprContents(self) -> list[str]: contents = [] if self.identifier is not None: contents.append(f"identifier='{self.identifier!r}'") if self.glyph is not None: contents.append("in glyph") contents += self.glyph._reprContents() return contents def copyData(self, source: BaseContourType) -> None: """Copy data from another contour instance. This will copy the contents of the following attributes from `source` into the current contour instance: - :attr:`BaseContour.points` - :attr:`BaseContour.bPoints` :param source: The source :class:`BaseContour` instance from which to copy data. Example:: >>> contour.copyData(sourceContour) """ super().copyData(source) for sourcePoint in source.points: self.appendPoint((0, 0)) selfPoint = self.points[-1] selfPoint.copyData(sourcePoint) # ------- # Parents # ------- # Glyph _glyph: Callable[[], BaseGlyph] | None = None glyph: dynamicProperty = dynamicProperty( "glyph", """Get or set the contour's parent glyph object. The value must be a :class:`BaseGlyph` instance or :obj:`None`. :return: The :class:`BaseGlyph` instance containing the contour or :obj:`None`. :raises AssertionError: If attempting to set the glyph when it has already been set. Example:: >>> glyph = contour.glyph """, ) def _get_glyph(self) -> BaseGlyph | None: if self._glyph is None: return None return self._glyph() def _set_glyph(self, glyph: BaseGlyph | Callable[[], BaseGlyph] | None) -> None: if self._glyph is not None: raise AssertionError("glyph for contour already set") if glyph is not None: glyph = reference(glyph) self._glyph = glyph # Font font: dynamicProperty = dynamicProperty( "font", """Get the contour's parent font object. This property is read-only. :return: The :class:`BaseFont` instance containing the contour or :obj:`None`. Example:: >>> font = contour.font """, ) def _get_font(self) -> BaseFont | None: if self._glyph is None: return None return self.glyph.font # Layer layer: dynamicProperty = dynamicProperty( "layer", """Get the contour's parent layer object. This property is read-only. :return: The :class:`BaseLayer` instance containing the contour or :obj:`None`. Example:: >>> layer = contour.layer """, ) def _get_layer(self) -> BaseLayer | None: if self._glyph is None: return None return self.glyph.layer # -------------- # Identification # -------------- # index index: dynamicProperty = dynamicProperty( "base_index", """Get or set the index of the contour. The value must be an :class:`int`. :return: An :class:`int` representing the contour's index within an ordered list of the parent glyph's contours, or :obj:`None` if the contour does not belong to a glyph. :raises FontPartsError: If the contour does not belong to a glyph. Example:: >>> contour.index 1 >>> contour.index = 0 """, ) def _get_base_index(self) -> int | None: glyph = self.glyph if glyph is None: return None value = self._get_index() value = normalizers.normalizeIndex(value) return value def _set_base_index(self, value: int) -> None: glyph = self.glyph if glyph is None: raise FontPartsError("The contour does not belong to a glyph.") normalizedValue = normalizers.normalizeIndex(value) if normalizedValue is None: return contourCount = len(glyph.contours) if normalizedValue < 0: normalizedValue = -(normalizedValue % contourCount) if normalizedValue >= contourCount: normalizedValue = contourCount self._set_index(normalizedValue)
[docs] def _get_index(self) -> int | None: """Get the index of the native contour. This is the environment implementation of the :attr:`BaseContour.index` property getter. :return: An :class:`int` representing the contour's index within an ordered list of the parent glyph's contours, or :obj:`None` if the contour does not belong to a glyph. The value will be normalized with :func:`normalizers.normalizeIndex`. .. note:: Subclasses may override this method. """ glyph = self.glyph return glyph.contours.index(self)
[docs] def _set_index(self, value: int) -> None: """Set the index of the contour. This is the environment implementation of the :attr:`BaseContour.index` property setter. :param value: The index to set as an :class:`int`. The value will have been normalized with :func:`normalizers.normalizeIndex`. :raises NotImplementedError: If the method has not been overridden by a subclass. .. important:: Subclasses must override this method. """ self.raiseNotImplementedError()
# identifier def getIdentifierForPoint(self, point: BasePoint) -> str: """Generate and assign a unique identifier to the given point. If `point` already has an identifier, the existing identifier is returned. Otherwise, a new unique identifier is created and assigned to `point`. :param point: The :class:`BasePoint` instance to which the identifier should be assigned. :return: A :class:`str` representing the newly assigned identifier. Example:: >>> contour.getIdentifierForPoint(point) 'ILHGJlygfds' """ point = normalizers.normalizePoint(point) return self._getIdentifierForPoint(point) def _getIdentifierForPoint(self, point: BasePoint) -> str: """Generate and assign a unique identifier to the given native point. This is the environment implementation of :meth:`BaseContour.getIdentifierForPoint`. :param point: The :class:`BasePoint` subclass instance to which the identifier should be assigned. The value will have been normalized with :func:`normalizers.normalizePoint`. :return: A :class:`str` representing the newly assigned identifier. :raises NotImplementedError: If the method has not been overridden by a subclass. .. important:: Subclasses must override this method. """ self.raiseNotImplementedError() # ---- # Pens # ----
[docs] def draw(self, pen: PenType) -> None: """Draw the contour's outline data to the given pen. :param pen: The :class:`fontTools.pens.basePen.AbstractPen` to which the outline data should be drawn. Example:: >>> contour.draw(pen) """ self._draw(pen)
[docs] def _draw(self, pen: PenType, **kwargs: Any) -> None: r"""Draw the native contour's outline data to the given pen. This is the environment implementation of :meth:`BaseContour.draw`. :param pen: The :class:`fontTools.pens.basePen.AbstractPen` to which the outline data should be drawn. :param \**kwargs: Additional keyword arguments. .. note:: Subclasses may override this method. """ from fontTools.ufoLib.pointPen import PointToSegmentPen adapter = PointToSegmentPen(pen) self.drawPoints(adapter)
[docs] def drawPoints(self, pen: PointPenType) -> None: """Draw the contour's outline data to the given point pen. :param pen: The :class:`fontTools.pens.basePen.AbstractPointPen` to which the outline data should be drawn. Example:: >>> contour.drawPoints(pointPen) """ self._drawPoints(pen)
[docs] def _drawPoints(self, pen: PointPenType, **kwargs: Any) -> None: r"""Draw the native contour's outline data to the given point pen. This is the environment implementation of :meth:`BaseContour.drawPoints`. :param pen: The :class:`fontTools.pens.basePen.AbstractPointPen` to which the outline data should be drawn. :param \**kwargs: Additional keyword arguments. .. note:: Subclasses may override this method. """ # The try: ... except TypeError: ... # handles backwards compatibility with # point pens that have not been upgraded # to point pen protocol 2. try: pen.beginPath(self.identifier) except TypeError: pen.beginPath() for point in self.points: typ = point.type if typ == "offcurve": typ = None try: pen.addPoint( pt=(point.x, point.y), segmentType=typ, smooth=point.smooth, name=point.name, identifier=point.identifier, ) except TypeError: pen.addPoint( pt=(point.x, point.y), segmentType=typ, smooth=point.smooth, name=point.name, ) pen.endPath()
# ------------------ # Data normalization # ------------------
[docs] def autoStartSegment(self) -> None: """Automatically calculate and set the contour's first segment. The behavior of this may vary across environments. Example:: >>> contour.autoStartSegment() """ self._autoStartSegment()
[docs] def _autoStartSegment(self, **kwargs: Any) -> None: r"""Automatically calculate and set the native contour's first segment. This is the environment implementation of :meth:`BaseContour.autoStartSegment`. :param \**kwargs: Additional keyword arguments. :raises NotImplementedError: If the method has not been overridden by a subclass. .. important:: Subclasses must override this method. """ self.raiseNotImplementedError()
[docs] def round(self) -> None: """Round all point coordinates in the contour to the nearest integer. Example:: >>> contour.round() """ self._round()
[docs] def _round(self, **kwargs: Any) -> None: r"""Round all point coordinates in the native contour to the nearest integer. This is the environment implementation of :meth:`BaseContour.round`. :param \**kwargs: Additional keyword arguments. .. note:: Subclasses may override this method. """ for point in self.points: point.round()
# -------------- # Transformation # --------------
[docs] def _transformBy(self, matrix: AffineTransformationLike, **kwargs: Any) -> None: r"""Transform the contour according to the given matrix. This is the environment implementation of :meth:`BaseContour.transformBy`. :param matrix: The :ref:`type-transformation` to apply. The value will be normalized with :func:`normalizers.normalizeTransformationMatrix`. :param \**kwargs: Additional keyword arguments. .. note:: Subclasses may override this method. """ for point in self.points: point.transformBy(matrix)
# ------------- # Interpolation # ------------- compatibilityReporterClass = ContourCompatibilityReporter def isCompatible( self, other: BaseContour, cls=None ) -> tuple[bool, ContourCompatibilityReporter]: """Evaluate interpolation compatibility with another contour. :param other: The other :class:`BaseContour` 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.ContourCompatibilityReporter` instance. Example:: >>> compatible, report = self.isCompatible(otherContour) >>> compatible False >>> compatible [Fatal] Contour: [0] + [0] [Fatal] Contour: [0] contains 4 segments | [0] contains 3 segments [Fatal] Contour: [0] is closed | [0] is open """ return super().isCompatible(other, BaseContour) def _isCompatible( self, other: BaseContour, reporter: ContourCompatibilityReporter ) -> None: """Evaluate interpolation compatibility with another native contour. This is the environment implementation of :meth:`BaseContour.isCompatible`. :param other: The other :class:`BaseContour` instance to check compatibility with. :param reporter: An object used to report compatibility issues. .. note:: Subclasses may override this method. """ contour1 = self contour2 = other # open/closed if contour1.open != contour2.open: reporter.openDifference = True # direction if contour1.clockwise != contour2.clockwise: reporter.directionDifference = True # segment count if len(contour1) != len(contour2.segments): reporter.segmentCountDifference = True reporter.fatal = True # segment pairs for i in range(min(len(contour1), len(contour2))): segment1 = contour1[i] segment2 = contour2[i] segmentCompatibility = segment1.isCompatible(segment2)[1] if segmentCompatibility.fatal or segmentCompatibility.warning: if segmentCompatibility.fatal: reporter.fatal = True if segmentCompatibility.warning: reporter.warning = True reporter.segments.append(segmentCompatibility) # ---- # Open # ---- open: dynamicProperty = dynamicProperty( "base_open", """Determine whether the contour is open. This property is read-only. :return: :obj:`True` if the contour is open, otherwise :obj:`False`. Example:: >>> contour.open True """, ) def _get_base_open(self) -> bool: value = self._get_open() value = normalizers.normalizeBoolean(value) return value def _get_open(self) -> bool: """Determine whether the native contour is open. This is the environment implementation of the :attr:`BaseContour.open` property getter. :return: :obj:`True` if the contour is open, otherwise :obj:`False`. The value will have been normalized with :func:`normalizers.normalizeBoolean`. :raises NotImplementedError: If the method has not been overridden by a subclass. .. important:: Subclasses must override this method. """ self.raiseNotImplementedError() # --------- # Direction # --------- clockwise: dynamicProperty = dynamicProperty( "base_clockwise", """Specify or determine whether the contour's winding direction is clockwise. The value must be a :class:`bool` indicating the contour's winding direction. :return: :obj:`True` if the contour's winding direction is clockwise, otherwise :obj:`False`. """, ) def _get_base_clockwise(self) -> bool: value = self._get_clockwise() value = normalizers.normalizeBoolean(value) return value def _set_base_clockwise(self, value: bool) -> None: value = normalizers.normalizeBoolean(value) self._set_clockwise(value)
[docs] def _get_clockwise(self) -> bool: """Determine whether the native contour's winding direction is clockwise. This is the environment implementation of the :attr:`BaseContour.clockwise` property getter. :return: :obj:`True` if the contour's winding direction is clockwise, otherwise :obj:`False`. The value will have been normalized with :func:`normalizers.normalizeBoolean`. :raises NotImplementedError: If the method has not been overridden by a subclass. .. important:: Subclasses must override this method. """ self.raiseNotImplementedError()
[docs] def _set_clockwise(self, value: bool) -> None: """Specify whether the native contour's winding direction is clockwise. This is the environment implementation of the :attr:`BaseContour.clockwise` property setter. :param value: A :class:`bool` indicating the desired winding direction. :obj:`True` sets the direction to clockwise, and :obj:`False` to counter-clockwise. The value will have been normalized with :func:`normalizers.normalizeBoolean`. .. note:: Subclasses may override this method. """ if self.clockwise != value: self.reverse()
[docs] def reverse(self) -> None: """Reverse the direction of the contour. Example:: >>> contour.clockwise False >>> contour.reverse() >>> contour.clockwise True """ self._reverse()
[docs] def _reverse(self, **kwargs) -> None: r"""Reverse the direction of the contour. This is the environment implementation of :meth:`BaseContour.reverse`. :param \**kwargs: Additional keyword arguments. :raises NotImplementedError: If the method has not been overridden by a subclass. .. important:: Subclasses must override this method. """ self.raiseNotImplementedError()
# ------------------------ # Point and Contour Inside # ------------------------
[docs] def pointInside(self, point: CoordinateLike) -> bool: """Check if `point` is within the filled area of the contour. :param point: The point to check as a :ref:`type-coordinate`. :return: :obj:`True` if `point` is inside the filled area of the contour, :obj:`False` otherwise. Example:: >>> contour.pointInside((40, 65)) True """ point = normalizers.normalizeCoordinateTuple(point) return self._pointInside(point)
[docs] def _pointInside(self, point: CoordinateLike) -> bool: """Check if `point` is within the filled area of the native contour. This is the environment implementation of :meth:`BaseContour.pointInside`. :param point: The point to check as a :ref:`type-coordinate`. The value will have been normalized with :func:`normalizers.normalizeCoordinateTuple`. :return: :obj:`True` if `point` is inside the filled area of the contour, :obj:`False` otherwise. .. note:: Subclasses may override this method. """ from fontTools.pens.pointInsidePen import PointInsidePen pen = PointInsidePen(glyphSet=None, testPoint=point, evenOdd=False) self.draw(pen) return pen.getResult()
def contourInside(self, otherContour: BaseContour) -> bool: """Check if `otherContour` is within the current contour's filled area. :param point: The :class:`BaseContour` instance to check. :return: :obj:`True` if `otherContour` is inside the filled area of the current contour instance, :obj:`False` otherwise. Example:: >>> contour.contourInside(otherContour) True """ otherContour = normalizers.normalizeContour(otherContour) return self._contourInside(otherContour) def _contourInside(self, otherContour: BaseContour) -> bool: """Check if `otherContour` is within the current native contour's filled area. This is the environment implementation of :meth:`BaseContour.contourInside`. :param point: The :class:`BaseContour` instance to check. The value will have been normalized with :func:`normalizers.normalizeContour`. :return: :obj:`True` if `otherContour` is inside the filled area of the current contour instance, :obj:`False` otherwise. :raises NotImplementedError: If the method has not been overridden by a subclass. .. important:: Subclasses must override this method. """ self.raiseNotImplementedError() # --------------- # Bounds and Area # --------------- bounds: dynamicProperty = dynamicProperty( "bounds", """Get the bounds of the contour. This property is read-only. :return: A :class:`tuple` of four :class:`int` or :class:`float` values in the form ``(x minimum, y minimum, x maximum, y maximum)`` representing the bounds of the contour, or :obj:`None` if the contour is open. Example:: >>> contour.bounds (10, 30, 765, 643) """, ) def _get_base_bounds(self) -> BoundingBox | None: value = self._get_bounds() if value is not None: value = normalizers.normalizeBoundingBox(value) return value
[docs] def _get_bounds(self) -> BoundingBox | None: """Get the bounds of the contour. This is the environment implementation of the :attr:`BaseContour.bounds` property getter. :return: A :class:`tuple` of four :class:`int` or :class:`float` values in the form ``(x minimum, y minimum, x maximum, y maximum)`` representing the bounds of the contour, or :obj:`None` if the contour is open. .. note:: Subclasses may override this method. """ from fontTools.pens.boundsPen import BoundsPen pen = BoundsPen(self.layer) self.draw(pen) return pen.bounds
area: dynamicProperty = dynamicProperty( "area", """Get the area of the contour This property is read-only. :return: A positive :class:`int` or a :class:` float value representing the area of the contour, or :obj:`None` if the contour is open. Example:: >>> contour.area 583 """, ) def _get_base_area(self) -> float | None: value = self._get_area() if value is not None: value = normalizers.normalizeArea(value) return value def _get_area(self) -> float | None: """Get the area of the native contour This is the environment implementation of the :attr:`BaseContour.area` property getter. :return: A positive :class:`int` or a :class:` float value representing the area of the contour, or :obj:`None` if the contour is open. .. note:: Subclasses may override this method. """ from fontTools.pens.areaPen import AreaPen pen = AreaPen(self.layer) self.draw(pen) return abs(pen.value) # -------- # Segments # -------- # The base class implements the full segment interaction API. # Subclasses do not need to override anything within the contour # other than registering segmentClass. Subclasses may choose to # implement this API independently if desired. def _setContourInSegment(self, segment: BaseSegment) -> None: if segment.contour is None: segment.contour = self segments: dynamicProperty = dynamicProperty( "segments", """Get the contour's segments. This property is read-only. :return: A :class:`tuple` of :class:`BaseSegment` instances. Example:: >>> contour.segments (<BaseSegment curve index='0' at 4573388368>, ...) """, )
[docs] def _get_segments(self) -> tuple[BaseSegment, ...]: """Get the native contour's segments. This is the environment implementation of the :attr:`BaseContour.segments` property getter. :return: A :class:`tuple` of :class:`BaseSegment` subclass instances. .. note:: Subclasses may override this method. """ points = self.points if not points: return () segments: list[list[BasePoint]] = [[]] lastWasOffCurve = False firstIsMove = points[0].type == "move" for point in points: segments[-1].append(point) if point.type != "offcurve": segments.append([]) lastWasOffCurve = point.type == "offcurve" if len(segments[-1]) == 0: del segments[-1] if lastWasOffCurve and firstIsMove: # ignore trailing off curves del segments[-1] if lastWasOffCurve and not firstIsMove and len(segments) > 1: segment = segments.pop(-1) segment.extend(segments[0]) del segments[0] segments.append(segment) if not lastWasOffCurve and not firstIsMove: segment = segments.pop(0) segments.append(segment) # wrap into segments wrapped: list[BaseSegment] = [] for points in segments: if self.segmentClass is None: raise TypeError("segmentClass cannot be None.") pointSegment = self.segmentClass() pointSegment._setPoints(points) self._setContourInSegment(pointSegment) wrapped.append(pointSegment) return tuple(wrapped)
[docs] def __getitem__(self, index: int) -> BaseSegment: """Get the segment at the specified index. :param index: The zero-based index of the point to retrieve as an :class:`int`. :return: The :class:`BaseSegment` instance located at the specified `index`. :raises IndexError: If the specified `index` is out of range. """ return self.segments[index]
[docs] def __iter__(self) -> Iterator[BaseSegment]: """Return an iterator over the segments in the contour. :return: An iterator over the :class:`BaseSegment` instances belonging to the contour. """ return self._iterSegments()
def _iterSegments(self) -> Iterator[BaseSegment]: segments = self.segments count = len(segments) index = 0 while count: yield segments[index] count -= 1 index += 1
[docs] def __len__(self) -> int: """Return the number of segments in the contour. :return: An :class:`int` representing the number of :class:`BaseSegment` instances belonging to the contour. """ return self._len__segments()
[docs] def _len__segments(self, **kwargs: Any) -> int: r"""Return the number of segments in the native contour. This is the environment implementation of :meth:`BaseContour.__len__`. :return: An :class:`int` representing the number of :class:`BaseSegment` subclass instances belonging to the contour. :param \**kwargs: Additional keyword arguments. .. note:: Subclasses may override this method. """ return len(self.segments)
[docs] def appendSegment( self, type: str | None = None, points: PointCollectionType | None = None, smooth: bool = False, segment: BaseSegment | None = None, ) -> None: """Append the given segment to the contour. If `type` or `points` are specified, those values will be used instead of the values in the given `segment` object. The specified `smooth` state will be applied if ``segment=None``. :param type: An optional :attr:`BaseSegment.type` to be applied to the segment as a :class:`str`. Defaults to :obj:`None`. :param points: The optional :attr:`BaseSegment.points` to be applied to the segment as a :class:`list` or :class:`tuple` of :ref:`type-coordinate` items. Defaults to :obj:`None`. :param smooth: The :attr:`BaseSegment.smooth` state to be applied to the segment as a :class:`bool`. Defaults to :obj:`False`. :param segment: An optional :class:`BaseSegment` instance from which attribute values will be copied. Defualts to :obj:`None`. """ if segment is not None: if type is not None: type = segment.type if points is None: points = [(point.x, point.y) for point in segment.points] smooth = segment.smooth if type is None: raise TypeError("Type cannot be None.") type = normalizers.normalizeSegmentType(type) if points is not None: normalizedPoints = [normalizers.normalizeCoordinateTuple(p) for p in points] # Avoid mypy invariant List error. castPoints = cast(PointCollectionType, normalizedPoints) smooth = normalizers.normalizeBoolean(smooth) self._appendSegment(type=type, points=castPoints, smooth=smooth)
[docs] def _appendSegment( self, type: str, points: PointCollectionType, smooth: bool, **kwargs: Any ) -> None: r"""Append the given segment to the native contour. This is the environment implementation of :meth:`BaseContour.appendSegment`. :param type: The :attr:`BaseSegment.type` to be applied to the segment as a :class:`str`. The value will have been normalized with :func:`normalizers.normalizeSegmentType`. :param points: The :attr:`BaseSegment.points` to be applied to the segment as a :class:`list` or :class:`tuple` of :ref:`type-coordinate` items. The value will have been normalized with :func:`normalizers.normalizeCoordinateTuple`. :param smooth: The :attr:`BaseSegment.smooth` state to be applied to the segment as a :class:`bool`. The value will have been normalized with :func:`normalizers.normalizeBoolean`. :param \**kwargs: Additional keyword arguments. .. note:: Subclasses may override this method. """ self._insertSegment( len(self), type=type, points=points, smooth=smooth, **kwargs )
[docs] def insertSegment( self, index: int, type: str | None = None, points: PointCollectionType | None = None, smooth: bool = False, segment: BaseSegment | None = None, ) -> None: """Insert the given segment into the contour. If `type` or `points` are specified, those values will be used instead of the values in the given `segment` object. The specified `smooth` state will be applied if ``segment=None``. :param index: The :attr:`BaseSegment.index` to be applied to the segment as a :class:`int`. :param type: An optional :attr:`BaseSegment.type` to be applied to the segment as a :class:`str`. Defaults to :obj:`None`. :param points: The optional :attr:`BaseSegment.points` to be applied to the segment as a :class:`list` or :class:`tuple` of :ref:`type-coordinate` items. Defaults to :obj:`None`. :param smooth: The :attr:`BaseSegment.smooth` state to be applied to the segment as a :class:`bool`. Defaults to :obj:`False`. :param segment: An optional :class:`BaseSegment` instance from which attribute values will be copied. Defualts to :obj:`None`. """ if segment is not None: if type is not None: type = segment.type if points is None: points = [(point.x, point.y) for point in segment.points] smooth = segment.smooth normalizedIndex = normalizers.normalizeIndex(index) if normalizedIndex is None: raise TypeError("Index cannot be None.") if type is None: raise TypeError("Type cannot be None.") type = normalizers.normalizeSegmentType(type) if points is not None: normalizedPoints = [normalizers.normalizeCoordinateTuple(p) for p in points] # Avoid mypy invariant List error. castPoints = cast(PointCollectionType, normalizedPoints) smooth = normalizers.normalizeBoolean(smooth) self._insertSegment(index=index, type=type, points=castPoints, smooth=smooth)
[docs] def _insertSegment( self, index: int, type: str, points: PointCollectionType, smooth: bool, **kwargs: Any, ) -> None: r"""Insert the given segment into the native contour. This is the environment implementation of :meth:`BaseContour.insertSegment`. :param index: The :attr:`BaseSegment.index` to be applied to the segment as a :class:`int`. The value will have been normalized with :func:`normalizers.normalizeIndex`. :param type: The :attr:`BaseSegment.type` to be applied to the segment as a :class:`str`. The value will have been normalized with :func:`normalizers.normalizeSegmentType`. :param points: The :attr:`BaseSegment.points` to be applied to the segment as a :class:`list` or :class:`tuple` of :ref:`type-coordinate` items. The value will have been normalized with :func:`normalizers.normalizeCoordinateTuple`. :param smooth: The :attr:`BaseSegment.smooth` state to be applied to the segment as a :class:`bool`. The value will have been normalized with :func:`normalizers.normalizeBoolean`. :param \**kwargs: Additional keyword arguments. .. note:: Subclasses may override this method. """ onCurve = points[-1] offCurve = points[:-1] segments = self.segments addPointCount = 1 if self.open: index += 1 addPointCount = 0 ptCount = sum([len(segments[s].points) for s in range(index)]) + addPointCount self.insertPoint(ptCount, onCurve, type=type, smooth=smooth) for offCurvePoint in reversed(offCurve): self.insertPoint(ptCount, offCurvePoint, type="offcurve")
[docs] def removeSegment( self, segment: BaseSegment | int, preserveCurve: bool = False ) -> None: """Remove the given segment from the contour. If ``preserveCurve=True``, an attempt will be made to preserve the overall shape of the curve after the segment is removed, provided the environment supports such functionality. :param segment: The segment to remove as a :class:`BaseSegment` instance, or an :class:`int` representing the segment's index. :param preserveCurve: A :class:`bool` indicating whether to preserve the curve's shape after the segment is removed. Defaults to :obj:`False`. :raises ValueError: If the segment index is out of range or if the specified segment is not part of the contour. Example:: >>> contour.removeSegment(mySegment) >>> contour.removeSegment(2, preserveCurve=True) """ if not isinstance(segment, int): index = self.segments.index(segment) normalizedIndex = normalizers.normalizeIndex(index) if normalizedIndex is None: return if normalizedIndex >= self._len__segments(): raise ValueError(f"No segment located at index {normalizedIndex}.") preserveCurve = normalizers.normalizeBoolean(preserveCurve) self._removeSegment(normalizedIndex, preserveCurve)
[docs] def _removeSegment(self, index: int, preserveCurve: bool, **kwargs: Any) -> None: r"""Remove the given segment from the native contour. This is the environment implementation of :meth:`BaseContour.removeSegment`. :param index: The segment to remove as an :class:`int` representing the segment's index. The value will have been normalized with :func:`normalizers.normalizeIndex`. :param preserveCurve: A :class:`bool` indicating whether to preserve the curve's shape after the segment is removed. Defaults to :obj:`False`. The value will have been normalized with :func:`normalizers.normalizeBoolean`. :param \**kwargs: Additional keyword arguments. .. note:: Subclasses may override this method. """ segment = self.segments[index] for point in segment.points: self.removePoint(point, preserveCurve)
[docs] def setStartSegment(self, segment: BaseSegment | int) -> None: """Set the first segment in the contour. :param segment: The segment to set as the first instance in the contour as a :class:`BaseSegment` instance, or an :class:`int` representing the segment's index. :raises FontPartsError: If the contour is open. :raises ValueError: If the segment index is out of range or if the specified segment is not part of the contour. Example:: >>> contour.setStartSegment(mySegment) >>> contour.setStartSegment(2) """ if self.open: raise FontPartsError("An open contour can not change the starting segment.") segments = self.segments if not isinstance(segment, int): segmentIndex = segments.index(segment) else: segmentIndex = segment if len(self.segments) < 2: return if segmentIndex == 0: return if segmentIndex >= len(segments): raise ValueError( f"The contour does not contain a segment at index {segmentIndex}" ) self._setStartSegment(segmentIndex)
[docs] def _setStartSegment(self, segmentIndex: int, **kwargs: Any) -> None: r"""Set the first segment in the native contour. This is the environment implementation of :meth:`BaseContour.setStartSegment`. :param segmentIndex: An :class:`int` representing the index of the segment to be set as the first instance in the contour. :param \**kwargs: Additional keyword arguments. .. note:: Subclasses may override this method. """ # get the previous segment and set # its on curve as the first point # in the contour. this matches the # iteration behavior of self.segments. segmentIndex -= 1 segments = self.segments segment = segments[segmentIndex] self.setStartPoint(segment.points[-1])
# ------- # bPoints # ------- bPoints: dynamicProperty = dynamicProperty( "bPoints", """Get a list of all bPoints in the contour. This property is read-only. :return: A :class:`tuple` of :class`BaseBPoints`. """, ) def _get_bPoints(self) -> tuple[BaseBPoint, ...]: bPoints: list[BaseBPoint] = [] for point in self.points: if point.type not in ("move", "line", "curve"): continue if self.bPointClass is None: raise TypeError("bPointClass cannot be None.") bPoint = self.bPointClass() bPoint.contour = self bPoint._setPoint(point) bPoints.append(bPoint) return tuple(bPoints)
[docs] def appendBPoint( self, type: str | None = None, anchor: CoordinateLike | None = None, bcpIn: CoordinateLike | None = None, bcpOut: CoordinateLike | None = None, bPoint: BaseBPoint | None = None, ) -> None: """Append the given bPoint to the contour. If `type`, `anchor`, `bcpIn` or `bcpOut` are specified, those values will be used instead of the values in the given `segment` object. :param type: An optional :attr:`BaseBPoint.type` to be applied to the bPoint as a :class:`str`. Defaults to :obj:`None`. :param anchor: An optional :attr:`BaseBPoint.anchor` to be applied to the bPoint as a :ref:`type-coordinate`. Defaults to :obj:`None`. :param bcpIn: An optional :attr:`BaseBPoint.bcpIn` to be applied to the bPoint as a :ref:`type-coordinate`. Defaults to :obj:`None`. :param bcpOut: An optional :attr:`BaseBPoint.bcpOut` to be applied to the bPoint as a :ref:`type-coordinate`. Defaults to :obj:`None`. :param bPoint: An optional :class:`BaseBPoint` instance from which attribute values will be copied. Defualts to :obj:`None`. """ if bPoint is not None: if type is None: type = bPoint.type if anchor is None: anchor = bPoint.anchor if bcpIn is None: bcpIn = bPoint.bcpIn if bcpOut is None: bcpOut = bPoint.bcpOut if type is None: raise TypeError("Type cannot be None.") type = normalizers.normalizeBPointType(type) if anchor is None: raise TypeError("Anchor cannot be None.") anchor = normalizers.normalizeCoordinateTuple(anchor) if bcpIn is None: bcpIn = (0, 0) bcpIn = normalizers.normalizeCoordinateTuple(bcpIn) if bcpOut is None: bcpOut = (0, 0) bcpOut = normalizers.normalizeCoordinateTuple(bcpOut) self._appendBPoint(type, anchor, bcpIn=bcpIn, bcpOut=bcpOut)
[docs] def _appendBPoint( self, type: str, anchor: CoordinateLike, bcpIn: CoordinateLike, bcpOut: CoordinateLike, **kwargs: Any, ) -> None: r"""Append the given bPoint to the native contour. This is the environment implementation of :meth:`BaseContour.appendBPoint`. :param type: The :attr:`BaseBPoint.type` to be applied to the bPoint as a :class:`str`. The value will have been normalized with :func:`normalizers.normalizeBPointType`. :param anchor: The :attr:`BaseBPoint.anchor` to be applied to the bPoint as a :ref:`type-coordinate`. The value will have been normalized with :func:`normalizers.normalizeCoordinateTuple`. :param bcpIn: The :attr:`BaseBPoint.bcpIn` to be applied to the bPoint as a :ref:`type-coordinate`. The value will have been normalized with :func:`normalizers.normalizeCoordinateTuple`. :param bcpOut: An optional :attr:`BaseBPoint.bcpOut` to be applied to the bPoint as a :ref:`type-coordinate`. The value will have been normalized with :func:`normalizers.normalizeCoordinateTuple`. :param \**kwargs: Additional keyword arguments. .. note:: Subclasses may override this method. """ self.insertBPoint(len(self.bPoints), type, anchor, bcpIn=bcpIn, bcpOut=bcpOut)
[docs] def insertBPoint( self, index: int, type: str | None = None, anchor: CoordinateLike | None = None, bcpIn: CoordinateLike | None = None, bcpOut: CoordinateLike | None = None, bPoint: BaseBPoint | None = None, ) -> None: """Insert the given bPoint into the contour. If `type`, `anchor`, `bcpIn` or `bcpOut` are specified, those values will be used instead of the values in the given `segment` object. :param index: The :attr:`BaseBPoint.index` to be applied to the bPoint as an :class:`int`. :param type: An optional :attr:`BaseBPoint.type` to be applied to the bPoint as a :class:`str`. Defaults to :obj:`None`. :param anchor: An optional :attr:`BaseBPoint.anchor` to be applied to the bPoint as a :ref:`type-coordinate`. Defaults to :obj:`None`. :param bcpIn: An optional :attr:`BaseBPoint.bcpIn` to be applied to the bPoint as a :ref:`type-coordinate`. Defaults to :obj:`None`. :param bcpOut: An optional :attr:`BaseBPoint.bcpOut` to be applied to the bPoint as a :ref:`type-coordinate`. Defaults to :obj:`None`. :param bPoint: An optional :class:`BaseBPoint` instance from which attribute values will be copied. Defualts to :obj:`None`. """ if bPoint is not None: if type is None: type = bPoint.type if anchor is None: anchor = bPoint.anchor if bcpIn is None: bcpIn = bPoint.bcpIn if bcpOut is None: bcpOut = bPoint.bcpOut normalizedIndex = normalizers.normalizeIndex(index) if normalizedIndex is None: raise TypeError("Index cannot be None.") if type is None: raise TypeError("Type cannot be None.") type = normalizers.normalizeBPointType(type) if anchor is None: raise TypeError("Anchor cannot be None.") anchor = normalizers.normalizeCoordinateTuple(anchor) if bcpIn is None: bcpIn = (0, 0) bcpIn = normalizers.normalizeCoordinateTuple(bcpIn) if bcpOut is None: bcpOut = (0, 0) bcpOut = normalizers.normalizeCoordinateTuple(bcpOut) self._insertBPoint( index=normalizedIndex, type=type, anchor=anchor, bcpIn=bcpIn, bcpOut=bcpOut )
[docs] def _insertBPoint( self, index: int, type: str, anchor: CoordinateLike, bcpIn: CoordinateLike, bcpOut: CoordinateLike, **kwargs: Any, ) -> None: r"""Insert the given bPoint into the native contour. This is the environment implementation of :meth:`BaseContour.insertBPoint`. :param index: The :attr:`BaseBPoint.index` to be applied to the bPoint as an :class:`int`. The value will have been normalized with :func:`normalizers.normalizeIndex`. :param type: An optional :attr:`BaseBPoint.type` to be applied to the bPoint as a :class:`str`. The value will have been normalized with :func:`normalizers.normalizeBPointType`. :param anchor: The :attr:`BaseBPoint.anchor` to be applied to the bPoint as a :ref:`type-coordinate`. The value will have been normalized with :func:`normalizers.normalizeCoordinateTuple`. :param bcpIn: The :attr:`BaseBPoint.bcpIn` to be applied to the bPoint as a :ref:`type-coordinate`. The value will have been normalized with :func:`normalizers.normalizeCoordinateTuple`. :param bcpOut: An optional :attr:`BaseBPoint.bcpOut` to be applied to the bPoint as a :ref:`type-coordinate`. The value will have been normalized with :func:`normalizers.normalizeCoordinateTuple`. :param \**kwargs: Additional keyword arguments. .. note:: Subclasses may override this method. """ # insert a simple line segment at the given anchor # look it up as a bPoint and change the bcpIn and bcpOut there # this avoids code duplication self._insertSegment(index=index, type="line", points=[anchor], smooth=False) bPoints = self.bPoints index += 1 if index >= len(bPoints): # its an append instead of an insert # so take the last bPoint index = -1 bPoint = bPoints[index] bPoint.bcpIn = bcpIn bPoint.bcpOut = bcpOut bPoint.type = type
def removeBPoint(self, bPoint: BaseBPoint | int) -> None: """Remove the given bPoint from the contour. :param bPoint: The bPoint to remove as a :class:`BaseBPoint` instance, or an :class:`int` representing the bPoint's index. :raises ValueError: If the bPoint index is out of range or if the specified bPoint is not part of the contour. Example:: >>> contour.removeBPoint(myBPoint) >>> contour.removeBPoint(2) """ index = bPoint.index if not isinstance(bPoint, int) else bPoint normalizedIndex = normalizers.normalizeIndex(index) # Avoid mypy conflict with normalizeIndex -> Optional[int] if normalizedIndex is None: return if normalizedIndex >= self._len__points(): raise ValueError(f"No bPoint located at index {normalizedIndex}.") self._removeBPoint(normalizedIndex) def _removeBPoint(self, index: int, **kwargs: Any) -> None: r"""Remove the given bPoint from the native contour. This is the environment implementation of :meth:`BaseContour.removeBPoint`. :param index: The index representing the :class:`BaseBPoint` subclass instance to remove as an :class:`int`. The value will have been normalized with :func:`normalizers.normalizeIndex`. :param \**kwargs: Additional keyword arguments. .. note:: Subclasses may override this method. """ bPoint = self.bPoints[index] nextSegment = bPoint._nextSegment offCurves = nextSegment.offCurve if offCurves: offCurve = offCurves[0] self.removePoint(offCurve) segment = bPoint._segment offCurves = segment.offCurve if offCurves: offCurve = offCurves[-1] self.removePoint(offCurve) self.removePoint(bPoint._point) # ------ # Points # ------ def _setContourInPoint(self, point: BasePoint) -> None: if point.contour is None: point.contour = self points: dynamicProperty = dynamicProperty( "points", """Get a list of all points in the contour. This property is read-only. :return: A :class:`tuple` of :class`BasePoints`. """, )
[docs] def _get_points(self) -> tuple[BasePoint, ...]: """Get a list of all points in the native contour. This is the environment implementation of the :attr:`BaseContour.points` property getter. :return: A :class:`tuple` of :class`BasePoint` subclass instances. .. note:: Subclasses may override this method. """ return tuple(self._getitem__points(i) for i in range(self._len__points()))
def _len__points(self) -> int: return self._lenPoints()
[docs] def _lenPoints(self, **kwargs: Any) -> int: r"""Return the number of points in the native contour. :param \**kwargs: Additional keyword arguments. :return: An :class:`int` representing the number of :class:`BasePoint` subclass instances belonging to the contour. .. important:: Subclasses must override this method. """ self.raiseNotImplementedError()
def _getitem__points(self, index: int) -> BasePoint: normalizedIndex = normalizers.normalizeIndex(index) if normalizedIndex is None or normalizedIndex >= self._len__points(): raise ValueError(f"No point located at index {normalizedIndex}.") point = self._getPoint(normalizedIndex) self._setContourInPoint(point) return point
[docs] def _getPoint(self, index: int, **kwargs: Any) -> BasePoint: r"""Get the given point from the native contour. :param index: The index representing the :class:`BaseBPoint` subclass instance to retrieve as an :class:`int`. The value will have been normalized with :func:`normalizers.normalizeIndex`. :param \**kwargs: Additional keyword arguments. :return: A :class:`BasePoint` subclass instance. .. important:: Subclasses must override this method. """ self.raiseNotImplementedError()
def _getPointIndex(self, point: BasePoint) -> int: for i, other in enumerate(self.points): if point == other: return i raise FontPartsError("The point could not be found.")
[docs] def appendPoint( self, position: CoordinateLike | None = None, type: str = "line", smooth: bool = False, name: str | None = None, identifier: str | None = None, point: BasePoint | None = None, ) -> None: """Append the given point to the contour. If `position`, `type` or `name` are specified, those values will be used instead of the values in the given `segment` object. The specified `smooth` state will be applied if ``point=None``. :param position: An optional position to be applied to the point as a :ref:`type-coordinate`. Defaults to :obj:`None`. :param type: An optional :attr:`BasePoint.type` to be applied to the point as a :class:`str`. Defaults to ``'line'``. :param smooth: The :attr:`BasePoint.smooth` state to be applied to the point as a :class:`bool`. Defaults to :obj:`False`. :param name: An optional :attr:`BasePoint.name` to be applied to the point as a :class:`str`. Defaults to :obj:`None`. :param identifier: An optional :attr:`BasePoint.identifier` to be applied to the point as a :class:`str`. Defaults to :obj:`None`. :param point: An optional :class:`BasePoint` instance from which attribute values will be copied. Defualts to :obj:`None`. """ if point is not None: if position is None: position = point.position type = point.type smooth = point.smooth if name is None: name = point.name if identifier is not None: identifier = point.identifier self.insertPoint( len(self.points), position=position, type=type, smooth=smooth, name=name, identifier=identifier, )
[docs] def insertPoint( self, index: int, position: CoordinateLike | None = None, type: str = "line", smooth: bool = False, name: str | None = None, identifier: str | None = None, point: BasePoint | None = None, ) -> None: """Insert the given point into the contour. If `position`, `type` or `name` are specified, those values will be used instead of the values in the given `segment` object. The specified `smooth` state will be applied if ``point=None``. :param index: The :attr:`BasePoint.index` to be applied to the point as an :class:`int`. :param position: An optional position to be applied to the point as a :ref:`type-coordinate`. Defaults to :obj:`None`. :param type: An optional :attr:`BasePoint.type` to be applied to the point as a :class:`str`. Defaults to ``'line'``. :param smooth: The :attr:`BasePoint.smooth` state to be applied to the point as a :class:`bool`. Defaults to :obj:`False`. :param name: An optional :attr:`BasePoint.name` to be applied to the point as a :class:`str`. Defaults to :obj:`None`. :param identifier: An optional :attr:`BasePoint.identifier` to be applied to the point as a :class:`str`. Defaults to :obj:`None`. :param point: An optional :class:`BasePoint` instance from which attribute values will be copied. Defualts to :obj:`None`. """ if point is not None: if position is None: position = point.position type = point.type smooth = point.smooth if name is None: name = point.name if identifier is not None: identifier = point.identifier normalizedIndex = normalizers.normalizeIndex(index) if normalizedIndex is None: raise TypeError("Index cannot be None.") if position is None: raise TypeError("Position cannot be None.") position = normalizers.normalizeCoordinateTuple(position) type = normalizers.normalizePointType(type) smooth = normalizers.normalizeBoolean(smooth) if name is not None: name = normalizers.normalizePointName(name) if identifier is not None: identifier = normalizers.normalizeIdentifier(identifier) self._insertPoint( normalizedIndex, position=position, type=type, smooth=smooth, name=name, identifier=identifier, )
[docs] def _insertPoint( self, index: int, position: CoordinateLike, type: str, smooth: bool, name: str | None, identifier: str | None, **kwargs: Any, ) -> None: r"""Insert the given point into the native contour. This is the environment implementation of :meth:`BaseContour.insertPoint`. :param index: The :attr:`BasePoint.index` to be applied to the point as an :class:`int`. The value will have been normalized with :func:`normalizers.normalizeIndex`. :param position: The position to be applied to the point as a :ref:`type-coordinate`. The value will have been normalized with :func:`normalizers.normalizeCoordinateTuple`. :param type: The :attr:`BasePoint.type` to be applied to the point as a :class:`str`. The value will have been normalized with :func:`normalizers.normalizePointType`. :param smooth: The :attr:`BasePoint.smooth` state to be applied to the point as a :class:`bool`. The value will have been normalized with :func:`normalizers.normalizeBoolean`. :param name: An optional :attr:`BasePoint.name` to be applied to the point as a :class:`str`. The value will have been normalized with :func:`normalizers.normalizePointName` :param identifier: An optional :attr:`BasePoint.identifier` to be applied to the point as a :class:`str`. The value will have been normalized with :func:`normalizers.normalizeIdentifier`, but will not have been tested for uniqueness. :param \**kwargs: Additional keyword arguments. :raises NotImplementedError: If the method has not been overridden by a subclass. .. important:: Subclasses must override this method. """ self.raiseNotImplementedError()
[docs] def removePoint(self, point: BasePoint | int, preserveCurve: bool = False) -> None: """Remove the given point from the contour. If ``preserveCurve=True``, an attempt will be made to preserve the overall shape of the curve after the segment is removed, provided the environment supports such functionality. :param point: The point to remove as a :class:`BasePoint` instance, or an :class:`int` representing the point's index. :param preserveCurve: A :class:`bool` indicating whether to preserve the curve's shape after the point is removed. Defaults to :obj:`False`. :raises ValueError: If the point index is out of range or if the specified point is not part of the contour. Example:: >>> contour.removePoint(myPoint) >>> contour.removePoint(2, preserveCurve=True) """ index = self.points.index(point) if not isinstance(point, int) else point normalizedIndex = normalizers.normalizeIndex(index) # Avoid mypy conflict with normalizeIndex -> Optional[int] if normalizedIndex is None: return if normalizedIndex >= self._len__points(): raise ValueError(f"No point located at index {normalizedIndex}.") preserveCurve = normalizers.normalizeBoolean(preserveCurve) self._removePoint(normalizedIndex, preserveCurve)
[docs] def _removePoint(self, index: int, preserveCurve: bool, **kwargs: Any) -> None: r"""Remove the given point from the native contour. This is the environment implementation of :meth:`BaseContour.removePoint`. :param index: The index representing the :class:`BasePoint` subclass instance to remove as an :class:`int`. The value will have been normalized with :func:`normalizers.normalizeIndex`. :param preserveCurve: A :class:`bool` indicating whether to preserve the curve's shape after the point is removed. The value will have been normalized with :func:`normalizers.normalizeBoolean`. :param \**kwargs: Additional keyword arguments. :raises NotImplementedError: If the method has not been overridden by a subclass. .. important:: Subclasses must override this method. """ self.raiseNotImplementedError()
def setStartPoint(self, point: BasePoint | int) -> None: """Set the first segment in the contour. :param segment: The point to set as the first instance in the contour as a :class:`BasePoint` instance, or an :class:`int` representing the point's index. :raises FontPartsError: If the contour is open. :raises ValueError: If the point index is out of range or if the specified point is not part of the contour. Example:: >>> contour.setStartPoint(myPoint) >>> contour.setStartPoint(2) """ if self.open: raise FontPartsError("An open contour can not change the starting point.") points = self.points if not isinstance(point, int): pointIndex = points.index(point) else: pointIndex = point if pointIndex == 0: return if pointIndex >= len(points): raise ValueError( f"The contour does not contain a point at index {pointIndex}" ) self._setStartPoint(pointIndex) def _setStartPoint(self, pointIndex: int, **kwargs: Any) -> None: r"""Set the first segment in the native contour. This is the environment implementation of :meth:`BaseContour.setStartPoint`. :param pointIndex: An :class:`int` representing the index of the point to be set as the first instance in the contour. :param \**kwargs: Additional keyword arguments. .. note:: Subclasses may override this method. """ points = self.points points = points[pointIndex:] + points[:pointIndex] # Clear the points. for point in self.points: self.removePoint(point) # Add the points. for point in points: self.appendPoint( (point.x, point.y), type=point.type, smooth=point.smooth, name=point.name, identifier=point.identifier, ) # --------- # Selection # --------- # segments selectedSegments: dynamicProperty = dynamicProperty( "base_selectedSegments", """Get or set the selected segments in the contour. The value must be a :class:`tuple` or :class:`list` of either :class:`BaseSegment` instances or :class:`int` values representing segment indexes to select. :return: A :class:`tuple` of the currently selected :class:`BaseSegment` instances. Getting selected segments:: >>> for segment in contour.selectedSegments: ... segment.move((10, 20)) Setting selected segments:: >>> contour.selectedSegments = someSegments Setting selection using indexes:: >>> contour.selectedSegments = [0, 2] """, ) def _get_base_selectedSegments(self) -> tuple[BaseSegment, ...]: selected = tuple( normalizers.normalizeSegment(segment) for segment in self._get_selectedSegments() ) return selected def _get_selectedSegments(self) -> tuple[BaseSegment, ...]: """Get the selected segments in the native contour. This is the environment implementation of the :attr:`BaseContour.selectedSegments` property getter. :return: A :class:`tuple` of the currently selected :class:`BaseSegment` instances. Each value item will be normalized with :func:`normalizers.normalizeSegment`. .. note:: Subclasses may override this method. """ return self._getSelectedSubObjects(self.segments) def _set_base_selectedSegments( self, value: CollectionType[BaseSegment | int] ) -> None: normalized = [] for segment in value: normalizedSegment: BaseSegment | int if isinstance(segment, int): normalizedIndex = normalizers.normalizeIndex(segment) # Avoid mypy conflict with normalizeIndex -> Optional[int] if normalizedIndex is None: continue normalizedSegment = normalizedIndex else: normalizedSegment = normalizers.normalizeSegment(segment) normalized.append(normalizedSegment) self._set_selectedSegments(normalized) def _set_selectedSegments(self, value: CollectionType[BaseSegment | int]) -> None: """Set the selected segments in the native contour. This is the environment implementation of the :attr:`BaseContour.selectedSegments` property setter. :param value: The segments to select as a :class:`tuple` or :class:`list` of either :class:`BaseContour` instances or :class:`int` values representing segment indexes. Each value item will have been normalized with :func:`normalizers.normalizeSegment` or :func:`normalizers.normalizeIndex`. .. note:: Subclasses may override this method. """ return self._setSelectedSubObjects(self.segments, value) # points selectedPoints: dynamicProperty = dynamicProperty( "base_selectedPoints", """Get or set the selected points in the contour. The value must be a :class:`tuple` or :class:`list` of either :class:`BasePoint` instances or :class:`int` values representing point indexes to select. :return: A :class:`tuple` of the currently selected :class:`BasePoint` instances. Getting selected points:: >>> for point in contour.selectedPoints: ... point.move((10, 20)) Setting selected points:: >>> contour.selectedPoints = somePoints Setting selection using indexes:: >>> contour.selectedPoints = [0, 2] """, ) def _get_base_selectedPoints(self) -> tuple[BasePoint, ...]: selected = tuple( normalizers.normalizePoint(point) for point in self._get_selectedPoints() ) return selected def _get_selectedPoints(self) -> tuple[BasePoint, ...]: """Get the selected points in the native contour. This is the environment implementation of the :attr:`BaseContour.selectedPoints` property getter. :return: A :class:`tuple` of the currently selected :class:`BasePoint` instances. Each value item will be normalized with :func:`normalizers.normalizePoint`. .. note:: Subclasses may override this method. """ return self._getSelectedSubObjects(self.points) def _set_base_selectedPoints(self, value: CollectionType[BasePoint | int]) -> None: normalized = [] for point in value: normalizedPoint: BasePoint | int if isinstance(point, int): normalizedIndex = normalizers.normalizeIndex(point) # Avoid mypy conflict with normalizeIndex -> Optional[int] if normalizedIndex is None: continue normalizedPoint = normalizedIndex else: normalizedPoint = normalizers.normalizePoint(point) normalized.append(normalizedPoint) self._set_selectedPoints(normalized) def _set_selectedPoints(self, value: CollectionType[BasePoint | int]) -> None: """Set the selected points in the native contour. This is the environment implementation of the :attr:`BaseContour.selectedPoints` property setter. :param value: The points to select as a :class:`tuple` or :class:`list` of either :class:`BasePoint` instances or :class:`int` values representing point indexes to select. Each value item will have been normalized with :func:`normalizers.normalizePoint` or :func:`normalizers.normalizeIndex`. .. note:: Subclasses may override this method. """ return self._setSelectedSubObjects(self.points, value) # bPoints selectedBPoints: dynamicProperty = dynamicProperty( "base_selectedBPoints", """Get or set the selected bPoints in the contour. The value must be a :class:`tuple` or :class:`list` of either :class:`BaseBPoint` instances or :class:`int` values representing bPoint indexes to select. :return: A :class:`tuple` of the currently selected :class:`BaseBPoint` instances. Getting selected bPoints:: >>> for bPoint in contour.selectedBPoints: ... bPoint.move((10, 20)) Setting selected bPoints:: >>> contour.selectedBPoints = someBPoints Setting selection using indexes:: >>> contour.selectedBPoints = [0, 2] """, ) def _get_base_selectedBPoints(self) -> tuple[BaseBPoint, ...]: selected = tuple( normalizers.normalizeBPoint(bPoint) for bPoint in self._get_selectedBPoints() ) return selected def _get_selectedBPoints(self) -> tuple[BaseBPoint, ...]: """Get the selected bPoints in the native contour. This is the environment implementation of the :attr:`BaseContour.selectedBPoints` property getter. :return: A :class:`tuple` of the currently selected :class:`BaseBPoint` instances. Each value item will be normalized with :func:`normalizers.normalizeBPoint`. .. note:: Subclasses may override this method. """ return self._getSelectedSubObjects(self.bPoints) def _set_base_selectedBPoints( self, value: CollectionType[BaseBPoint | int] ) -> None: normalized = [] for bPoint in value: normalizedBPoint: BaseBPoint | int if isinstance(bPoint, int): normalizedIndex = normalizers.normalizeIndex(bPoint) # Avoid mypy conflict with normalizeIndex -> Optional[int] if normalizedIndex is None: continue normalizedBPoint = normalizedIndex else: normalizedBPoint = normalizers.normalizeBPoint(bPoint) normalized.append(normalizedBPoint) self._set_selectedBPoints(normalized) def _set_selectedBPoints(self, value: CollectionType[BaseBPoint | int]) -> None: """Set the selected bPoints in the native contour. This is the environment implementation of the :attr:`BaseContour.selectedBPoints` property setter. :param value: The bPoints to select as a :class:`tuple` or :class:`list` of either :class:`BaseBPoint` instances or :class:`int` values representing bPoint indexes to select. Each value item will have been normalized with :func:`normalizers.normalizeBPoint` or :func:`normalizers.normalizeIndex`. .. note:: Subclasses may override this method. """ return self._setSelectedSubObjects(self.bPoints, value)