-
Notifications
You must be signed in to change notification settings - Fork 13
/
contour.py
184 lines (143 loc) · 5.55 KB
/
contour.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
from __future__ import annotations
import warnings
from collections.abc import MutableSequence
from typing import TYPE_CHECKING, Iterable, Iterator, List, Optional, overload
from attrs import define, field
from fontTools.pens.basePen import AbstractPen
from fontTools.pens.pointPen import AbstractPointPen, PointToSegmentPen
from ufoLib2.objects.misc import BoundingBox, getBounds, getControlBounds
from ufoLib2.objects.point import Point
from ufoLib2.serde import serde
from ufoLib2.typing import GlyphSet
# For Python 3.7 compatibility.
if TYPE_CHECKING:
ContourMapping = MutableSequence[Point]
else:
ContourMapping = MutableSequence
@serde
@define
class Contour(ContourMapping):
"""Represents a contour as a list of points.
Behavior:
The Contour object has list-like behavior. This behavior allows you to interact
with point data directly. For example, to get a particular point::
point = contour[0]
To iterate over all points::
for point in contour:
...
To get the number of points::
pointCount = len(contour)
To delete a particular point::
del contour[0]
To set a particular point to another Point object::
contour[0] = anotherPoint
"""
points: List[Point] = field(factory=list)
"""The list of points in the contour."""
identifier: Optional[str] = field(default=None, repr=False)
"""The globally unique identifier of the contour."""
# collections.abc.MutableSequence interface
def __delitem__(self, index: int | slice) -> None:
del self.points[index]
@overload
def __getitem__(self, index: int) -> Point:
...
@overload
def __getitem__(self, index: slice) -> list[Point]: # noqa: F811
...
def __getitem__(self, index: int | slice) -> Point | list[Point]: # noqa: F811
return self.points[index]
def __setitem__( # noqa: F811
self, index: int | slice, point: Point | Iterable[Point]
) -> None:
if isinstance(index, int) and isinstance(point, Point):
self.points[index] = point
elif (
isinstance(index, slice)
and isinstance(point, Iterable)
and all(isinstance(p, Point) for p in point)
):
self.points[index] = point
else:
raise TypeError(
f"Expected Point or Iterable[Point], found {type(point).__name__}."
)
def __iter__(self) -> Iterator[Point]:
return iter(self.points)
def __len__(self) -> int:
return len(self.points)
def insert(self, index: int, value: Point) -> None:
"""Insert Point object ``value`` into the contour at ``index``."""
if not isinstance(value, Point):
raise TypeError(f"Expected Point, found {type(value).__name__}.")
self.points.insert(index, value)
# TODO: rotate method?
@property
def open(self) -> bool:
"""Returns whether the contour is open or closed."""
if not self.points:
return True
return self.points[0].type == "move"
def move(self, delta: tuple[float, float]) -> None:
"""Moves contour by (x, y) font units."""
for point in self.points:
point.move(delta)
def getBounds(self, layer: GlyphSet | None = None) -> BoundingBox | None:
"""Returns the (xMin, yMin, xMax, yMax) bounding box of the glyph,
taking the actual contours into account.
Args:
layer: Not applicable to contours, here for API symmetry.
"""
return getBounds(self, layer)
@property
def bounds(self) -> BoundingBox | None:
"""Returns the (xMin, yMin, xMax, yMax) bounding box of the glyph,
taking the actual contours into account.
|defcon_compat|
"""
return self.getBounds()
def getControlBounds(self, layer: GlyphSet | None = None) -> BoundingBox | None:
"""Returns the (xMin, yMin, xMax, yMax) bounding box of the glyph,
taking only the control points into account.
Args:
layer: Not applicable to contours, here for API symmetry.
"""
return getControlBounds(self, layer)
@property
def controlPointBounds(self) -> BoundingBox | None:
"""Returns the (xMin, yMin, xMax, yMax) bounding box of the glyph,
taking only the control points into account.
|defcon_compat|
"""
return self.getControlBounds()
# -----------
# Pen methods
# -----------
def draw(self, pen: AbstractPen) -> None:
"""Draws contour into given pen."""
pointPen = PointToSegmentPen(pen)
self.drawPoints(pointPen)
def drawPoints(self, pointPen: AbstractPointPen) -> None:
"""Draws points of contour into given point pen."""
try:
pointPen.beginPath(identifier=self.identifier)
for p in self.points:
pointPen.addPoint(
(p.x, p.y),
segmentType=p.type,
smooth=p.smooth,
name=p.name,
identifier=p.identifier,
)
except TypeError:
pointPen.beginPath()
for p in self.points:
pointPen.addPoint(
(p.x, p.y), segmentType=p.type, smooth=p.smooth, name=p.name
)
warnings.warn(
"The pointPen needs an identifier kwarg. "
"Identifiers have been discarded.",
UserWarning,
)
pointPen.endPath()