diff --git a/.vscode/settings.json b/.vscode/settings.json index 883f778..2157f82 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,8 @@ "rendec", "schemascii", "tspan" - ] + ], + "python.linting.pylintEnabled": false, + "python.linting.flake8Enabled": false, + "python.linting.enabled": true } \ No newline at end of file diff --git a/dist/schemascii-0.2.3-py3-none-any.whl b/dist/schemascii-0.2.3-py3-none-any.whl new file mode 100644 index 0000000..798c114 Binary files /dev/null and b/dist/schemascii-0.2.3-py3-none-any.whl differ diff --git a/dist/schemascii-0.2.3.tar.gz b/dist/schemascii-0.2.3.tar.gz new file mode 100644 index 0000000..39a73c0 Binary files /dev/null and b/dist/schemascii-0.2.3.tar.gz differ diff --git a/dist/schemascii-0.2.4-py3-none-any.whl b/dist/schemascii-0.2.4-py3-none-any.whl new file mode 100644 index 0000000..a9fc797 Binary files /dev/null and b/dist/schemascii-0.2.4-py3-none-any.whl differ diff --git a/dist/schemascii-0.2.4.tar.gz b/dist/schemascii-0.2.4.tar.gz new file mode 100644 index 0000000..bdf6b1f Binary files /dev/null and b/dist/schemascii-0.2.4.tar.gz differ diff --git a/dist/schemascii-0.3.0-py3-none-any.whl b/dist/schemascii-0.3.0-py3-none-any.whl new file mode 100644 index 0000000..ef829a3 Binary files /dev/null and b/dist/schemascii-0.3.0-py3-none-any.whl differ diff --git a/dist/schemascii-0.3.0.tar.gz b/dist/schemascii-0.3.0.tar.gz new file mode 100644 index 0000000..3794ccf Binary files /dev/null and b/dist/schemascii-0.3.0.tar.gz differ diff --git a/dist/schemascii-0.3.1-py3-none-any.whl b/dist/schemascii-0.3.1-py3-none-any.whl new file mode 100644 index 0000000..158f25e Binary files /dev/null and b/dist/schemascii-0.3.1-py3-none-any.whl differ diff --git a/dist/schemascii-0.3.1.tar.gz b/dist/schemascii-0.3.1.tar.gz new file mode 100644 index 0000000..acc9f32 Binary files /dev/null and b/dist/schemascii-0.3.1.tar.gz differ diff --git a/dist/schemascii-0.3.2-py3-none-any.whl b/dist/schemascii-0.3.2-py3-none-any.whl new file mode 100644 index 0000000..744e369 Binary files /dev/null and b/dist/schemascii-0.3.2-py3-none-any.whl differ diff --git a/dist/schemascii-0.3.2.tar.gz b/dist/schemascii-0.3.2.tar.gz new file mode 100644 index 0000000..be1aab1 Binary files /dev/null and b/dist/schemascii-0.3.2.tar.gz differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..7d2e0e7 --- /dev/null +++ b/index.html @@ -0,0 +1,81 @@ + + + +
+https://github.com/dragoncoder047/schemascii/
+ + + + diff --git a/pyproject.toml b/pyproject.toml index 39cf0ee..91486a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "schemascii" -version = "0.2.2" +version = "0.3.2" description = "Render ASCII-art schematics to SVG" readme = "README.md" authors = [{ name = "dragoncoder047", email = "101021094+dragoncoder047@users.noreply.github.com" }] diff --git a/schemascii/__init__.py b/schemascii/__init__.py index 24a839f..96d7308 100644 --- a/schemascii/__init__.py +++ b/schemascii/__init__.py @@ -8,7 +8,7 @@ from .utils import XML from .errors import * -__version__ = "0.2.2" +__version__ = "0.3.2" def render(filename: str, text: str = None, **options) -> str: @@ -19,36 +19,44 @@ def render(filename: str, text: str = None, **options) -> str: # get everything grid = Grid(filename, text) # Passed-in options override diagram inline options - options = apply_config_defaults(options - | get_inline_configs(grid) - | options.get("override_options", {})) + options = apply_config_defaults( + options | get_inline_configs(grid) | options.get("override_options", {}) + ) components, bom_data = find_all(grid) terminals = {c: find_edge_marks(grid, c) for c in components} - fixed_bom_data = {c: [b for b in bom_data if - b.id == c.id and b.type == c.type] - for c in components} + fixed_bom_data = { + c: [b for b in bom_data if b.id == c.id and b.type == c.type] + for c in components + } # get some options - padding = options['padding'] - scale = options['scale'] + padding = options["padding"] + scale = options["scale"] wires = get_wires(grid, **options) - components_strs = (render_component( - c, terminals[c], fixed_bom_data[c], **options) - for c in components) + components_strs = ( + render_component(c, terminals[c], fixed_bom_data[c], **options) + for c in components + ) return XML.svg( - wires, *components_strs, + wires, + *components_strs, width=grid.width * scale + padding * 2, height=grid.height * scale + padding * 2, - viewBox=f'{-padding} {-padding} ' - f'{grid.width * scale + padding * 2} ' - f'{grid.height * scale + padding * 2}', + viewBox=f"{-padding} {-padding} " + f"{grid.width * scale + padding * 2} " + f"{grid.height * scale + padding * 2}", xmlns="http://www.w3.org/2000/svg", class_="schemascii", ) -if __name__ == '__main__': - print(render( - "test_data/test_resistors.txt", - scale=20, padding=20, stroke_width=2, - stroke="black")) +if __name__ == "__main__": + print( + render( + "test_data/test_resistors.txt", + scale=20, + padding=20, + stroke_width=2, + stroke="black", + ) + ) diff --git a/schemascii/__main__.py b/schemascii/__main__.py index 823199f..b987580 100644 --- a/schemascii/__main__.py +++ b/schemascii/__main__.py @@ -8,17 +8,19 @@ def cli_main(): ap = argparse.ArgumentParser( - prog="schemascii", - description="Render ASCII-art schematics into SVG.") - ap.add_argument("-V", "--version", - action="version", - version="%(prog)s " + __version__) - ap.add_argument("in_file", - help="File to process.") - ap.add_argument("-o", "--out", - default=None, - dest="out_file", - help="Output SVG file. (default input file plus .svg)") + prog="schemascii", description="Render ASCII-art schematics into SVG." + ) + ap.add_argument( + "-V", "--version", action="version", version="%(prog)s " + __version__ + ) + ap.add_argument("in_file", help="File to process.") + ap.add_argument( + "-o", + "--out", + default=None, + dest="out_file", + help="Output SVG file. (default input file plus .svg)", + ) add_config_arguments(ap) args = ap.parse_args() if args.out_file is None: @@ -43,5 +45,5 @@ def cli_main(): out.write(result_svg) -if __name__ == '__main__': +if __name__ == "__main__": cli_main() diff --git a/schemascii/components.py b/schemascii/components.py index 542bcbd..3bbeaa0 100644 --- a/schemascii/components.py +++ b/schemascii/components.py @@ -4,7 +4,7 @@ from .errors import DiagramSyntaxError, BOMError -SMALL_COMPONENT_OR_BOM = re.compile(r'#*([A-Z]+)(\d+|\.\w+)(:[^\s]+)?#*') +SMALL_COMPONENT_OR_BOM = re.compile(r"#*([A-Z]+)(\d*|\.\w+)(:[^\s]+)?#*") def find_small(grid: Grid) -> tuple[list[Cbox], list[BOMData]]: @@ -14,19 +14,24 @@ def find_small(grid: Grid) -> tuple[list[Cbox], list[BOMData]]: boms: list[BOMData] = [] for i, line in enumerate(grid.lines): for m in SMALL_COMPONENT_OR_BOM.finditer(line): + ident = m.group(2) or "0" if m.group(3): - boms.append(BOMData(m.group(1), - m.group(2), m.group(3)[1:])) + boms.append(BOMData(m.group(1), ident, m.group(3)[1:])) else: - components.append(Cbox(complex(m.start(), i), - complex(m.end() - 1, i), - m.group(1), m.group(2))) + components.append( + Cbox( + complex(m.start(), i), + complex(m.end() - 1, i), + m.group(1), + ident, + ) + ) for z in range(*m.span(0)): grid.setmask(complex(z, i)) return components, boms -TOP_OF_BOX = re.compile(r'\.~+\.') +TOP_OF_BOX = re.compile(r"\.~+\.") def find_big(grid: Grid) -> tuple[list[Cbox], list[BOMData]]: @@ -48,35 +53,37 @@ def find_big(grid: Grid) -> tuple[list[Cbox], list[BOMData]]: if cs == tb: y2 = j break - if not cs[0] == cs[-1] == ':': + if not cs[0] == cs[-1] == ":": raise DiagramSyntaxError( - f'{grid.filename}: Fragmented box ' - f'starting at line {y1 + 1}, col {x1 + 1}') + f"{grid.filename}: Fragmented box " + f"starting at line {y1 + 1}, col {x1 + 1}" + ) else: raise DiagramSyntaxError( - f'{grid.filename}: Unfinished box ' - f'starting at line {y1 + 1}, col {x1 + 1}') + f"{grid.filename}: Unfinished box " + f"starting at line {y1 + 1}, col {x1 + 1}" + ) inside = grid.clip(complex(x1, y1), complex(x2, y2)) results, resb = find_small(inside) if len(results) == 0 and len(resb) == 0: raise BOMError( - f'{grid.filename}: Box starting at ' - f'line {y1 + 1}, col {x1 + 1} is ' - f'missing reference designator') + f"{grid.filename}: Box starting at " + f"line {y1 + 1}, col {x1 + 1} is " + f"missing reference designator" + ) if len(results) != 1 and len(resb) != 1: raise BOMError( - f'{grid.filename}: Box starting at ' - f'line {y1 + 1}, col {x1 + 1} has ' - f'multiple reference designators') + f"{grid.filename}: Box starting at " + f"line {y1 + 1}, col {x1 + 1} has " + f"multiple reference designators" + ) if not results: merd = resb[0] else: merd = results[0] boxes.append( - Cbox(complex(x1, y1), - complex(x2 - 1, y2), - merd.type, - merd.id)) + Cbox(complex(x1, y1), complex(x2 - 1, y2), merd.type, merd.id) + ) boms.extend(resb) # mark everything for i in range(x1, x2): @@ -93,10 +100,10 @@ def find_all(grid: Grid) -> tuple[list[Cbox], list[BOMData]]: and masks off all of them, leaving only wires and extraneous text.""" b1, l1 = find_big(grid) b2, l2 = find_small(grid) - return b1+b2, l1+l2 + return b1 + b2, l1 + l2 -if __name__ == '__main__': +if __name__ == "__main__": test_grid = Grid("test_data/test_resistors.txt") bbb, _ = find_all(test_grid) all_pts = [] diff --git a/schemascii/components_render.py b/schemascii/components_render.py index 7142bf0..46fd3bf 100644 --- a/schemascii/components_render.py +++ b/schemascii/components_render.py @@ -2,10 +2,25 @@ from cmath import phase, rect from math import pi from warnings import warn -from .utils import (Cbox, Terminal, BOMData, XML, Side, arrow_points, - polylinegon, id_text, make_text_point, - bunch_o_lines, deep_transform, make_plus, make_variable, - sort_counterclockwise, light_arrows, sort_for_flags, is_clockwise) +from .utils import ( + Cbox, + Terminal, + BOMData, + XML, + Side, + arrow_points, + polylinegon, + id_text, + make_text_point, + bunch_o_lines, + deep_transform, + make_plus, + make_variable, + sort_counterclockwise, + light_arrows, + sort_for_flags, + is_clockwise, +) from .errors import TerminalsError, BOMError, UnsupportedComponentError # pylint: disable=unbalanced-tuple-unpacking @@ -15,52 +30,51 @@ def component(*rd_s: list[str]) -> Callable: "Registers the component under a set of reference designators." + def rendec(func: Callable[[Cbox, list[Terminal], list[BOMData]], str]): for r_d in rd_s: rdu = r_d.upper() if rdu in RENDERERS: - raise RuntimeError( - f"{rdu} reference designator already taken") + raise RuntimeError(f"{rdu} reference designator already taken") RENDERERS[rdu] = func return func + return rendec def n_terminal(n_terminals: int) -> Callable: "Ensures the component has N terminals." + def n_inner(func: Callable) -> Callable: def n_check( - box: Cbox, - terminals: list[Terminal], - bom_data: list[BOMData], - **options): + box: Cbox, terminals: list[Terminal], bom_data: list[BOMData], **options + ): if len(terminals) != n_terminals: raise TerminalsError( f"{box.type}{box.id} component can only " - f"have {n_terminals} terminals") + f"have {n_terminals} terminals" + ) return func(box, terminals, bom_data, **options) + n_check.__doc__ = func.__doc__ return n_check + return n_inner def no_ambiguous(func: Callable) -> Callable: "Ensures the component has exactly one BOM data marker, and unwraps it." + def de_ambiguous( - box: Cbox, - terminals: list[Terminal], - bom_data: list[BOMData], - **options): + box: Cbox, terminals: list[Terminal], bom_data: list[BOMData], **options + ): if len(bom_data) > 1: raise BOMError( f"Ambiguous BOM data for {box.type}{box.id}: {bom_data!r}") if not bom_data: bom_data = [BOMData(box.type, box.id, "")] - return func( - box, - terminals, - bom_data[0], - **options) + return func(box, terminals, bom_data[0], **options) + de_ambiguous.__doc__ = func.__doc__ return de_ambiguous @@ -68,22 +82,18 @@ def de_ambiguous( def polarized(func: Callable) -> Callable: """Ensures the component has 2 terminals, and then sorts them so the + terminal is first.""" + def sort_terminals( - box: Cbox, - terminals: list[Terminal], - bom_data: list[BOMData], - **options): + box: Cbox, terminals: list[Terminal], bom_data: list[BOMData], **options + ): if len(terminals) != 2: raise TerminalsError( - f"{box.type}{box.id} component can only " - f"have 2 terminals") - if terminals[1].flag == '+': + f"{box.type}{box.id} component can only " f"have 2 terminals" + ) + if terminals[1].flag == "+": terminals[0], terminals[1] = terminals[1], terminals[0] - return func( - box, - terminals, - bom_data, - **options) + return func(box, terminals, bom_data, **options) + sort_terminals.__doc__ = func.__doc__ return sort_terminals @@ -91,11 +101,7 @@ def sort_terminals( @component("R", "RV", "VR") @n_terminal(2) @no_ambiguous -def resistor( - box: Cbox, - terminals: list[Terminal], - bom_data: BOMData, - **options): +def resistor(box: Cbox, terminals: list[Terminal], bom_data: BOMData, **options): """Resistor, Variable resistor, etc. bom:ohms[,watts]""" t1, t2 = terminals[0].pt, terminals[1].pt @@ -106,24 +112,27 @@ def resistor( quad_angle = angle + pi / 2 points = [t1] for i in range(1, 4 * int(length)): - points.append(t1 - rect(i / 4, angle) + - pow(-1, i) * rect(1, quad_angle) / 4) + points.append(t1 - rect(i / 4, angle) + pow(-1, i) + * rect(1, quad_angle) / 4) points.append(t2) - return (polylinegon(points, **options) - + make_variable(mid, angle, "V" in box.type, **options) - + id_text( - box, bom_data, terminals, (("Ω", False), ("W", False)), - make_text_point(t1, t2, **options), **options)) + return ( + polylinegon(points, **options) + + make_variable(mid, angle, "V" in box.type, **options) + + id_text( + box, + bom_data, + terminals, + (("Ω", False), ("W", False)), + make_text_point(t1, t2, **options), + **options, + ) + ) @component("C", "CV", "VC") @n_terminal(2) @no_ambiguous -def capacitor( - box: Cbox, - terminals: list[Terminal], - bom_data: BOMData, - **options): +def capacitor(box: Cbox, terminals: list[Terminal], bom_data: BOMData, **options): """Draw a capacitor, variable capacitor, etc. bom:farads[,volts] flags:+=positive""" @@ -131,26 +140,34 @@ def capacitor( mid = (t1 + t2) / 2 angle = phase(t1 - t2) lines = [ - (t1, mid + rect(.25, angle)), - (t2, mid + rect(-.25, angle))] + deep_transform([ - (complex(.4, .25), complex(-.4, .25)), - (complex(.4, -.25), complex(-.4, -.25)), - ], mid, angle) - return (bunch_o_lines(lines, **options) - + make_plus(terminals, mid, angle, **options) - + make_variable(mid, angle, "V" in box.type, **options) - + id_text( - box, bom_data, terminals, (("F", True), ("V", False)), - make_text_point(t1, t2, **options), **options)) + (t1, mid + rect(0.25, angle)), + (t2, mid + rect(-0.25, angle)), + ] + deep_transform( + [ + (complex(0.4, 0.25), complex(-0.4, 0.25)), + (complex(0.4, -0.25), complex(-0.4, -0.25)), + ], + mid, + angle, + ) + return ( + bunch_o_lines(lines, **options) + + make_plus(terminals, mid, angle, **options) + + make_variable(mid, angle, "V" in box.type, **options) + + id_text( + box, + bom_data, + terminals, + (("F", True), ("V", False)), + make_text_point(t1, t2, **options), + **options, + ) + ) @component("L", "VL", "LV") @no_ambiguous -def inductor( - box: Cbox, - terminals: list[Terminal], - bom_data: BOMData, - **options): +def inductor(box: Cbox, terminals: list[Terminal], bom_data: BOMData, **options): """Draw an inductor (coil, choke, etc) bom:henries""" t1, t2 = terminals[0].pt, terminals[1].pt @@ -162,26 +179,30 @@ def inductor( data = f"M{t1.real * scale} {t1.imag * scale}" dxdy = rect(scale, angle) for _ in range(int(length)): - data += f"a1 1 0 01{-dxdy.real} {dxdy.imag}" - return (XML.path( - d=data, - stroke=options["stroke"], - fill="transparent", - stroke__width=options["stroke_width"]) + data += f"a1 1 0 01 {-dxdy.real} {dxdy.imag}" + return ( + XML.path( + d=data, + stroke=options["stroke"], + fill="transparent", + stroke__width=options["stroke_width"], + ) + make_variable(mid, angle, "V" in box.type, **options) + id_text( - box, bom_data, terminals, (("H", False),), - make_text_point(t1, t2, **options), **options)) + box, + bom_data, + terminals, + (("H", False),), + make_text_point(t1, t2, **options), + **options, + ) + ) @component("B", "BT", "BAT") @polarized @no_ambiguous -def battery( - box: Cbox, - terminals: list[Terminal], - bom_data: BOMData, - **options): +def battery(box: Cbox, terminals: list[Terminal], bom_data: BOMData, **options): """Draw a battery cell. bom:volts[,amp-hours] flags:+=positive""" @@ -189,27 +210,32 @@ def battery( mid = (t1 + t2) / 2 angle = phase(t1 - t2) lines = [ - (t1, mid + rect(.5, angle)), - (t2, mid + rect(-.5, angle))] + deep_transform([ - (complex(.5, .5), complex(-.5, .5)), - (complex(.25, .16), complex(-.25, .16)), - (complex(.5, -.16), complex(-.5, -.16)), - (complex(.25, -.5), complex(-.25, -.5)), - ], mid, angle) - return (id_text( - box, bom_data, terminals, (("V", False), ("Ah", False)), - make_text_point(t1, t2, **options), **options) - + bunch_o_lines(lines, **options)) + (t1, mid + rect(0.5, angle)), + (t2, mid + rect(-0.5, angle)), + ] + deep_transform( + [ + (complex(0.5, 0.5), complex(-0.5, 0.5)), + (complex(0.25, 0.16), complex(-0.25, 0.16)), + (complex(0.5, -0.16), complex(-0.5, -0.16)), + (complex(0.25, -0.5), complex(-0.25, -0.5)), + ], + mid, + angle, + ) + return id_text( + box, + bom_data, + terminals, + (("V", False), ("Ah", False)), + make_text_point(t1, t2, **options), + **options, + ) + bunch_o_lines(lines, **options) @component("D", "LED", "CR", "IR") @polarized @no_ambiguous -def diode( - box: Cbox, - terminals: list[Terminal], - bom_data: BOMData, - **options): +def diode(box: Cbox, terminals: list[Terminal], bom_data: BOMData, **options): """Draw a diode or LED. bom:part-number flags:+=positive""" @@ -217,23 +243,31 @@ def diode( mid = (t1 + t2) / 2 angle = phase(t1 - t2) lines = [ - (t2, mid + rect(.3, angle)), - (t1, mid + rect(-.3, angle)), - deep_transform((-.3-.3j, .3-.3j), mid, angle)] - triangle = deep_transform((-.3j, .3+.3j, -.3+.3j), mid, angle) + (t2, mid + rect(0.3, angle)), + (t1, mid + rect(-0.3, angle)), + deep_transform((-0.3 - 0.3j, 0.3 - 0.3j), mid, angle), + ] + triangle = deep_transform((-0.3j, 0.3 + 0.3j, -0.3 + 0.3j), mid, angle) light_emitting = box.type in ("LED", "IR") fill_override = {"stroke": bom_data.data} if box.type == "LED" else {} - return ((light_arrows(mid, angle, True, **options) - if light_emitting else "") - + id_text(box, bom_data, terminals, None, - make_text_point(t1, t2, **options), **options) - + bunch_o_lines(lines, **(options | fill_override)) - + polylinegon(triangle, True, **options)) + return ( + (light_arrows(mid, angle, True, **options) if light_emitting else "") + + id_text( + box, + bom_data, + terminals, + None, + make_text_point(t1, t2, **options), + **options, + ) + + bunch_o_lines(lines, **(options | fill_override)) + + polylinegon(triangle, True, **options) + ) SIDE_TO_ANGLE_MAP = { Side.RIGHT: pi, - Side.TOP: pi / 2, + Side.TOP: pi / 2, Side.LEFT: 0, Side.BOTTOM: 3 * pi / 2, } @@ -242,10 +276,8 @@ def diode( @component("U", "IC") @no_ambiguous def integrated_circuit( - box: Cbox, - terminals: list[Terminal], - bom_data: BOMData, - **options): + box: Cbox, terminals: list[Terminal], bom_data: BOMData, **options +): """Draw an IC. bom:part-number[,pin1-label[,pin2-label[,...]]]""" label_style = options["label"] @@ -260,12 +292,12 @@ def integrated_circuit( height=sz.imag, stroke__width=options["stroke_width"], stroke=options["stroke"], - fill="transparent") + fill="transparent", + ) for term in terminals: - out += bunch_o_lines([( - term.pt, - term.pt + rect(1, SIDE_TO_ANGLE_MAP[term.side]) - )], **options) + out += bunch_o_lines( + [(term.pt, term.pt + rect(1, SIDE_TO_ANGLE_MAP[term.side]))], **options + ) if "V" in label_style and part_num: out += XML.text( XML.tspan(part_num, class_="part-num"), @@ -273,7 +305,8 @@ def integrated_circuit( y=mid.imag, text__anchor="middle", font__size=options["scale"], - fill=options["stroke"]) + fill=options["stroke"], + ) mid -= 1j * scale if "L" in label_style and not options["nolabels"]: out += XML.text( @@ -282,7 +315,8 @@ def integrated_circuit( y=mid.imag, text__anchor="middle", font__size=options["scale"], - fill=options["stroke"]) + fill=options["stroke"], + ) s_terminals = sort_counterclockwise(terminals) for terminal, label in zip(s_terminals, pin_labels): sc_text_pt = terminal.pt * scale @@ -290,66 +324,76 @@ def integrated_circuit( label, x=sc_text_pt.real, y=sc_text_pt.imag, - text__anchor=("start" if (terminal.side in (Side.TOP, Side.BOTTOM)) - else "middle"), + text__anchor=( + "start" if (terminal.side in ( + Side.TOP, Side.BOTTOM)) else "middle" + ), font__size=options["scale"], fill=options["stroke"], - class_="pin-label") - warn("ICs are not fully implemented yet. " - "Pin labels may have not been rendered correctly.") + class_="pin-label", + ) + warn( + "ICs are not fully implemented yet. " + "Pin labels may have not been rendered correctly." + ) return out @component("J", "P") @n_terminal(1) @no_ambiguous -def jack( - box: Cbox, - terminals: list[Terminal], - bom_data: BOMData, - **options): +def jack(box: Cbox, terminals: list[Terminal], bom_data: BOMData, **options): """Draw a jack connector or plug. - bom:label""" + bom:label[,{circle/input/output}]""" scale = options["scale"] - sc_t1 = terminals[0].pt * scale - sc_t2 = sc_t1 + rect(scale, SIDE_TO_ANGLE_MAP[terminals[0].side]) - sc_text_pt = sc_t2 + rect(scale * 2, SIDE_TO_ANGLE_MAP[terminals[0].side]) - return ( - XML.line( - x1=sc_t1.real, - x2=sc_t2.real, - y1=sc_t1.imag, - y2=sc_t2.imag, - stroke__width=options["stroke_width"], - stroke=options["stroke"]) - + XML.circle( - cx=sc_t2.real, - cy=sc_t2.imag, - r=scale / 4, - stroke__width=options["stroke_width"], - stroke=options["stroke"], - fill="transparent") - + id_text(box, bom_data, terminals, None, sc_text_pt, **options)) + t1 = terminals[0].pt + t2 = t1 + rect(1, SIDE_TO_ANGLE_MAP[terminals[0].side]) + sc_t2 = t2 * scale + sc_text_pt = sc_t2 + rect(scale / 2, SIDE_TO_ANGLE_MAP[terminals[0].side]) + style = "input" if terminals[0].side in (Side.LEFT, Side.TOP) else "output" + if any(bom_data.data.endswith(x) for x in (",circle", ",input", ",output")): + style = bom_data.data.split(",")[-1] + bom_data = BOMData( + bom_data.type, + bom_data.id, + bom_data.data.rstrip("cirlenputo").removesuffix(","), + ) + if style == "circle": + return ( + bunch_o_lines([(t1, t2)], **options) + + XML.circle( + cx=sc_t2.real, + cy=sc_t2.imag, + r=scale / 4, + stroke__width=options["stroke_width"], + stroke=options["stroke"], + fill="transparent", + ) + + id_text(box, bom_data, terminals, None, sc_text_pt, **options) + ) + if style == "output": + t1, t2 = t2, t1 + return bunch_o_lines(arrow_points(t1, t2), **options) + id_text( + box, bom_data, terminals, None, sc_text_pt, **options + ) @component("Q", "MOSFET", "MOS", "FET") @n_terminal(3) @no_ambiguous -def transistor( - box: Cbox, - terminals: list[Terminal], - bom_data: BOMData, - **options): +def transistor(box: Cbox, terminals: list[Terminal], bom_data: BOMData, **options): """Draw a bipolar transistor (PNP/NPN) or FET (NFET/PFET). bom:{npn/pnp/nfet/pfet}:part-number flags:s=source,d=drain,g=gate,e=emitter,c=collector,b=base""" - if not any(bom_data.data.lower().startswith(x) for x in ("pnp:", "npn:", "nfet:", "pfet:")): - raise BOMError( - f"Need type of transistor for {box.type}{box.id}") - silicon_type, part_num = bom_data.data.split(":") + if not any( + bom_data.data.lower().startswith(x) for x in ("pnp", "npn", "nfet", "pfet") + ): + raise BOMError(f"Need type of transistor for {box.type}{box.id}") + silicon_type, *part_num = bom_data.data.split(":") + part_num = ":".join(part_num) silicon_type = silicon_type.lower() bom_data = BOMData(bom_data.type, bom_data.id, part_num) - if 'fet' in silicon_type: + if "fet" in silicon_type: ae, se, ctl = sort_for_flags(terminals, box, "s", "d", "g") else: ae, se, ctl = sort_for_flags(terminals, box, "e", "c", "b") @@ -363,44 +407,145 @@ def transistor( # From wolfram alpha "solve m*(x-x1)+y1=(-1/m)*(x-x2)+y2 for x" # x = (m^2 x1 - m y1 + m y2 + x2)/(m^2 + 1) slope = diff.imag / diff.real - mid_x = (slope ** 2 * ap.real - slope * ap.imag + slope * - ctl.pt.imag + ctl.pt.real) / (slope ** 2 + 1) + mid_x = ( + slope**2 * ap.real - slope * ap.imag + slope * ctl.pt.imag + ctl.pt.real + ) / (slope**2 + 1) mid = complex(mid_x, slope * (mid_x - ap.real) + ap.imag) theta = phase(ap - sp) backwards = 1 if is_clockwise([ae, se, ctl]) else -1 thetaquarter = theta + (backwards * pi / 2) out_lines = [ - (ap, mid + rect(.8, theta)), # Lead in - (sp, mid - rect(.8, theta)), # Lead out + (ap, mid + rect(0.8, theta)), # Lead in + (sp, mid - rect(0.8, theta)), # Lead out ] - if 'fet' in silicon_type: - arr = mid + rect(.8, theta), mid + rect(.8, theta) + \ - rect(.7, thetaquarter) - if 'nfet' == silicon_type: + if "fet" in silicon_type: + arr = mid + rect(0.8, theta), mid + \ + rect(0.8, theta) + rect(0.7, thetaquarter) + if "nfet" == silicon_type: arr = arr[1], arr[0] - out_lines.extend([ - *arrow_points(*arr), - (mid - rect(.8, theta), mid - rect(.8, theta) + rect(.7, thetaquarter)), - (mid + rect(1, theta) + rect(.7, thetaquarter), - mid - rect(1, theta) + rect(.7, thetaquarter)), - (mid + rect(.5, theta) + rect(1, thetaquarter), - mid - rect(.5, theta) + rect(1, thetaquarter)), - ]) + out_lines.extend( + [ + *arrow_points(*arr), + ( + mid - rect(0.8, theta), + mid - rect(0.8, theta) + rect(0.7, thetaquarter), + ), + ( + mid + rect(1, theta) + rect(0.7, thetaquarter), + mid - rect(1, theta) + rect(0.7, thetaquarter), + ), + ( + mid + rect(0.5, theta) + rect(1, thetaquarter), + mid - rect(0.5, theta) + rect(1, thetaquarter), + ), + ] + ) else: - arr = mid + rect(.8, theta), mid + rect(.4, theta) + \ - rect(1, thetaquarter) - if 'npn' == silicon_type: + arr = mid + rect(0.8, theta), mid + \ + rect(0.4, theta) + rect(1, thetaquarter) + if "npn" == silicon_type: arr = arr[1], arr[0] - out_lines.extend([ - *arrow_points(*arr), - (mid - rect(.8, theta), mid - rect(.4, theta) + rect(1, thetaquarter)), - (mid + rect(1, theta) + rect(1, thetaquarter), - mid - rect(1, theta) + rect(1, thetaquarter)), - ]) + out_lines.extend( + [ + *arrow_points(*arr), + ( + mid - rect(0.8, theta), + mid - rect(0.4, theta) + rect(1, thetaquarter), + ), + ( + mid + rect(1, theta) + rect(1, thetaquarter), + mid - rect(1, theta) + rect(1, thetaquarter), + ), + ] + ) out_lines.append((mid + rect(1, thetaquarter), ctl.pt)) - return (id_text(box, bom_data, [ae, se], None, - make_text_point(ap, sp, **options), **options) - + bunch_o_lines(out_lines, **options)) + return id_text( + box, bom_data, [ae, se], None, make_text_point(ap, sp, **options), **options + ) + bunch_o_lines(out_lines, **options) + + +@component("G", "GND") +@n_terminal(1) +@no_ambiguous +def ground(box: Cbox, terminals: list[Terminal], bom_data: BOMData, **options): + """Draw a ground symbol. + bom:[{earth/chassis/signal/common}]""" + icon_type = bom_data.data or "earth" + points = [(0, 1j), (-0.5 + 1j, 0.5 + 1j)] + match icon_type: + case "earth": + points += [(-0.33 + 1.25j, 0.33 + 1.25j), + (-0.16 + 1.5j, 0.16 + 1.5j)] + case "chassis": + points += [ + (-0.5 + 1j, -0.25 + 1.5j), + (1j, 0.25 + 1.5j), + (0.5 + 1j, 0.75 + 1.5j), + ] + case "signal": + points += [(-0.5 + 1j, 1.5j), (0.5 + 1j, 1.5j)] + case "common": + pass + case _: + raise BOMError(f"Unknown ground symbol type: {icon_type}") + points = deep_transform(points, terminals[0].pt, pi / 2) + return bunch_o_lines(points, **options) + + +@component("S", "SW", "PB") +@n_terminal(2) +@no_ambiguous +def switch(box: Cbox, terminals: list[Terminal], bom_data: BOMData, **options): + """Draw a mechanical switch symbol. + bom:{nc/no}[m][:label]""" + icon_type = bom_data.data or "no" + if ":" in icon_type: + icon_type, *b = icon_type.split(":") + bom_data = BOMData(bom_data.type, bom_data.id, ":".join(b)) + else: + bom_data = BOMData(bom_data.type, bom_data.id, "") + t1, t2 = terminals[0].pt, terminals[1].pt + mid = (t1 + t2) / 2 + angle = phase(t1 - t2) + quad_angle = angle + pi / 2 + scale = options["scale"] + out = (XML.circle( + cx=(rect(-scale, angle) + mid * scale).real, + cy=(rect(-scale, angle) + mid * scale).imag, + r=scale / 4, + stroke="transparent", + fill=options["stroke"], + class_="filled", + ) + XML.circle( + cx=(rect(scale, angle) + mid * scale).real, + cy=(rect(scale, angle) + mid * scale).imag, + r=scale / 4, + stroke="transparent", + fill=options["stroke"], + class_="filled", + ) + bunch_o_lines([(t1, mid + rect(1, angle)), (t2, mid + rect(-1, angle))], **options)) + sc = 1 + match icon_type: + case "nc": + points = [(-1j, -.3+1j)] + case "no": + points = [(-1j, -.8+1j)] + sc = 1.9 + case "ncm": + points = [(.3-1j, .3+1j)] + out += polylinegon(deep_transform([-.5+.6j, -.5-.6j, .3-.6j, .3+.6j], mid, angle), True, **options) + sc = 1.3 + case "nom": + points = [(-.5-1j, -.5+1j)] + out += polylinegon(deep_transform([-1+.6j, -1-.6j, -.5-.6j, -.5+.6j], mid, angle), True, **options) + sc = 2.5 + case _: + raise BOMError(f"Unknown switch symbol type: {icon_type}") + points = deep_transform(points, mid, angle) + return bunch_o_lines(points, **options) + out + id_text( + box, bom_data, terminals, None, make_text_point( + t1, t2, **(options | {"offset_scaler": sc})), **options) + # code for drawing stuff # https://github.com/pfalstad/circuitjs1/tree/master/src/com/lushprojects/circuitjs1/client @@ -412,30 +557,28 @@ def transistor( # + is on the top unless otherwise noted # terminals will be connected at (0, -1) and (0, 1) relative to the paths here # if they aren't the path will be transformed -twoterminals = { +{ # fuse - 'F': 'M0-.9A.1.1 0 000-1.1.1.1 0 000-.9ZM0-1Q.5-.5 0 0T0 1Q-.5.5 0 0T0-1ZM0 1.1A.1.1 0 000 .9.1.1 0 000 1.1Z', + "F": "M0-.9A.1.1 0 000-1.1.1.1 0 000-.9ZM0-1Q.5-.5 0 0T0 1Q-.5.5 0 0T0-1ZM0 1.1A.1.1 0 000 .9.1.1 0 000 1.1Z", # jumper pads - 'JP': 'M0-1Q-1-1-1-.25H1Q1-1 0-1ZM0 1Q-1 1-1 .25H1Q1 1 0 1', + "JP": "M0-1Q-1-1-1-.25H1Q1-1 0-1ZM0 1Q-1 1-1 .25H1Q1 1 0 1", # loudspeaker - 'LS': 'M0-1V-.5H-.25V.5H.25V-.5H0M0 1V.5ZM1-1 .25-.5V.5L1 1Z', + "LS": "M0-1V-.5H-.25V.5H.25V-.5H0M0 1V.5ZM1-1 .25-.5V.5L1 1Z", # electret mic - 'MIC': 'M1 0A1 1 0 00-1 0 1 1 0 001 0V-1 1Z', + "MIC": "M1 0A1 1 0 00-1 0 1 1 0 001 0V-1 1Z", } def render_component( - box: Cbox, - terminals: list[Terminal], - bom_data: list[BOMData], - **options): + box: Cbox, terminals: list[Terminal], bom_data: list[BOMData], **options +): "Render the component into an SVG string." if box.type not in RENDERERS: raise UnsupportedComponentError(box.type) return XML.g( RENDERERS[box.type](box, terminals, bom_data, **options), - class_=f"component {box.type}" + class_=f"component {box.type}", ) -__all__ = ['render_component'] +__all__ = ["render_component"] diff --git a/schemascii/configs.py b/schemascii/configs.py index 97a400c..d2376d7 100644 --- a/schemascii/configs.py +++ b/schemascii/configs.py @@ -12,16 +12,24 @@ class ConfigConfig: OPTIONS = [ - ConfigConfig("padding", float, 10, - "Amount of padding to add on the edges."), - ConfigConfig("scale", float, 15, - "Scale at which to enlarge the entire diagram by."), + ConfigConfig("padding", float, 10, "Amount of padding to add on the edges."), + ConfigConfig( + "scale", float, 15, "Scale at which to enlarge the entire diagram by." + ), ConfigConfig("stroke_width", float, 2, "Width of the lines"), ConfigConfig("stroke", str, "black", "Color of the lines."), - ConfigConfig("label", ["L", "V", "VL"], "VL", - "Component label style (L=include label, V=include value, VL=both)"), - ConfigConfig("nolabels", bool, False, - "Turns off labels on all components, except for part numbers on ICs."), + ConfigConfig( + "label", + ["L", "V", "VL"], + "VL", + "Component label style (L=include label, V=include value, VL=both)", + ), + ConfigConfig( + "nolabels", + bool, + False, + "Turns off labels on all components, except for part numbers on ICs.", + ), ] @@ -33,18 +41,21 @@ def add_config_arguments(a: argparse.ArgumentParser): "--" + opt.name, help=opt.description, choices=opt.clazz, - default=opt.default) + default=opt.default, + ) elif opt.clazz is bool: a.add_argument( "--" + opt.name, help=opt.description, - action="store_false" if opt.default else "store_true") + action="store_false" if opt.default else "store_true", + ) else: a.add_argument( "--" + opt.name, help=opt.description, type=opt.clazz, - default=opt.default) + default=opt.default, + ) def apply_config_defaults(options: dict) -> dict: @@ -58,12 +69,15 @@ def apply_config_defaults(options: dict) -> dict: raise ArgumentError( f"config option {opt.name}: " f"invalid choice: {options[opt.name]} " - f"(valid options are {', '.join(map(repr, opt.clazz))})") + f"(valid options are {', '.join(map(repr, opt.clazz))})" + ) continue try: options[opt.name] = opt.clazz(options[opt.name]) except ValueError as err: - raise ArgumentError(f"config option {opt.name}: " - f"invalid {opt.clazz.__name__} value: " - f"{options[opt.name]}") from err + raise ArgumentError( + f"config option {opt.name}: " + f"invalid {opt.clazz.__name__} value: " + f"{options[opt.name]}" + ) from err return options diff --git a/schemascii/edgemarks.py b/schemascii/edgemarks.py index aea494e..d9bd19c 100644 --- a/schemascii/edgemarks.py +++ b/schemascii/edgemarks.py @@ -6,44 +6,51 @@ def over_edges(box: Cbox) -> list: "Decorator - Runs around the edges of the box on the grid." + def inner_over_edges(func: FunctionType): out = [] for p, s in chain( # Top side - ((complex(xx, int(box.p1.imag) - 1), Side.TOP) - for xx in range(int(box.p1.real), int(box.p2.real) + 1)), + ( + (complex(xx, int(box.p1.imag) - 1), Side.TOP) + for xx in range(int(box.p1.real), int(box.p2.real) + 1) + ), # Right side - ((complex(int(box.p2.real) + 1, yy), Side.RIGHT) - for yy in range(int(box.p1.imag), int(box.p2.imag) + 1)), + ( + (complex(int(box.p2.real) + 1, yy), Side.RIGHT) + for yy in range(int(box.p1.imag), int(box.p2.imag) + 1) + ), # Bottom side - ((complex(xx, int(box.p2.imag) + 1), Side.BOTTOM) - for xx in range(int(box.p1.real), int(box.p2.real) + 1)), + ( + (complex(xx, int(box.p2.imag) + 1), Side.BOTTOM) + for xx in range(int(box.p1.real), int(box.p2.real) + 1) + ), # Left side - ((complex(int(box.p1.real) - 1, yy), Side.LEFT) - for yy in range(int(box.p1.imag), int(box.p2.imag) + 1)), + ( + (complex(int(box.p1.real) - 1, yy), Side.LEFT) + for yy in range(int(box.p1.imag), int(box.p2.imag) + 1) + ), ): result = func(p, s) if result is not None: out.append(result) return out + return inner_over_edges def take_flags(grid: Grid, box: Cbox) -> list[Flag]: """Runs around the edges of the component box, collects the flags, and masks them off to wires.""" + @over_edges(box) def flags(p: complex, s: Side) -> Flag | None: c = grid.get(p) - if c in ' -|()*': + if c in " -|()*": return None - if s in (Side.TOP, Side.BOTTOM): - grid.setmask(p, '|') - return Flag(p, c, s) - if s in (Side.LEFT, Side.RIGHT): - grid.setmask(p, '-') - return Flag(p, c, s) - return None + grid.setmask(p, "*") + return Flag(p, c, s) + return flags @@ -54,11 +61,13 @@ def find_edge_marks(grid: Grid, box: Cbox) -> list[Terminal]: @over_edges(box) def terminals(p: complex, s: Side) -> Terminal | None: c = grid.get(p) - if ((c in "*|()" and s in (Side.TOP, Side.BOTTOM)) - or (c in "*-" and s in (Side.LEFT, Side.RIGHT))): + if (c in "*|()" and s in (Side.TOP, Side.BOTTOM)) or ( + c in "*-" and s in (Side.LEFT, Side.RIGHT) + ): maybe_flag = [f for f in flags if f.pt == p] if maybe_flag: return Terminal(p, maybe_flag[0].char, s) return Terminal(p, None, s) return None + return terminals diff --git a/schemascii/grid.py b/schemascii/grid.py index 9463ac8..85e39d4 100644 --- a/schemascii/grid.py +++ b/schemascii/grid.py @@ -8,12 +8,12 @@ def __init__(self, filename: str, data: str = None): data = f.read() self.filename: str = filename self.raw: str = data - lines: list[str] = data.split('\n') + lines: list[str] = data.split("\n") maxlen: int = max(len(line) for line in lines) - self.data: list[list[str]] = [ - list(line.ljust(maxlen, ' ')) for line in lines] + self.data: list[list[str]] = [list(line.ljust(maxlen, " ")) for line in lines] self.masks: list[list[bool | str]] = [ - [False for x in range(maxlen)] for y in range(len(lines))] + [False for x in range(maxlen)] for y in range(len(lines)) + ] self.width = maxlen self.height = len(self.data) @@ -27,14 +27,16 @@ def get(self, p: complex) -> str: the mask character if it was set, otherwise the original character.""" if not self.validbounds(p): - return ' ' + return " " return self.getmask(p) or self.data[int(p.imag)][int(p.real)] @property def lines(self): "The current contents, with masks applied." - return [''.join(self.get(complex(x, y)) for x in range(self.width)) - for y in range(self.height)] + return [ + "".join(self.get(complex(x, y)) for x in range(self.width)) + for y in range(self.height) + ] def getmask(self, p: complex) -> str | bool: """Sees the mask applied to the specified point; @@ -43,7 +45,7 @@ def getmask(self, p: complex) -> str | bool: return False return self.masks[int(p.imag)][int(p.real)] - def setmask(self, p: complex, mask: str | bool = ' '): + def setmask(self, p: complex, mask: str | bool = " "): "Sets or clears the mask at the point." if not self.validbounds(p): return @@ -55,19 +57,19 @@ def clrmask(self, p: complex): def clrall(self): "Clears all the masks at once." - self.masks = [[False for x in range(self.width)] - for y in range(self.height)] + self.masks = [[False for x in range(self.width)] for y in range(self.height)] def clip(self, p1: complex, p2: complex): """Returns a sub-grid with the contents bounded by the p1 and p2 box. Masks are not copied.""" ls = slice(int(p1.real), int(p2.real)) cs = slice(int(p1.imag), int(p2.imag) + 1) - d = '\n'.join(''.join(ln[ls]) for ln in self.data[cs]) + d = "\n".join("".join(ln[ls]) for ln in self.data[cs]) return Grid(self.filename, d) def __repr__(self): return f"Grid({self.filename!r}, '''\n{chr(10).join(self.lines)}''')" + __str__ = __repr__ def spark(self, *points): @@ -82,6 +84,7 @@ def spark(self, *points): print(char, end="") print() -if __name__ == '__main__': - x = Grid('', ' \n \n ') - x.spark(0, 1, 2, 1j, 2j, 1+2j, 2+2j, 2+1j) + +if __name__ == "__main__": + x = Grid("", " \n \n ") + x.spark(0, 1, 2, 1j, 2j, 1 + 2j, 2 + 2j, 2 + 1j) diff --git a/schemascii/inline_config.py b/schemascii/inline_config.py index f72acbe..a37d629 100644 --- a/schemascii/inline_config.py +++ b/schemascii/inline_config.py @@ -22,12 +22,14 @@ def get_inline_configs(grid: Grid) -> dict: return out -if __name__ == '__main__': - g = Grid("null", - """ +if __name__ == "__main__": + g = Grid( + "null", + """ foobar -------C1------- !padding=30!!label=! !foobar=bar! -""") +""", + ) print(get_inline_configs(g)) print(g) diff --git a/schemascii/metric.py b/schemascii/metric.py index ef8182f..031b86c 100644 --- a/schemascii/metric.py +++ b/schemascii/metric.py @@ -23,17 +23,17 @@ def prefix_to_exponent(prefix: int) -> str: E.g. "k" --> 3 (kilo) E.g. " " --> 0 (no prefix) E.g. "u" --> -6 (micro)""" - if prefix in (' ', ''): + if prefix in (" ", ""): return 0 if prefix == "µ": - prefix = "u" # allow unicode - if prefix == 'K': + prefix = "u" # allow unicode + if prefix == "K": prefix = prefix.lower() # special case (preferred is lowercase) i = "pnum kMG".index(prefix) return (i - 4) * 3 -def format_metric_unit(num: str, unit: str = '', six: bool = False) -> str: +def format_metric_unit(num: str, unit: str = "", six: bool = False) -> str: "Normalizes the Metric unit on the number." num = num.strip() match = METRIC_NUMBER.match(num) @@ -41,30 +41,31 @@ def format_metric_unit(num: str, unit: str = '', six: bool = False) -> str: return num digits_str, prefix = match.group(1), match.group(2) digits_decimal = Decimal(digits_str) - digits_decimal *= Decimal('10') ** Decimal(prefix_to_exponent(prefix)) + digits_decimal *= Decimal("10") ** Decimal(prefix_to_exponent(prefix)) res = ENG_NUMBER.match(digits_decimal.to_eng_string()) if not res: raise RuntimeError - digits, exp = Decimal(res.group(1)), int(res.group(2) or '0') - assert exp % 3 == 0, 'failed to make engineering notation' + digits, exp = Decimal(res.group(1)), int(res.group(2) or "0") + assert exp % 3 == 0, "failed to make engineering notation" possibilities = [] for d_e in range(-6, 9, 3): if (exp + d_e) % 6 == 0 or not six: new_exp = exp - d_e - new_digits = str(digits * (Decimal('10') ** Decimal(d_e))) - if 'e' in new_digits.lower(): + new_digits = str(digits * (Decimal("10") ** Decimal(d_e))) + if "e" in new_digits.lower(): continue - if '.' in new_digits: - new_digits = new_digits.rstrip('0').removesuffix('.') + if "." in new_digits: + new_digits = new_digits.rstrip("0").removesuffix(".") possibilities.append((new_exp, new_digits)) # heuristic: shorter is better, prefer no decimal point - exp, digits = sorted(possibilities, key=lambda x: len( - x[1]) + (0.5 * ('.' in x[1])))[0] + exp, digits = sorted( + possibilities, key=lambda x: len(x[1]) + (0.5 * ("." in x[1])) + )[0] out = digits + " " + exponent_to_prefix(exp) + unit return out.replace(" u", " µ") -if __name__ == '__main__': +if __name__ == "__main__": print(">>", format_metric_unit("2.5", "V")) print(">>", format_metric_unit("50n", "F", True)) print(">>", format_metric_unit("1234", "Ω")) diff --git a/schemascii/utils.py b/schemascii/utils.py index 60dd48b..4b07dcd 100644 --- a/schemascii/utils.py +++ b/schemascii/utils.py @@ -8,10 +8,10 @@ from .metric import format_metric_unit from .errors import TerminalsError -Cbox = namedtuple('Cbox', 'p1 p2 type id') -BOMData = namedtuple('BOMData', 'type id data') -Flag = namedtuple('Flag', 'pt char side') -Terminal = namedtuple('Terminal', 'pt flag side') +Cbox = namedtuple("Cbox", "p1 p2 type id") +BOMData = namedtuple("BOMData", "type id data") +Flag = namedtuple("Flag", "pt char side") +Terminal = namedtuple("Terminal", "pt flag side") class Side(IntEnum): @@ -22,9 +22,14 @@ class Side(IntEnum): BOTTOM = 3 -def colinear(points: list[complex]) -> bool: +def colinear(*points: complex) -> bool: "Returns true if all the points are in the same line." - return len(set(phase(p-points[0]) for p in points[1:])) == 1 + return len(set(phase(p - points[0]) for p in points[1:])) == 1 + + +def force_int(p: complex) -> complex: + "Force the coordinates of the complex number to lie on the integer grid." + return complex(round(p.real), round(p.imag)) def sharpness_score(points: list[complex]) -> float: @@ -49,33 +54,53 @@ def intersecting(a, b, p, q): return sort_a <= sort_p <= sort_b or sort_p <= sort_b <= sort_q -# UNUSED as of yet -def merge_colinear(points: list[tuple[complex, complex]]) -> list[tuple[complex, complex]]: - "Merges line segments that are colinear." - points = list(set(points)) - out = [] - a, b = points[0] - while points: - print(points) - for pq in points[1:]: - p, q = pq - if not (colinear((a, b, p, q)) and intersecting(a, b, p, q)): +def take_next_group(links: list[tuple[complex, complex]]) -> list[tuple[complex, complex]]: + """Pops the longes possible link off of the `links` list and returns it, + mutating the input list.""" + best = [links.pop()] + while True: + for pair in links: + if best[0][0] == pair[1]: + best.insert(0, pair) + links.remove(pair) + elif best[0][0] == pair[0]: + best.insert(0, (pair[1], pair[0])) + links.remove(pair) + elif best[-1][1] == pair[0]: + best.append(pair) + links.remove(pair) + elif best[-1][1] == pair[1]: + best.append((pair[1], pair[0])) + links.remove(pair) + else: continue - points.remove(pq) - a, b = sorted((a, b, p, q), key=lambda x: x.real)[::3] break else: - out.append((a, b)) - (a, b), points = points[0], points[1:] - return out + break + return best + + +def merge_colinear(links: list[tuple[complex, complex]]): + "Merges line segments that are colinear. Mutates the input list." + i = 1 + while True: + if i == len(links): + break + elif links[i][0] == links[i][1]: + links.remove(links[i]) + elif links[i-1][1] == links[i][0] and colinear(links[i-1][0], links[i][0], links[i][1]): + links[i-1] = (links[i-1][0], links[i][1]) + links.remove(links[i]) + else: + i += 1 -def iterate_line(p1: complex, p2: complex, step: float = 1.): +def iterate_line(p1: complex, p2: complex, step: float = 1.0): "Yields complex points along a line." vec = p2 - p1 point = p1 while abs(vec) > abs(point - p1): - yield point + yield force_int(point) point += rect(step, phase(vec)) yield point @@ -87,39 +112,41 @@ def deep_transform(data, origin: complex, theta: float): if isinstance(data, list | tuple): return [deep_transform(d, origin, theta) for d in data] if isinstance(data, complex): - return (origin - + rect(data.real, theta + pi / 2) - + rect(data.imag, theta)) + return origin + rect(data.real, theta + pi / 2) + rect(data.imag, theta) try: return deep_transform(complex(data), origin, theta) except TypeError as err: - raise TypeError( - "bad type to deep_transform(): " + type(data).__name__) from err + raise TypeError("bad type to deep_transform(): " + + type(data).__name__) from err def fix_number(n: float) -> str: - """If n is an integer, remove the trailing ".0". + """If n is an integer, remove the trailing ".0". Otherwise round it to 2 digits.""" if n.is_integer(): return str(int(n)) - return str(round(n, 4)) + n = round(n, 2) + if n.is_integer(): + return str(int(n)) + return str(n) class XMLClass: def __getattr__(self, tag) -> Callable: - def mk_tag(*contents, **attrs) -> str: - out = f'<{tag} ' + def mk_tag(*contents: str, **attrs: str) -> str: + out = f"<{tag} " for k, v in attrs.items(): if v is False: continue if isinstance(v, float): v = fix_number(v) elif isinstance(v, str): - v = re.sub(r"\d+(\.\d+)", + v = re.sub(r"\b\d+(\.\d+)\b", lambda m: fix_number(float(m.group())), v) out += f'{k.removesuffix("_").replace("__", "-")}="{v}" ' - out = out.rstrip() + '>' + ''.join(contents) - return out + f'{tag}>' + out = out.rstrip() + ">" + "".join(contents) + return out + f"{tag}>" + return mk_tag @@ -127,24 +154,24 @@ def mk_tag(*contents, **attrs) -> str: del XMLClass -def polylinegon(points: list[complex], is_polygon: bool = False, **options): +def polylinegon(points: list[complex], is_polygon: bool = False, **options) -> str: "Turn the list of points into a