Skip to content

Commit a704452

Browse files
pep 257 on utils.py
1 parent 5614335 commit a704452

File tree

1 file changed

+80
-42
lines changed

1 file changed

+80
-42
lines changed

schemascii/utils.py

Lines changed: 80 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -27,39 +27,49 @@
2727

2828

2929
class Cbox(typing.NamedTuple):
30+
"""Component bounding box. Also holds the letter
31+
and number of the reference designator.
32+
"""
33+
# XXX is this still used?
3034
p1: complex
3135
p2: complex
3236
type: str
3337
id: str
3438

3539

3640
class BOMData(typing.NamedTuple):
41+
"""Data to link the BOM data entry with the reference designator."""
3742
type: str
3843
id: str
3944
data: str
4045

4146

4247
class Flag(typing.NamedTuple):
48+
"""Data indicating the non-wire character next to a component."""
4349
pt: complex
4450
char: str
4551
side: Side
4652

4753

4854
class Terminal(typing.NamedTuple):
55+
"""Data indicating what and where wires connect to the component."""
4956
pt: complex
5057
flag: str | None
5158
side: Side
5259

5360

5461
class Side(enum.Enum):
55-
"Which edge the flag was found on."
62+
"""One of the four cardinal directions."""
5663
RIGHT = 0
5764
TOP = -pi / 2
5865
LEFT = pi
5966
BOTTOM = pi / 2
6067

6168
@classmethod
6269
def from_phase(cls, pt: complex) -> Side:
70+
"""Return the side that is closest to pt, if it is interpreted as
71+
a vector originating from the origin.
72+
"""
6373
ops = {
6474
-pi: Side.LEFT,
6575
pi: Side.LEFT,
@@ -85,11 +95,12 @@ def flood_walk(
8595
directions: defaultdict[str, defaultdict[
8696
complex, list[complex] | None]],
8797
seen: set[complex]) -> list[complex]:
88-
"""Flood-fills the area on the grid starting from seed, only following
89-
connections in the directions allowed by start_dirs and directions.
98+
"""Flood-fill the area on the grid starting from seed, only following
99+
connections in the directions allowed by start_dirs and directions, and
100+
return the list of reached points.
90101
91-
Updates the set seen for points that were walked into
92-
and returns the list of walked-into points."""
102+
Also updates the set seen for points that were walked into.
103+
"""
93104
points: list[complex] = []
94105
stack: list[tuple[complex, list[complex]]] = [
95106
(p, start_dirs[grid.get(p)])
@@ -108,14 +119,16 @@ def flood_walk(
108119
next_pt = point + dir
109120
next_dirs = directions[grid.get(next_pt)]
110121
if next_dirs is None:
122+
# shortcut
111123
next_dirs = defaultdict(lambda: None)
112124
stack.append((next_pt, next_dirs[dir]))
113125
return points
114126

115127

116128
def perimeter(pts: list[complex]) -> list[complex]:
117-
"""The set of points that are on the boundary of
118-
the grid-aligned set pts."""
129+
"""Return the set of points that are on the boundary of
130+
the grid-aligned set pts.
131+
"""
119132
out = []
120133
for pt in pts:
121134
for d in ORTHAGONAL + DIAGONAL:
@@ -133,29 +146,32 @@ def centroid(pts: list[complex]) -> complex:
133146

134147
def sort_counterclockwise(pts: list[complex],
135148
center: complex | None = None) -> list[complex]:
136-
"""Returns pts sorted so that the points
149+
"""Return pts sorted so that the points
137150
progress clockwise around the center, starting with the
138-
rightmost point."""
151+
rightmost point.
152+
"""
139153
if center is None:
140154
center = centroid(pts)
141155
return sorted(pts, key=lambda p: phase(p - center))
142156

143157

144158
def colinear(*points: complex) -> bool:
145-
"Returns true if all the points are in the same line."
159+
"""Return true if all the points are in the same line."""
146160
return len(set(phase(p - points[0]) for p in points[1:])) == 1
147161

148162

149163
def force_int(p: complex) -> complex:
150-
"Force the coordinates of the complex number to lie on the integer grid."
164+
"""Return p with the coordinates rounded to lie on the integer grid."""
151165
return complex(round(p.real), round(p.imag))
152166

153167

154168
def sharpness_score(points: list[complex]) -> float:
155-
"""Returns a number indicating how twisty the line is -- higher means
156-
the corners are sharper."""
169+
"""Return a number indicating how twisty the line is -- higher means
170+
the corners are sharper. The result is 0 if the line is degenerate or
171+
has no corners.
172+
"""
157173
if len(points) < 3:
158-
return float("nan")
174+
return 0
159175
score = 0
160176
prev_pt = points[1]
161177
prev_ph = phase(points[1] - points[0])
@@ -167,8 +183,12 @@ def sharpness_score(points: list[complex]) -> float:
167183
return score
168184

169185

170-
def intersecting(a: complex, b: complex, p: complex, q: complex):
171-
"""Return true if colinear line segments AB and PQ intersect."""
186+
def intersecting(a: complex, b: complex, p: complex, q: complex) -> bool:
187+
"""Return true if colinear line segments AB and PQ intersect.
188+
189+
If the line segments are not colinear, the result is undefined and
190+
unpredictable.
191+
"""
172192
a, b, p, q = a.real, b.real, p.real, q.real
173193
sort_a, sort_b = min(a, b), max(a, b)
174194
sort_p, sort_q = min(p, q), max(p, q)
@@ -177,8 +197,9 @@ def intersecting(a: complex, b: complex, p: complex, q: complex):
177197

178198
def take_next_group(links: list[tuple[complex, complex]]) -> list[
179199
tuple[complex, complex]]:
180-
"""Pops the longest possible path off of the `links` list and returns it,
181-
mutating the input list."""
200+
"""Pop the longest possible continuous path off of the `links` list and
201+
return it, mutating the input list.
202+
"""
182203
best = [links.pop()]
183204
while True:
184205
for pair in links:
@@ -200,9 +221,11 @@ def take_next_group(links: list[tuple[complex, complex]]) -> list[
200221

201222

202223
def merge_colinear(links: list[tuple[complex, complex]]):
203-
"Merges line segments that are colinear. Mutates the input list."
224+
"""Merge adjacent line segments that are colinear, mutating the input
225+
list.
226+
"""
204227
i = 1
205-
while True:
228+
while links:
206229
if i >= len(links):
207230
break
208231
elif links[i][0] == links[i][1]:
@@ -216,9 +239,13 @@ def merge_colinear(links: list[tuple[complex, complex]]):
216239

217240

218241
def iterate_line(p1: complex, p2: complex, step: float = 1.0):
219-
"Yields complex points along a line."
220-
# this isn't Bresenham's algorithm but I only use it for vertical or
221-
# horizontal lines, so it works well enough
242+
"""Yield complex points along a line. Like range() but for complex
243+
numbers.
244+
245+
This isn't Bresenham's algorithm but I only use it for perfectly vertical
246+
or perfectly horizontal lines, so it works well enough. If the line is
247+
diagonal then weird stuff happens.
248+
"""
222249
vec = p2 - p1
223250
point = p1
224251
while abs(vec) > abs(point - p1):
@@ -229,8 +256,11 @@ def iterate_line(p1: complex, p2: complex, step: float = 1.0):
229256

230257
def deep_transform(data, origin: complex, theta: float):
231258
"""Transform the point or points first by translating by origin,
232-
then rotating by theta. Returns an identical data structure,
233-
but with the transformed points substituted."""
259+
then rotating by theta. Return an identical data structure,
260+
but with the transformed points substituted.
261+
262+
TODO: add type statements for the data argument. This is really weird.
263+
"""
234264
if isinstance(data, list | tuple):
235265
return [deep_transform(d, origin, theta) for d in data]
236266
if isinstance(data, complex):
@@ -246,7 +276,9 @@ def deep_transform(data, origin: complex, theta: float):
246276

247277
def fix_number(n: float) -> str:
248278
"""If n is an integer, remove the trailing ".0".
249-
Otherwise round it to 2 digits."""
279+
Otherwise round it to 2 digits, and return the stringified
280+
number.
281+
"""
250282
if n.is_integer():
251283
return str(int(n))
252284
n = round(n, 2)
@@ -282,8 +314,9 @@ def mk_tag(*contents: str, **attrs: str) -> str:
282314

283315

284316
def points2path(points: list[complex], close: bool = False) -> str:
285-
"""Converts list of points into SVG <path> commands
286-
to draw the set of lines."""
317+
"""Convert the list of points into SVG <path> commands
318+
to draw the set of lines.
319+
"""
287320
def fix(number: float) -> float | int:
288321
return int(number) if number.is_integer() else number
289322

@@ -318,7 +351,8 @@ def polylinegon(
318351
"""Turn the list of points into a line or filled area.
319352
320353
If is_polygon is true, stroke color is used as fill color instead
321-
and stroke width is ignored."""
354+
and stroke width is ignored.
355+
"""
322356
scaled_pts = [x * scale for x in points]
323357
if is_polygon:
324358
return XML.path(d=points2path(scaled_pts, True),
@@ -329,7 +363,7 @@ def polylinegon(
329363

330364

331365
def find_dots(points: list[tuple[complex, complex]]) -> list[complex]:
332-
"Finds all the points where there are 4 or more connecting wires."
366+
"""Find all the points where there are 4 or more connecting wires."""
333367
seen = {}
334368
for p1, p2 in points:
335369
if p1 == p2:
@@ -349,8 +383,9 @@ def find_dots(points: list[tuple[complex, complex]]) -> list[complex]:
349383
def bunch_o_lines(
350384
pairs: list[tuple[complex, complex]],
351385
*, scale: int, stroke_width: int, stroke: str) -> str:
352-
"""Collapse the pairs of points and return
353-
the smallest number of <polyline>s."""
386+
"""Combine the pairs (p1, p2) into a set of SVG <path> commands
387+
to draw all of the lines.
388+
"""
354389
lines = []
355390
while pairs:
356391
group = take_next_group(pairs)
@@ -378,7 +413,7 @@ def id_text(
378413
scale: int,
379414
stroke: str
380415
) -> str:
381-
"Format the component ID and value around the point."
416+
"""Format the component ID and value around the point."""
382417
if nolabels:
383418
return ""
384419
if point is None:
@@ -426,7 +461,7 @@ def id_text(
426461

427462
def make_text_point(t1: complex, t2: complex,
428463
*, scale: int, offset_scale: int = 1) -> complex:
429-
"Compute the scaled coordinates of the text anchor point."
464+
"""Compute the scaled coordinates of the text anchor point."""
430465
quad_angle = phase(t1 - t2) + pi / 2
431466
text_pt = (t1 + t2) * scale / 2
432467
offset = rect(scale / 2 * offset_scale, quad_angle)
@@ -436,7 +471,7 @@ def make_text_point(t1: complex, t2: complex,
436471

437472
def make_plus(terminals: list[Terminal], center: complex,
438473
theta: float, **options) -> str:
439-
"Make a + sign if the terminals indicate the component is polarized."
474+
"""Make a + sign if the terminals indicate the component is polarized."""
440475
if all(t.flag != "+" for t in terminals):
441476
return ""
442477
return XML.g(
@@ -453,7 +488,7 @@ def make_plus(terminals: list[Terminal], center: complex,
453488

454489

455490
def arrow_points(p1: complex, p2: complex) -> list[tuple[complex, complex]]:
456-
"Return points to make an arrow from p1 pointing to p2."
491+
"""Return points to make an arrow from p1 pointing to p2."""
457492
angle = phase(p2 - p1)
458493
tick_len = min(0.5, abs(p2 - p1))
459494
return [
@@ -465,7 +500,7 @@ def arrow_points(p1: complex, p2: complex) -> list[tuple[complex, complex]]:
465500

466501
def make_variable(center: complex, theta: float,
467502
is_variable: bool = True, **options) -> str:
468-
"Draw a 'variable' arrow across the component."
503+
"""Draw a 'variable' arrow across the component."""
469504
if not is_variable:
470505
return ""
471506
return bunch_o_lines(deep_transform(arrow_points(-1, 1),
@@ -476,7 +511,8 @@ def make_variable(center: complex, theta: float,
476511

477512
def light_arrows(center: complex, theta: float, out: bool, **options):
478513
"""Draw arrows towards or away from the component
479-
(i.e. light-emitting or light-dependent)."""
514+
(i.e. light-emitting or light-dependent).
515+
"""
480516
a, b = 1j, 0.3 + 0.3j
481517
if out:
482518
a, b = b, a
@@ -491,7 +527,7 @@ def light_arrows(center: complex, theta: float, out: bool, **options):
491527

492528
def sort_terminals_counterclockwise(
493529
terminals: list[Terminal]) -> list[Terminal]:
494-
"Sort the terminals in counterclockwise order."
530+
"""Sort the terminals in counterclockwise order."""
495531
partitioned = {
496532
side: list(filtered_terminals)
497533
for side, filtered_terminals in itertools.groupby(
@@ -508,7 +544,7 @@ def sort_terminals_counterclockwise(
508544

509545

510546
def is_clockwise(terminals: list[Terminal]) -> bool:
511-
"Return true if the terminals are clockwise order."
547+
"""Return true if the terminals are clockwise order."""
512548
sort = sort_terminals_counterclockwise(terminals)
513549
for _ in range(len(sort)):
514550
if sort == terminals:
@@ -520,7 +556,8 @@ def is_clockwise(terminals: list[Terminal]) -> bool:
520556
def sort_for_flags(terminals: list[Terminal],
521557
box: Cbox, *flags: list[str]) -> list[Terminal]:
522558
"""Sorts out the terminals in the specified order using the flags.
523-
Raises and error if the flags are absent."""
559+
Raises an error if the flags are absent.
560+
"""
524561
out = ()
525562
for flag in flags:
526563
matching_terminals = list(filter(lambda t: t.flag == flag, terminals))
@@ -536,7 +573,8 @@ def sort_for_flags(terminals: list[Terminal],
536573
)
537574
(terminal,) = matching_terminals
538575
out = *out, terminal
539-
terminals.remove(terminal)
576+
# terminals.remove(terminal)
577+
# is this necessary with the checks above?
540578
return out
541579

542580

0 commit comments

Comments
 (0)