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 @@ + + + + + Schemascii + + + + + + +

Schemascii Playground

+

using schemascii version

+
+
+

Schemascii Source

+
+
+

CSS

+
+
+

Result

+
+

Messages

+

+        

Errors

+

+        

More Information

+

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'' + out = out.rstrip() + ">" + "".join(contents) + return out + f"" + 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 or ." scale = options["scale"] w = options["stroke_width"] c = options["stroke"] - pts = ' '.join( - f'{x.real * scale},{x.imag * scale}' - for x in points) + pts = " ".join(f"{x.real * scale},{x.imag * scale}" for x in points) if is_polygon: - return XML.polygon(points=pts, fill=c) - return XML.polyline( - points=pts, fill="transparent", stroke__width=w, stroke=c) + return XML.polygon(points=pts, fill=c, class_="filled") + return XML.polyline(points=pts, fill="transparent", stroke__width=w, stroke=c) def find_dots(points: list[tuple[complex, complex]]) -> list[complex]: "Finds all the points where there are 4 or more connecting wires." seen = {} for p1, p2 in points: + if p1 == p2: + # Skip zero-length wires + continue if p1 not in seen: seen[p1] = 1 else: @@ -156,32 +183,26 @@ def find_dots(points: list[tuple[complex, complex]]) -> list[complex]: return [pt for pt, count in seen.items() if count > 3] -def bunch_o_lines(points: list[tuple[complex, complex]], **options): - "Return a for each pair of points." - out = '' - scale = options['scale'] - w = options["stroke_width"] - c = options["stroke"] - for p1, p2 in points: - if abs(p1 - p2) == 0: - continue - out += XML.polyline( - points=f"{p1.real * scale}," - f"{p1.imag * scale} " - f"{p2.real * scale}," - f"{p2.imag * scale}", - stroke=c, - stroke__width=w) - return out +def bunch_o_lines(pairs: list[tuple[complex, complex]], **options) -> str: + "Collapse the pairs of points and return the smallest number of s." + lines = [] + while pairs: + group = take_next_group(pairs) + merge_colinear(group) + # make it a polyline + pts = [group[0][0]] + [p[1] for p in group] + lines.append(pts) + return "".join(polylinegon(line, **options) for line in lines) def id_text( - box: Cbox, - bom_data: BOMData, - terminals: list[Terminal], - unit: str | list[str] | None, - point: complex | None = None, - **options): + box: Cbox, + bom_data: BOMData, + terminals: list[Terminal], + unit: str | list[str] | None, + point: complex | None = None, + **options, +) -> str: "Format the component ID and value around the point." if options["nolabels"]: return "" @@ -198,10 +219,24 @@ def id_text( text = format_metric_unit(text, unit) classy = "cmp-value" else: - text = " ".join(format_metric_unit(x, y, six) - for x, (y, six) in zip(text.split(","), unit)) + text = " ".join( + format_metric_unit(x, y, six) + for x, (y, six) in zip(text.split(","), unit) + ) classy = "cmp-value" data = XML.tspan(text, class_=classy) + if len(terminals) > 1: + textach = ( + "start" + if ( + any(Side.BOTTOM == t.side for t in terminals) + or any(Side.TOP == t.side for t in terminals) + ) + else "middle" + ) + else: + textach = "middle" if terminals[0].side in ( + Side.TOP, Side.BOTTOM) else "start" return XML.text( (XML.tspan(f"{box.type}{box.id}", class_="cmp-id") * bool("L" in label_style)), @@ -209,12 +244,10 @@ def id_text( data * bool("V" in label_style), x=point.real, y=point.imag, - text__anchor="start" if ( - any(Side.BOTTOM == t.side for t in terminals) - or any(Side.TOP == t.side for t in terminals) - ) else "middle", + text__anchor=textach, font__size=options["scale"], - fill=options["stroke"]) + fill=options["stroke"], + ) def make_text_point(t1: complex, t2: complex, **options) -> complex: @@ -222,24 +255,26 @@ def make_text_point(t1: complex, t2: complex, **options) -> complex: quad_angle = phase(t1 - t2) + pi / 2 scale = options["scale"] text_pt = (t1 + t2) * scale / 2 - offset = rect(scale / 2, quad_angle) + offset = rect(scale / 2 * options.get("offset_scaler", 1), quad_angle) text_pt += complex(abs(offset.real), -abs(offset.imag)) return text_pt -def make_plus( - terminals: list[Terminal], - center: complex, - theta: float, - **options) -> str: +def make_plus(terminals: list[Terminal], center: complex, theta: float, **options) -> str: "Make a + sign if the terminals indicate the component is polarized." if all(t.flag != "+" for t in terminals): return "" return XML.g( - bunch_o_lines(deep_transform(deep_transform( - [(.125, -.125), (.125j, -.125j)], 0, theta), - center + deep_transform(.33+.75j, 0, theta), 0), **options), - class_="plus") + bunch_o_lines( + deep_transform( + deep_transform([(0.125, -0.125), (0.125j, -0.125j)], 0, theta), + center + deep_transform(0.33 + 0.75j, 0, theta), + 0, + ), + **options, + ), + class_="plus", + ) def arrow_points(p1: complex, p2: complex) -> list[tuple[complex, complex]]: @@ -248,59 +283,48 @@ def arrow_points(p1: complex, p2: complex) -> list[tuple[complex, complex]]: tick_len = min(0.5, abs(p2 - p1)) return [ (p2, p1), - (p2, p2 - rect(tick_len, angle + pi/5)), - (p2, p2 - rect(tick_len, angle - pi/5))] + (p2, p2 - rect(tick_len, angle + pi / 5)), + (p2, p2 - rect(tick_len, angle - pi / 5)), + ] -def make_variable( - center: complex, - theta: float, - is_variable: bool = True, - **options) -> str: +def make_variable(center: complex, theta: float, is_variable: bool = True, **options) -> str: "Draw a 'variable' arrow across the component." if not is_variable: return "" return bunch_o_lines( - deep_transform( - arrow_points(-1, 1), - center, - (theta % pi) + pi/4), - **options) - - -def light_arrows( - center: complex, - theta: float, - out: bool, - **options): + deep_transform(arrow_points(-1, 1), center, (theta % pi) + pi / 4), **options + ) + + +def light_arrows(center: complex, theta: float, out: bool, **options): """Draw arrows towards or away from the component (i.e. light-emitting or light-dependent).""" - a, b = 1j, .3+.3j + a, b = 1j, 0.3 + 0.3j if out: a, b = b, a return bunch_o_lines( - deep_transform( - arrow_points(a, b), - center, theta - pi/2), - **options) + bunch_o_lines( - deep_transform( - arrow_points(a - .5, b - .5), - center, theta - pi/2), - **options) + deep_transform(arrow_points(a, b), center, theta - pi / 2), **options + ) + bunch_o_lines( + deep_transform(arrow_points(a - 0.5, b - 0.5), center, theta - pi / 2), + **options, + ) def sort_counterclockwise(terminals: list[Terminal]) -> list[Terminal]: "Sort the terminals in counterclockwise order." partitioned = { side: list(filtered_terminals) - for side, filtered_terminals in groupby( - terminals, - lambda t: t.side)} - return list(chain( - sorted(partitioned.get(Side.LEFT, []), key=lambda t: t.pt.imag), - sorted(partitioned.get(Side.BOTTOM, []), key=lambda t: t.pt.real), - sorted(partitioned.get(Side.RIGHT, []), key=lambda t: -t.pt.imag), - sorted(partitioned.get(Side.TOP, []), key=lambda t: -t.pt.real))) + for side, filtered_terminals in groupby(terminals, lambda t: t.side) + } + return list( + chain( + sorted(partitioned.get(Side.LEFT, []), key=lambda t: t.pt.imag), + sorted(partitioned.get(Side.BOTTOM, []), key=lambda t: t.pt.real), + sorted(partitioned.get(Side.RIGHT, []), key=lambda t: -t.pt.imag), + sorted(partitioned.get(Side.TOP, []), key=lambda t: -t.pt.real), + ) + ) def is_clockwise(terminals: list[Terminal]) -> bool: @@ -322,12 +346,14 @@ def sort_for_flags(terminals: list[Terminal], box: Cbox, *flags: list[str]) -> l if len(matching_terminals) > 1: raise TerminalsError( f"Multiple terminals with the same flag {flag} " - f"on component {box.type}{box.id}") + f"on component {box.type}{box.id}" + ) if len(matching_terminals) == 0: raise TerminalsError( f"Need a terminal with the flag {flag} " - f"on component {box.type}{box.id}") - terminal, = matching_terminals + f"on component {box.type}{box.id}" + ) + (terminal,) = matching_terminals out = *out, terminal terminals.remove(terminal) return out diff --git a/schemascii/wires.py b/schemascii/wires.py index e6fe999..ef5cb94 100644 --- a/schemascii/wires.py +++ b/schemascii/wires.py @@ -1,41 +1,38 @@ from cmath import phase, rect from math import pi from .grid import Grid -from .utils import iterate_line, merge_colinear, XML, find_dots +from .utils import force_int, iterate_line, bunch_o_lines, XML, find_dots # cSpell:ignore dydx DIRECTIONS = [1, -1, 1j, -1j] -def next_in_dir( - grid: Grid, - point: complex, - dydx: complex) -> tuple[complex, complex] | None: +def next_in_dir(grid: Grid, point: complex, dydx: complex) -> tuple[complex, complex] | None: """Follows the wire starting at the point in the specified direction, until some interesting change (a corner, junction, or end). Returns the tuple (new, old).""" old_point = point match grid.get(point): - case '|' | '(' | ')': + case "|" | "(" | ")": # extend up or down if dydx in (1j, -1j): - while grid.get(point) in '-|()': + while grid.get(point) in "-|()": point += dydx - if grid.get(point) != '*': + if grid.get(point) != "*": point -= dydx else: return None # The vertical wires do not connect horizontally - case '-': + case "-": # extend sideways if dydx in (1, -1): - while grid.get(point) in '-|()': + while grid.get(point) in "-|()": point += dydx - if grid.get(point) != '*': + if grid.get(point) != "*": point -= dydx else: return None # The horizontal wires do not connect vertically - case '*': + case "*": # can extend any direction if grid.get(point + dydx) in "|()-*": point += dydx @@ -80,19 +77,16 @@ def blank_wire(grid: Grid, p1: complex, p2: complex): "Blank out the wire from p1 to p2." # Crazy math!! way = int(phase(p1 - p2) / pi % 1.0 * 2) - side = rect(1, phase(p1 - p2) + pi / 2) + side = force_int(rect(1, phase(p1 - p2) + pi / 2)) + # way: 0: Horizontal, 1: Vertical + # Don't mask out wire crosses + cross = ["|()", "-"][way] + swap = "|-"[way] for px in iterate_line(p1, p2): - old = grid.get(px) - # way: 0: Horizontal, 1: Vertical - # Don't mask out wire crosses - keep = ["|()", "-"][way] - swap = "|-"[way] - if old not in keep: - if (grid.get(px + side) in keep and - grid.get(px - side) in keep): - grid.setmask(px, swap) - else: - grid.setmask(px) + if grid.get(px + side) in cross and grid.get(px - side) in cross: + grid.setmask(px, swap) + else: + grid.setmask(px) def next_wire(grid: Grid, **options) -> str | None: @@ -103,7 +97,7 @@ def next_wire(grid: Grid, **options) -> str | None: color = options["stroke"] # Find the first wire or return None for i, line in enumerate(grid.lines): - indexes = [line.index(c) for c in '-|()*' if c in line] + indexes = [line.index(c) for c in "-|()*" if c in line] if len(indexes) > 0: line_pieces = search_wire(grid, complex(min(indexes), i)) if line_pieces: @@ -117,24 +111,19 @@ def next_wire(grid: Grid, **options) -> str | None: raise RuntimeError("0-length wire") dots = find_dots(line_pieces) return XML.g( - *( - XML.line( - x1=p1.real * scale, - y1=p1.imag * scale, - x2=p2.real * scale, - y2=p2.imag * scale, - stroke__width=stroke_width, - stroke=color) - for p1, p2 in line_pieces), + bunch_o_lines(line_pieces, **options), *( XML.circle( cx=pt.real * scale, cy=pt.imag * scale, r=2 * stroke_width, stroke="none", - fill=color) - for pt in dots), - class_="wire") + fill=color, + ) + for pt in dots + ), + class_="wire" + ) def get_wires(grid: Grid, **options) -> str: @@ -147,7 +136,7 @@ def get_wires(grid: Grid, **options) -> str: return out -if __name__ == '__main__': - xg = Grid('test_data/test_resistors.txt') +if __name__ == "__main__": + xg = Grid("test_data/test_resistors.txt") print(get_wires(xg, scale=20)) print(xg) diff --git a/schemascii_example.css b/schemascii_example.css index 0d2f62b..379ae3e 100644 --- a/schemascii_example.css +++ b/schemascii_example.css @@ -1,60 +1,59 @@ svg.schemascii { background: black; -} - -svg.schemascii .wire line { - stroke: var(--sch-color, blue); - stroke-width: 2; - stroke-linecap: round; - transition-duration: 0.2s; -} - -svg.schemascii .wire circle { - fill: var(--sch-color, blue); - transition-duration: 0.2s; -} - -svg.schemascii :is(.wire, .component):hover { - --sch-color: lime; -} - -svg.schemascii .component :is(polyline, path, line, polygon, rect, circle) { - stroke: var(--sch-color, red); - stroke-width: 2; - stroke-linecap: round; - transition-duration: 0.2s; - fill: transparent; -} - -svg.schemascii .component .plus :is(polyline, path, line) { - stroke-width: 1; -} - -svg.schemascii .component polygon { - fill: var(--sch-color, red); -} - -svg.schemascii .component text { - fill: white; - transition-duration: 0.2s; -} -svg.schemascii .component:hover text { - font-weight: bold; -} - -svg.schemascii .component tspan:is(.cmp-value, .part-num) { - opacity: 50%; + & .wire polyline { + stroke: var(--sch-color, blue); + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; + transition-duration: 0.2s; + fill: transparent; + } + & .wire circle { + fill: var(--sch-color, blue); + transition-duration: 0.2s; + } + & :is(.wire, .component):hover { + --sch-color: lime; + } + & .component :is(polyline, path, line, polygon, rect, circle):not(.filled) { + stroke: var(--sch-color, red); + stroke-width: 2; + stroke-linecap: round; + transition-duration: 0.2s; + fill: transparent; + } + & .component :is(polyline, path, line, polygon, rect, circle).filled { + fill: var(--sch-color, red); + stroke: none; + transition-duration: 0.2s; + } + & .component .plus :is(polyline, path, line) { + stroke-width: 1; + } + & .component polygon { + fill: var(--sch-color, red); + } + & .component text { + fill: white; + transition-duration: 0.2s; + } + & .component:hover text { + font-weight: bold; + } + & .component tspan:is(.cmp-value, .part-num) { + opacity: 50%; + } } @media all and (prefers-color-scheme: light) { svg.schemascii { background: white; - } - svg.schemascii .component text { - fill: black; - } - svg.schemascii :is(.wire, .component):hover { - --sch-color: lime; + & .component text { + fill: black; + } + & :is(.wire, .component):hover { + --sch-color: lime; + } } } diff --git a/scripts/cdoc.py b/scripts/docs.py similarity index 65% rename from scripts/cdoc.py rename to scripts/docs.py index 0be1791..631d7ca 100755 --- a/scripts/cdoc.py +++ b/scripts/docs.py @@ -1,19 +1,26 @@ #! /usr/bin/env python3 import re +import os from itertools import groupby from schemascii.components_render import RENDERERS -# pylint: disable=unspecified-encoding,missing-function-docstring,invalid-name,not-an-iterable -# cSpell:ignore siht etareneg redner iicsa stpircs -# cSpell:ignore mehcs daetsn detareneg yllacitamotua codc stnenopmoc +# pylint: disable=unspecified-encoding,missing-function-docstring,invalid-name +# pylint: disable=not-an-iterable +# cSpell:ignore siht etareneg redner iicsa stpircs nettirwrevo ylpmis segnahc +# cSpell:ignore mehcs daetsn detareneg yllacitamotua codc stnenopmoc lliw ruo +# cSpell:ignore sgnirtscod TOP = ("# Supported Schemascii Components\n\n\n\n| Reference Designators | Description | BOM Syntax | Supported Flags |" - "\n|:--:|:--|:--:|:--|\n") + + "".join(reversed("/stpircs nur dna\nyp.redner_stnenopmoc/iicsa" + "mehcs ni sgnirtscod eht tide ,daetsnI\n" + ".nettirwrevo eb ylpmis " + "lliw segnahc ruoY\n!TIDE TON OD\n!ELIF" + " DETARENEG YLLACITAMOTUA")) + + os.path.basename(__file__) + + "".join(reversed(".elif siht etareneg-er ot ")) + + "\n-->\n\n| Reference Designators | Description | " + + "BOM Syntax | Supported Flags |" + + "\n|:--:|:--|:--:|:--|\n") def group_components_by_func(): diff --git a/scripts/monkeypatch.py b/scripts/monkeypatch.py new file mode 100644 index 0000000..affc0fd --- /dev/null +++ b/scripts/monkeypatch.py @@ -0,0 +1,17 @@ +import sys +if "schemascii" in sys.modules: + del sys.modules["schemascii"] + +import warnings +import schemascii + +print("monkeypatching... ", end="") + +def patched(src): + with warnings.catch_warnings(record=True) as captured_warnings: + out = schemascii.render("", src) + for warn in captured_warnings: + print("warning:", warn.message) + return out + +schemascii.patched_render = patched diff --git a/scripts/release.py b/scripts/release.py index 9d67f5a..bfc6ab6 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -39,7 +39,7 @@ def writefile(file, text): re.sub(r'__version__ = "[\d.]+"', f'__version__ = "{args.version}"', init_text)) - +cmd("scripts/docs.py") cmd("python3 -m build --sdist") cmd("python3 -m build --wheel") diff --git a/scripts/web_startup.js b/scripts/web_startup.js new file mode 100644 index 0000000..db6ac5e --- /dev/null +++ b/scripts/web_startup.js @@ -0,0 +1,119 @@ +// cSpell:ignore pyodide pyproject +var pyodide; +var console = document.getElementById("console"); +var errors = document.getElementById("errors"); +var css_box = document.getElementById("css"); +var source = document.getElementById("schemascii"); +var download_button = document.getElementById("download"); +var ver_switcher = document.getElementById("version"); +var style_elem = document.getElementById("custom-css"); +var output = document.getElementById("output"); + +var schemascii; +var monkeysrc; + +var ver_map; + +async function main() { + try { + info("Loading Python... "); + pyodide = await loadPyodide({ stdout: info, stderr: error }); + info("done\nInstalling micropip..."); + await pyodide.loadPackage("micropip", { errorCallback: error, messageCallback: () => { } }); + info("done\nFetching versions... "); + monkeysrc = await fetch("scripts/monkeypatch.py").then(r => r.text()); + var foo = await fetch("https://api.github.com/repos/dragoncoder047/schemascii/contents/dist").then(r => r.json()); + foo = foo.filter(x => x.name.endsWith(".whl")).map(x => x.path); + ver_map = Object.fromEntries(foo.map(x => [/\/schemascii-([\d.]+)-/.exec(x)[1], x])) + var all_versions = Object.keys(ver_map); + //all_versions.push("DEV"); + for (var v of all_versions) { + var o = document.createElement("option"); + o.textContent = o.value = v; + ver_switcher.append(o); + } + var latest_version = await fetch("pyproject.toml").then(r => r.text()).then(r => /version = "([\d.]+)"/.exec(r)[1]); + ver_switcher.value = latest_version; + info(`["${all_versions.join('", "')}"]\nlatest=${latest_version}\n`); + await switch_version(); + + css_box.addEventListener("input", debounce(sync_css)); + source.addEventListener("input", debounce(catched(render))); + download_button.addEventListener("click", download); + ver_switcher.addEventListener("change", acatched(switch_version)); + + css_box.value = await fetch("schemascii_example.css").then(r => r.text()); + sync_css(); + source.removeAttribute("disabled"); + css_box.removeAttribute("disabled"); + console.textContent = "Ready"; + } catch (e) { + error(`\nFATAL ERROR:\n${e.stack}\n`); + throw e; + } +} +function monkeypatch() { + pyodide.runPython(monkeysrc); +} +function info(line) { + console.textContent += line; +} +function error(text) { + errors.textContent += text; +} +function debounce(fun) { + var timeout; + return function () { + if (timeout) clearTimeout(timeout); + timeout = setTimeout(fun.bind(this, arguments), 100); + }; +} +function catched(fun) { + return function () { + try { + fun.call(this, arguments); + } catch (e) { + error(e.stack); + } + }; +} +async function acatched(fun) { + return async function() { + try { + await fun.call(this, arguments); + } catch (e) { + error(e.stack); + } + }; +} +function sync_css() { + style_elem.innerHTML = css_box.value; +} +function render() { + console.textContent = ""; + errors.textContent = ""; + output.innerHTML = schemascii.patched_render(source.value); +} + +async function switch_version() { + source.setAttribute("disabled", true); + info("Installing Schemascii version " + ver_switcher.value + "... ") + await pyodide.pyimport("micropip").install(ver_map[ver_switcher.value]); + monkeypatch(); + schemascii = pyodide.runPython("import schemascii\nschemascii"); + info("done\n"); + output.innerHTML = ""; + source.removeAttribute("disabled"); +} + +function download() { + if (!output.innerHTML) return; + var a = document.createElement("a"); + a.setAttribute("href", URL.createObjectURL(new Blob([output.innerHTML], {"type": "application/svg+xml"}))); + a.setAttribute("download", `schemascii_playground_${new Date().toISOString()}_no_css.svg`); + a.click(); +} + +main(); + +// fetch("https://github.com/dragoncoder047/schemascii/zipball/main/").then(r => r.arrayBuffer()).then(b => pyodide.unpackArchive(b, "zip")); diff --git a/supported-components.md b/supported-components.md index 46270e0..7138a45 100644 --- a/supported-components.md +++ b/supported-components.md @@ -3,7 +3,9 @@ | Reference Designators | Description | BOM Syntax | Supported Flags | @@ -14,5 +16,7 @@ Instead, edit schemascii/components_render.py and run scripts/cdoc.py to re-gene | `B`, `BT`, `BAT` | Draw a battery cell. | `volts[,amp-hours]` | `+` = positive | | `D`, `LED`, `CR`, `IR` | Draw a diode or LED. | `part-number` | `+` = positive | | `U`, `IC` | Draw an IC. | `part-number[,pin1-label[,pin2-label[,...]]]` | | -| `J`, `P` | Draw a jack connector or plug. | `label` | | +| `J`, `P` | Draw a jack connector or plug. | `label[,{circle/input/output}]` | | | `Q`, `MOSFET`, `MOS`, `FET` | Draw a bipolar transistor (PNP/NPN) or FET (NFET/PFET). | `{npn/pnp/nfet/pfet}:part-number` | `s` = source
`d` = drain
`g` = gate
`e` = emitter
`c` = collector
`b` = base | +| `G`, `GND` | Draw a ground symbol. | `[{earth/chassis/signal/common}]` | | +| `S`, `SW`, `PB` | Draw a mechanical switch symbol. | `{nc/no}[m][:label]` | | diff --git a/test_data/all_gates_3t.txt b/test_data/all_gates_3t.txt new file mode 100644 index 0000000..054f985 --- /dev/null +++ b/test_data/all_gates_3t.txt @@ -0,0 +1,67 @@ +Q:nfet: R:1k !label=V!!padding=30! +J1:A J2:B J4:VCC,circle J5:gnd,circle +J1 J2 J4 J5 +| | | | +| | *-R-----*----------J1001 J1001:NAND +| | | | d +*----------------gQ +| | | | s +| | | | d +| *-------------gQ +| | | | s +| | | *--* +| | | | +| | *-R-----*---*------J1002 J1002:NOR +| | | | d d +*----------------gQ *gQ +| | | | s | s +| | | *--*-|-* +| *-----------|----* +| | | | +| | *-R---------*------J1003 J1003:XNOR +| | | | | +| | | | d-* +*----------------*-gQ | +| | | | | s | +| *--------------*-* | +| | | | || | +| | | | || d-* +| | | | |*gQ +| | | | | s +| | | | *--* +| | | | +| | *-R-------*--------J1004 J1004:AND +| | | | d +| | *-R-----*gQ +| | | | d s +*----------------gQ | +| | | | s | +| | | | d | +| *-------------gQ | +| | | | s | +| | | *--*-* +| | | | +| | *-R-----------*----J1005 J1005:OR +| | | | d +| | *-R-----*---*gQ +| | | | d d s +*----------------gQ *gQ | +| | | | s | s | +| | | *--*-|-*-* +| *-----------|----* +| | | | +| | *-R-----------*----J1006 J1006:XOR +| | | | | +| | *-R---------* | +| | | | | | +| | | | d-* | +*----------------*-gQ | | +| | | | | s | | +| *--------------*-* | | +| | | | || | d +| | | | || d-*gQ +| | | | |*gQ s +| | | | | s | +| | | | *--* | +| | | *--------* +| | | | diff --git a/test_data/test1.txt b/test_data/test1.txt index 8c9080e..a07ca88 100644 --- a/test_data/test1.txt +++ b/test_data/test1.txt @@ -1,6 +1,5 @@ -!padding=30! -----------L1------ - -------L2###########------- -L1:220u -L2:330u \ No newline at end of file +---------------------* + | +---------------------|------ + | +---------------------* diff --git a/test_data/test_charge_pump.txt.svg b/test_data/test_charge_pump.txt.svg index 5d945b7..128baf2 100644 --- a/test_data/test_charge_pump.txt.svg +++ b/test_data/test_charge_pump.txt.svg @@ -1 +1 @@ -NE555U176215348BAT1 5 VR1 10 kΩC2 10 µFD2 1N4001J1 -5VR2 100 kΩC1 0.01 µFC3 100 µFC4 10 pFD1 1N4001J2 GND \ No newline at end of file +NE555U176215348BAT1 5 VR1 10 kΩC2 10 µFD2 1N4001J1 -5VR2 100 kΩC1 0.01 µFC3 100 µFC4 10 pFD1 1N4001J2 GND \ No newline at end of file diff --git a/test_data/test_switches.txt b/test_data/test_switches.txt new file mode 100644 index 0000000..03a1ecd --- /dev/null +++ b/test_data/test_switches.txt @@ -0,0 +1,6 @@ +!padding=30! +*------S1-------S2--------S3-----S4-- +| S1:nc S2:no S3:ncm S4:nom +S5 S5:nc:LIMIT +| +| \ No newline at end of file pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy