from __future__ import annotations
from typing import TYPE_CHECKING, Any, List, Optional, Union
from collections.abc import Callable
from fontTools.misc import transform
from fontParts.base.base import (
BaseObject,
TransformationMixin,
SelectionMixin,
IdentifierMixin,
dynamicProperty,
reference,
)
from fontParts.base.errors import FontPartsError
from fontParts.base import normalizers
from fontParts.base.deprecated import DeprecatedBPoint, RemovedBPoint
from fontParts.base.annotations import (
AffineTransformationLike,
Coordinate,
CoordinateLike,
IntFloatType,
)
if TYPE_CHECKING:
from fontParts.base.contour import BaseContour
from fontParts.base.font import BaseFont
from fontParts.base.glyph import BaseGlyph
from fontParts.base.layer import BaseLayer
from fontParts.base.point import BasePoint
from fontParts.base.segment import BaseSegment
[docs]
class BaseBPoint(
BaseObject,
TransformationMixin,
SelectionMixin,
DeprecatedBPoint,
IdentifierMixin,
RemovedBPoint,
):
"""Represent the basis for a bPoint object."""
def _reprContents(self) -> list[str]:
contents = [f"{self.type}", f"anchor='({self.anchor[0]}, {self.anchor[1]})'"]
return contents
def _setPoint(self, point: BasePoint) -> None:
if hasattr(self, "_point"):
raise AssertionError("point for bPoint already set")
self._point = point
def __eq__(self, other):
if hasattr(other, "_point"):
return self._point == other._point
return NotImplemented
# this class should not be used in hashable
# collections since it is dynamically generated.
__hash__ = None # type: ignore[assignment]
# -------
# Parents
# -------
# identifier
def _get_identifier(self) -> str | None:
"""Get the native bPoint's unique identifier.
This is the environment implementation of :attr:`BaseBPoint.identifier`.
If the native object does not have an identifier assigned, one may be
assigned with :meth:`BaseBPoint.getIdentifier`
:return: The unique identifier assigned to the object as a :class:`str`,
or :obj:`None` indicating the object has no identifier.
.. note::
Subclasses may override this method.
"""
return self._point.identifier
def _getIdentifier(self) -> str:
"""Generate and assign a unique identifier to the native bPoint.
This is the environment implementation of :meth:`BaseBPoint.getIdentifier`.
:return: A unique object identifier as a :class:`str`.
.. note::
Subclasses may override this method.
"""
return self._point.getIdentifier()
# Segment
_segment: dynamicProperty = dynamicProperty("base_segment")
def _get_base_segment(self) -> BaseSegment | None:
point = self._point
for segment in self.contour.segments:
if segment.onCurve == point:
return segment
return None
_nextSegment: dynamicProperty = dynamicProperty("base_nextSegment")
def _get_base_nextSegment(self) -> BaseSegment | None:
contour = self.contour
if contour is None:
return None
segments = contour.segments
segment = self._segment
i = segments.index(segment) + 1
if i >= len(segments):
i = i % len(segments)
nextSegment = segments[i]
return nextSegment
# Contour
_contour: Callable[[], BaseContour] | None = None
contour = dynamicProperty(
"contour",
"""Get or set the bPoint's parent contour object.
The value must be a :class:`BaseContour` instance or :obj:`None`.
:return: The :class:`BaseContour` instance containing the bPoint
or :obj:`None`.
:raises AssertionError: If attempting to set the contour when it
has already been set.
Example::
>>> contour = bPoint.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 bPoint already set")
if contour is not None:
contour = reference(contour)
self._contour = contour
# Glyph
glyph: dynamicProperty = dynamicProperty(
"glyph",
"""Get the bPoint'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 bPoint
or :obj:`None`.
Example::
>>> glyph = bPoint.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 bPoint's parent layer object.
This property is read-only.
:return: The :class:`BaseLayer` instance containing the bPoint
or :obj:`None`.
Example::
>>> layer = bPoint.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 bPoint's parent font object.
This property is read-only.
:return: The :class:`BaseFont` instance containing the bPoint
or :obj:`None`.
Example::
>>> font = bPoint.font
""",
)
def _get_font(self) -> BaseFont | None:
if self._contour is None:
return None
return self.glyph.font
# ----------
# Attributes
# ----------
# anchor
anchor: dynamicProperty = dynamicProperty(
"base_anchor",
"""Get or set the bPoint's anchor point.
The value must be a :ref:`type-coordinate`.
:return: a :ref:`type-coordinate` representing the anchor point of the bPoint.
""",
)
def _get_base_anchor(self) -> Coordinate:
value = self._get_anchor()
value = normalizers.normalizeCoordinateTuple(value)
return value
def _set_base_anchor(self, value: CoordinateLike) -> None:
value = normalizers.normalizeCoordinateTuple(value)
self._set_anchor(value)
[docs]
def _get_anchor(self) -> Coordinate:
"""Get the bPoint's anchor point.
This is the environment implementation of the :attr:`BaseBPoint.anchor`
property getter.
:return: a :ref:`type-coordinate` representing the anchor point of the
bPoint. The value will be normalized
with :func:`normalizers.normalizeCoordinateTuple`.
.. note::
Subclasses may override this method.
"""
point = self._point
return (point.x, point.y)
[docs]
def _set_anchor(self, value: CoordinateLike) -> None:
"""Set the bPoint's anchor point.
This is the environment implementation of the :attr:`BaseBPoint.anchor`
property setter.
:param value: The anchor point to set as a :ref:`type-coordinate`.
The value will have been normalized
with :func:`normalizers.normalizeCoordinateTuple`.
.. note::
Subclasses may override this method.
"""
pX, pY = self.anchor
x, y = value
dX = x - pX
dY = y - pY
self.moveBy((dX, dY))
# bcp in
bcpIn: dynamicProperty = dynamicProperty(
"base_bcpIn",
"""Get or set the bPoint's incoming off-curve.
The value must be a :ref:`type-coordinate`.
:return: A :ref:`type-coordinate` representing the incoming
off-curve of the bPoin.
""",
)
def _get_base_bcpIn(self) -> Coordinate:
value = self._get_bcpIn()
value = normalizers.normalizeCoordinateTuple(value)
return value
def _set_base_bcpIn(self, value: CoordinateLike) -> None:
value = normalizers.normalizeCoordinateTuple(value)
self._set_bcpIn(value)
[docs]
def _get_bcpIn(self) -> Coordinate:
"""Get the bPoint's incoming off-curve.
This is the environment implementation of the :attr:`BaseBPoint.bcpIn`
property getter.
:return: A :ref:`type-coordinate` representing the incoming off-curve of
the bPoin. The value will be normalized
with :func:`normalizers.normalizeCoordinateTuple`.
.. note::
Subclasses may override this method.
"""
segment = self._segment
offCurves = segment.offCurve
if offCurves:
bcp = offCurves[-1]
x, y = relativeBCPIn(self.anchor, (bcp.x, bcp.y))
else:
x = y = 0
return (x, y)
[docs]
def _set_bcpIn(self, value: CoordinateLike) -> None:
"""Set the bPoint's incoming off-curve.
This is the environment implementation of the :attr:`BaseBPoint.bcpIn`
property setter.
:param value: The incoming off-curve to set as
a :ref:`type-coordinate`. The value will have been normalized
with :func:`normalizers.normalizeCoordinateTuple`.
:raises FontPartsError: When attempting to set the incoming off-curve
for a the first point in an open contour.
.. note::
Subclasses may override this method.
"""
x, y = absoluteBCPIn(self.anchor, value)
segment = self._segment
if segment.type == "move" and value != (0, 0):
raise FontPartsError(
"Cannot set the bcpIn for the first point in an open contour."
)
offCurves = segment.offCurve
if offCurves:
# if the two off-curves are located at the anchor
# coordinates we can switch to a line segment type.
if value == (0, 0) and self.bcpOut == (0, 0):
segment.type = "line"
segment.smooth = False
else:
offCurves[-1].x = x
offCurves[-1].y = y
elif value != (0, 0):
segment.type = "curve"
offCurves = segment.offCurve
offCurves[-1].x = x
offCurves[-1].y = y
# bcp out
bcpOut: dynamicProperty = dynamicProperty(
"base_bcpOut",
"""Get or set the bPoint's outgoing off-curve.
The value must be a :ref:`type-coordinate`.
:return: A :ref:`type-coordinate` representing the outgoing
off-curve of the bPoin.
""",
)
def _get_base_bcpOut(self) -> Coordinate:
value = self._get_bcpOut()
value = normalizers.normalizeCoordinateTuple(value)
return value
def _set_base_bcpOut(self, value: CoordinateLike) -> None:
value = normalizers.normalizeCoordinateTuple(value)
self._set_bcpOut(value)
[docs]
def _get_bcpOut(self) -> Coordinate:
"""Get the bPoint's outgoing off-curve.
This is the environment implementation of the :attr:`BaseBPoint.bcpOut`
property getter.
:return: A :ref:`type-coordinate` representing the outgoing
off-curve of the bPoin. The value will be normalized
with :func:`normalizers.normalizeCoordinateTuple`.
.. note::
Subclasses may override this method.
"""
nextSegment = self._nextSegment
offCurves = nextSegment.offCurve
if offCurves:
bcp = offCurves[0]
x, y = relativeBCPOut(self.anchor, (bcp.x, bcp.y))
else:
x = y = 0
return (x, y)
[docs]
def _set_bcpOut(self, value: CoordinateLike) -> None:
"""Set the bPoint's outgoing off-curve.
This is the environment implementation of the :attr:`BaseBPoint.bcpOut`
property setter.
:param value: The outgoing off-curve to set as
a :ref:`type-coordinate`. The value will have been normalized
with :func:`normalizers.normalizeCoordinateTuple`.
:raises FontPartsError: When attempting to set the outgoing off-curve
for a the last point in an open contour.
.. note::
Subclasses may override this method.
"""
x, y = absoluteBCPOut(self.anchor, value)
segment = self._segment
nextSegment = self._nextSegment
if nextSegment.type == "move" and value != (0, 0):
raise FontPartsError(
"Cannot set the bcpOut for the last point in an open contour."
)
else:
offCurves = nextSegment.offCurve
if offCurves:
# if the off-curves are located at the anchor coordinates
# we can switch to a "line" segment type
if value == (0, 0) and self.bcpIn == (0, 0):
segment.type = "line"
segment.smooth = False
else:
offCurves[0].x = x
offCurves[0].y = y
elif value != (0, 0):
nextSegment.type = "curve"
offCurves = nextSegment.offCurve
offCurves[0].x = x
offCurves[0].y = y
# type
type: dynamicProperty = dynamicProperty(
"base_type",
"""Get or set the bPoint's type.
The value must be a :class:`str` containing one of the following
alternatives:
+--------------+-----------------------------------------------------------+
| Type | Description |
+--------------+-----------------------------------------------------------+
| ``'curve'`` | A point where bcpIn and bcpOut are smooth (linked). |
| ``'corner'`` | A point where bcpIn and bcpOut are not smooth (unlinked). |
+--------+-----------------------------------------------------------------+
:return: A :class:`str` representing the type of the bPoint.
""",
)
def _get_base_type(self) -> str:
value = self._get_type()
value = normalizers.normalizeBPointType(value)
return value
def _set_base_type(self, value: str) -> None:
value = normalizers.normalizeBPointType(value)
self._set_type(value)
[docs]
def _get_type(self) -> str:
"""Get the bPoint's type.
This is the environment implementation of the :attr:`BaseBPoint.type`
property getter.
:return: A :class:`str` representing the type of the bPoint. The value
will be normalized with :func:`normalizers.normalizeBPointType`.
:raises FontPartsError: If the point's type cannot be converted to a valid
bPoint type.
.. note::
Subclasses may override this method.
"""
point = self._point
typ = point.type
bType = None
if point.smooth:
if typ == "curve":
bType = "curve"
elif typ == "line" or typ == "move":
nextSegment = self._nextSegment
if nextSegment is not None and nextSegment.type == "curve":
bType = "curve"
else:
bType = "corner"
elif typ in ("move", "line", "curve"):
bType = "corner"
if bType is None:
raise FontPartsError(f"A {typ} point can not be converted to a bPoint.")
return bType
[docs]
def _set_type(self, value: str) -> None:
"""Set the bPoint's type.
This is the environment implementation of the :attr:`BaseBPoint.type`
property setter.
:param value: The outgoing off-curve to set as a :class:`str`. The value
will have been normalized with :func:`normalizers.normalizeBPointType`.
.. note::
Subclasses may override this method.
"""
point = self._point
# convert corner to curve
if value == "curve" and point.type == "line":
# This needs to insert off-curves without
# generating unnecessary points in the
# following segment. The segment object
# implements this logic, so delegate the
# change to the corresponding segment.
segment = self._segment
segment.type = "curve"
segment.smooth = True
# convert curve to corner
elif value == "corner" and point.type == "curve":
point.smooth = False
# --------------
# Identification
# --------------
index: dynamicProperty = dynamicProperty(
"base_index",
"""Get the index of the bPoint.
This property is read-only.
:return: An :class:`int` representing the bPoint's index within an
ordered list of the parent contour's bPoints, or :obj:`None` if the
bPoint does not belong to a contour.
Example::
>>> bPoint.index
0
""",
)
def _get_base_index(self) -> int | None:
if self.contour is None:
return None
value = self._get_index()
value = normalizers.normalizeIndex(value)
return value
[docs]
def _get_index(self) -> int | None:
"""Get the index of the native bPoint.
This is the environment implementation of the :attr:`BaseBPoint.index`
property getter.
:return: An :class:`int` representing the bPoint's index within an
ordered list of the parent contour's bPoints, or :obj:`None` if the
bPoint does not belong to a contour. The value will be
normalized with :func:`normalizers.normalizeIndex`.
.. note::
Subclasses may override this method.
"""
contour = self.contour
value = contour.bPoints.index(self)
return value
# --------------
# Transformation
# --------------
# ----
# Misc
# ----
[docs]
def round(self) -> None:
"""Round the bPoint's coordinates.
This applies to:
- :attr:`anchor`
- :attr:`bcpIn`
- :attr:`bcpOut`
Example::
>>> bPoint.round()
"""
x, y = self.anchor
self.anchor = (
normalizers.normalizeVisualRounding(x),
normalizers.normalizeVisualRounding(y),
)
x, y = self.bcpIn
self.bcpIn = (
normalizers.normalizeVisualRounding(x),
normalizers.normalizeVisualRounding(y),
)
x, y = self.bcpOut
self.bcpOut = (
normalizers.normalizeVisualRounding(x),
normalizers.normalizeVisualRounding(y),
)
def relativeBCPIn(anchor: CoordinateLike, BCPIn: CoordinateLike) -> Coordinate:
"""convert absolute incoming bcp value to a relative value.
:param anchor: The anchor reference point from which to measure the relative
BCP value as a :ref:`type-coordinate`.
:param BCPIn: The absolute incoming BCP value to be converted as
a :ref:`type-coordinate`.
:return: The relative position of the incoming BCP as a :ref:`type-coordinate`.
"""
return (BCPIn[0] - anchor[0], BCPIn[1] - anchor[1])
def absoluteBCPIn(anchor: CoordinateLike, BCPIn: CoordinateLike) -> Coordinate:
"""convert relative incoming bcp value to an absolute value.
:param anchor: The anchor reference point from which the relative BCP value
is measured as a :ref:`type-coordinate`.
:param BCPIn: The relative incoming BCP value to be converted as
a :ref:`type-coordinate`.
:return: The absolute position of the incoming BCP as a :ref:`type-coordinate`.
"""
return (BCPIn[0] + anchor[0], BCPIn[1] + anchor[1])
def relativeBCPOut(anchor: CoordinateLike, BCPOut: CoordinateLike) -> Coordinate:
"""convert absolute outgoing bcp value to a relative value.
:param anchor: The anchor reference point from which to measure the relative
BCP value as a :ref:`type-coordinate`.
:param BCPOut: The absolute outgoing BCP value to be converted as
a :ref:`type-coordinate`.
:return: The relative position of the outgoing BCP as a :ref:`type-coordinate`.
"""
return (BCPOut[0] - anchor[0], BCPOut[1] - anchor[1])
def absoluteBCPOut(anchor: CoordinateLike, BCPOut: CoordinateLike) -> Coordinate:
"""convert relative outgoing bcp value to an absolute value.
:param anchor: The anchor reference point from which the relative BCP value
is measured as a :ref:`type-coordinate`.
:param BCPOut: The relative outgoing BCP value to be converted as
a :ref:`type-coordinate`.
:return: The absolute position of the outgoing BCP as a :ref:`type-coordinate`.
"""
return (BCPOut[0] + anchor[0], BCPOut[1] + anchor[1])