27
27
28
28
29
29
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?
30
34
p1 : complex
31
35
p2 : complex
32
36
type : str
33
37
id : str
34
38
35
39
36
40
class BOMData (typing .NamedTuple ):
41
+ """Data to link the BOM data entry with the reference designator."""
37
42
type : str
38
43
id : str
39
44
data : str
40
45
41
46
42
47
class Flag (typing .NamedTuple ):
48
+ """Data indicating the non-wire character next to a component."""
43
49
pt : complex
44
50
char : str
45
51
side : Side
46
52
47
53
48
54
class Terminal (typing .NamedTuple ):
55
+ """Data indicating what and where wires connect to the component."""
49
56
pt : complex
50
57
flag : str | None
51
58
side : Side
52
59
53
60
54
61
class Side (enum .Enum ):
55
- "Which edge the flag was found on. "
62
+ """One of the four cardinal directions."" "
56
63
RIGHT = 0
57
64
TOP = - pi / 2
58
65
LEFT = pi
59
66
BOTTOM = pi / 2
60
67
61
68
@classmethod
62
69
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
+ """
63
73
ops = {
64
74
- pi : Side .LEFT ,
65
75
pi : Side .LEFT ,
@@ -85,11 +95,12 @@ def flood_walk(
85
95
directions : defaultdict [str , defaultdict [
86
96
complex , list [complex ] | None ]],
87
97
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.
90
101
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
+ """
93
104
points : list [complex ] = []
94
105
stack : list [tuple [complex , list [complex ]]] = [
95
106
(p , start_dirs [grid .get (p )])
@@ -108,14 +119,16 @@ def flood_walk(
108
119
next_pt = point + dir
109
120
next_dirs = directions [grid .get (next_pt )]
110
121
if next_dirs is None :
122
+ # shortcut
111
123
next_dirs = defaultdict (lambda : None )
112
124
stack .append ((next_pt , next_dirs [dir ]))
113
125
return points
114
126
115
127
116
128
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
+ """
119
132
out = []
120
133
for pt in pts :
121
134
for d in ORTHAGONAL + DIAGONAL :
@@ -133,29 +146,32 @@ def centroid(pts: list[complex]) -> complex:
133
146
134
147
def sort_counterclockwise (pts : list [complex ],
135
148
center : complex | None = None ) -> list [complex ]:
136
- """Returns pts sorted so that the points
149
+ """Return pts sorted so that the points
137
150
progress clockwise around the center, starting with the
138
- rightmost point."""
151
+ rightmost point.
152
+ """
139
153
if center is None :
140
154
center = centroid (pts )
141
155
return sorted (pts , key = lambda p : phase (p - center ))
142
156
143
157
144
158
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."" "
146
160
return len (set (phase (p - points [0 ]) for p in points [1 :])) == 1
147
161
148
162
149
163
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."" "
151
165
return complex (round (p .real ), round (p .imag ))
152
166
153
167
154
168
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
+ """
157
173
if len (points ) < 3 :
158
- return float ( "nan" )
174
+ return 0
159
175
score = 0
160
176
prev_pt = points [1 ]
161
177
prev_ph = phase (points [1 ] - points [0 ])
@@ -167,8 +183,12 @@ def sharpness_score(points: list[complex]) -> float:
167
183
return score
168
184
169
185
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
+ """
172
192
a , b , p , q = a .real , b .real , p .real , q .real
173
193
sort_a , sort_b = min (a , b ), max (a , b )
174
194
sort_p , sort_q = min (p , q ), max (p , q )
@@ -177,8 +197,9 @@ def intersecting(a: complex, b: complex, p: complex, q: complex):
177
197
178
198
def take_next_group (links : list [tuple [complex , complex ]]) -> list [
179
199
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
+ """
182
203
best = [links .pop ()]
183
204
while True :
184
205
for pair in links :
@@ -200,9 +221,11 @@ def take_next_group(links: list[tuple[complex, complex]]) -> list[
200
221
201
222
202
223
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
+ """
204
227
i = 1
205
- while True :
228
+ while links :
206
229
if i >= len (links ):
207
230
break
208
231
elif links [i ][0 ] == links [i ][1 ]:
@@ -216,9 +239,13 @@ def merge_colinear(links: list[tuple[complex, complex]]):
216
239
217
240
218
241
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
+ """
222
249
vec = p2 - p1
223
250
point = p1
224
251
while abs (vec ) > abs (point - p1 ):
@@ -229,8 +256,11 @@ def iterate_line(p1: complex, p2: complex, step: float = 1.0):
229
256
230
257
def deep_transform (data , origin : complex , theta : float ):
231
258
"""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
+ """
234
264
if isinstance (data , list | tuple ):
235
265
return [deep_transform (d , origin , theta ) for d in data ]
236
266
if isinstance (data , complex ):
@@ -246,7 +276,9 @@ def deep_transform(data, origin: complex, theta: float):
246
276
247
277
def fix_number (n : float ) -> str :
248
278
"""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
+ """
250
282
if n .is_integer ():
251
283
return str (int (n ))
252
284
n = round (n , 2 )
@@ -282,8 +314,9 @@ def mk_tag(*contents: str, **attrs: str) -> str:
282
314
283
315
284
316
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
+ """
287
320
def fix (number : float ) -> float | int :
288
321
return int (number ) if number .is_integer () else number
289
322
@@ -318,7 +351,8 @@ def polylinegon(
318
351
"""Turn the list of points into a line or filled area.
319
352
320
353
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
+ """
322
356
scaled_pts = [x * scale for x in points ]
323
357
if is_polygon :
324
358
return XML .path (d = points2path (scaled_pts , True ),
@@ -329,7 +363,7 @@ def polylinegon(
329
363
330
364
331
365
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."" "
333
367
seen = {}
334
368
for p1 , p2 in points :
335
369
if p1 == p2 :
@@ -349,8 +383,9 @@ def find_dots(points: list[tuple[complex, complex]]) -> list[complex]:
349
383
def bunch_o_lines (
350
384
pairs : list [tuple [complex , complex ]],
351
385
* , 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
+ """
354
389
lines = []
355
390
while pairs :
356
391
group = take_next_group (pairs )
@@ -378,7 +413,7 @@ def id_text(
378
413
scale : int ,
379
414
stroke : str
380
415
) -> str :
381
- "Format the component ID and value around the point."
416
+ """ Format the component ID and value around the point."" "
382
417
if nolabels :
383
418
return ""
384
419
if point is None :
@@ -426,7 +461,7 @@ def id_text(
426
461
427
462
def make_text_point (t1 : complex , t2 : complex ,
428
463
* , 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."" "
430
465
quad_angle = phase (t1 - t2 ) + pi / 2
431
466
text_pt = (t1 + t2 ) * scale / 2
432
467
offset = rect (scale / 2 * offset_scale , quad_angle )
@@ -436,7 +471,7 @@ def make_text_point(t1: complex, t2: complex,
436
471
437
472
def make_plus (terminals : list [Terminal ], center : complex ,
438
473
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."" "
440
475
if all (t .flag != "+" for t in terminals ):
441
476
return ""
442
477
return XML .g (
@@ -453,7 +488,7 @@ def make_plus(terminals: list[Terminal], center: complex,
453
488
454
489
455
490
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."" "
457
492
angle = phase (p2 - p1 )
458
493
tick_len = min (0.5 , abs (p2 - p1 ))
459
494
return [
@@ -465,7 +500,7 @@ def arrow_points(p1: complex, p2: complex) -> list[tuple[complex, complex]]:
465
500
466
501
def make_variable (center : complex , theta : float ,
467
502
is_variable : bool = True , ** options ) -> str :
468
- "Draw a 'variable' arrow across the component."
503
+ """ Draw a 'variable' arrow across the component."" "
469
504
if not is_variable :
470
505
return ""
471
506
return bunch_o_lines (deep_transform (arrow_points (- 1 , 1 ),
@@ -476,7 +511,8 @@ def make_variable(center: complex, theta: float,
476
511
477
512
def light_arrows (center : complex , theta : float , out : bool , ** options ):
478
513
"""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
+ """
480
516
a , b = 1j , 0.3 + 0.3j
481
517
if out :
482
518
a , b = b , a
@@ -491,7 +527,7 @@ def light_arrows(center: complex, theta: float, out: bool, **options):
491
527
492
528
def sort_terminals_counterclockwise (
493
529
terminals : list [Terminal ]) -> list [Terminal ]:
494
- "Sort the terminals in counterclockwise order."
530
+ """ Sort the terminals in counterclockwise order."" "
495
531
partitioned = {
496
532
side : list (filtered_terminals )
497
533
for side , filtered_terminals in itertools .groupby (
@@ -508,7 +544,7 @@ def sort_terminals_counterclockwise(
508
544
509
545
510
546
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."" "
512
548
sort = sort_terminals_counterclockwise (terminals )
513
549
for _ in range (len (sort )):
514
550
if sort == terminals :
@@ -520,7 +556,8 @@ def is_clockwise(terminals: list[Terminal]) -> bool:
520
556
def sort_for_flags (terminals : list [Terminal ],
521
557
box : Cbox , * flags : list [str ]) -> list [Terminal ]:
522
558
"""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
+ """
524
561
out = ()
525
562
for flag in flags :
526
563
matching_terminals = list (filter (lambda t : t .flag == flag , terminals ))
@@ -536,7 +573,8 @@ def sort_for_flags(terminals: list[Terminal],
536
573
)
537
574
(terminal ,) = matching_terminals
538
575
out = * out , terminal
539
- terminals .remove (terminal )
576
+ # terminals.remove(terminal)
577
+ # is this necessary with the checks above?
540
578
return out
541
579
542
580
0 commit comments