diff --git a/.vscode/settings.json b/.vscode/settings.json index 898ba0c..2157f82 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,15 @@ { "cSpell.words": [ "Cbox", + "MOSFET", + "NFET", + "PFET", "polylinegon", "rendec", "schemascii", "tspan" - ] + ], + "python.linting.pylintEnabled": false, + "python.linting.flake8Enabled": false, + "python.linting.enabled": true } \ No newline at end of file diff --git a/designators.md b/designators.md index edd5fef..cc347da 100644 --- a/designators.md +++ b/designators.md @@ -1,71 +1,75 @@ +# Possible Designators + (copied from and edited lightly) -| Designator | Component type | Implemented? | -|:--:|:--|:--:| -| A, ASSY | Separable assembly or sub-assembly || -| AE | Aerial, antenna || -| AT | Attenuator or isolator || -| B, BT, BAT | Battery | Yes | -| BR | Bridge rectifier || -| C | Capacitor | Yes | -| CV, VC | Variable capacitor | Yes | -| CN | Connector || -| CRT | Cathode ray tube || -| D, LED, CR | Diode (all types, including LED), thyristor | Yes | -| DL | Delay line || -| DS | Display, general light source, lamp, signal light || -| DSP | Digital signal processor || -| F | Fuse || -| FB | Ferrite bead || -| FD | Fiducial || -| FET | Field-effect transistor || -| FL | Filter || -| G | Generator or oscillator || -| GDT, SVP | Gas discharge tube, Surge Voltage Protector || -| GN | General network || -| H | Pin header || -| HY | Circulator or directional coupler || -| IC, U | Integrated circuit | Partial | -| IR | Infrared Diode | Yes | -| J | Jack (least-movable connector of a connector pair) || -| J, JW | Wire link ("jumper") || -| JFET | Junction gate field-effect transistor || -| JP | Jumper (Link) || -| K, RY, RLA | Relay or contactor || -| L | Inductor or coil or ferrite bead || -| LA | Lightning arrester || -| LCD | Liquid crystal display || -| LDR | Light-dependent resistor || -| LS | Loudspeaker or buzzer || -| M | Motor || -| MCB | Miniature circuit breaker || -| MIC, MK | Microphone || -| MOSFET | Metal-oxide-semiconductor field-effect transistor || -| MP | Mechanical part (including screws and fasteners) || -| NE | Neon lamp || -| OP | Opto-isolator || -| P | Plug (most-movable connector of a connector pair) || -| PCB | Printed circuit board || -| PLC | Programmable logic controller || -| PS | Power supply || -| PU | Pickup || -| Q | Transistor (all types) || -| R | Resistor | Yes | -| RV, VR | Variable resistor (potentiometer or rheostat) | Yes | -| RN | Resistor network || -| RT | Thermistor || -| S, SW | Switch (all types, including buttons) || -| SCR | Silicon-controlled rectifier || -| SUS | Silicon unilateral switch || -| T | Transformer || -| TC | Thermocouple || -| TFT | Thin-film transistor (display) || -| TH | Thermistor || -| TP | Test point || -| TUN | Tuner || -| V | Vacuum tube || -| VDR, MOV | Voltage-dependent resistor (varistor) || -| VFD | Vacuum fluorescent display || -| VT | Voltage transformer || -| W | Wire | (implicit) | -| X, XTAL, Y | Crystal oscillator, ceramic resonator || +This is a list of all components that Schemascii *might* support. For a complete list of all supported components, (generated from the implementation file), please see [supported-components.md](./supported-components.md). If a component you want is not supported, have a look at [#3](https://github.com/dragoncoder047/schemascii/issues/3) or fork and implement it yourself. + +| Designator | Component type | +|:--:|:--| +| A, ASSY | Separable assembly or sub-assembly | +| AE | Aerial, antenna | +| AT | Attenuator or isolator | +| B, BT, BAT | Battery | +| BR | Bridge rectifier | +| C | Capacitor | +| CV, VC | Variable capacitor | +| CN | Connector | +| CRT | Cathode ray tube | +| D, LED, CR | Diode (all types, including LED), thyristor | +| DL | Delay line | +| DS | Display, general light source, lamp, signal light | +| DSP | Digital signal processor | +| F | Fuse | +| FB | Ferrite bead | +| FD | Fiducial | +| FET | Field-effect transistor | +| FL | Filter | +| G | Generator or oscillator | +| GDT, SVP | Gas discharge tube, Surge Voltage Protector | +| GN | General network | +| H | Pin header | +| HY | Circulator or directional coupler | +| IC, U | Integrated circuit | +| IR | Infrared Diode | +| J | Jack (least-movable connector of a connector pair) | +| J, JW | Wire link ("jumper") | +| JFET | Junction gate field-effect transistor | +| JP | Jumper (Link) | +| K, RY, RLA | Relay or contactor | +| L | Inductor or coil or ferrite bead | +| LA | Lightning arrester | +| LCD | Liquid crystal display | +| LDR | Light-dependent resistor | +| LS | Loudspeaker or buzzer | +| M | Motor | +| MCB | Miniature circuit breaker | +| MIC, MK | Microphone | +| MOSFET | Metal-oxide-semiconductor field-effect transistor | +| MP | Mechanical part (including screws and fasteners) | +| NE | Neon lamp | +| OP | Opto-isolator | +| P | Plug (most-movable connector of a connector pair) | +| PCB | Printed circuit board | +| PLC | Programmable logic controller | +| PS | Power supply | +| PU | Pickup | +| Q | Transistor (all types) | +| R | Resistor | +| RV, VR | Variable resistor (potentiometer or rheostat) | +| RN | Resistor network | +| RT | Thermistor | +| S, SW | Switch (all types, including buttons) | +| SCR | Silicon-controlled rectifier | +| SUS | Silicon unilateral switch | +| T | Transformer | +| TC | Thermocouple | +| TFT | Thin-film transistor (display) | +| TH | Thermistor | +| TP | Test point | +| TUN | Tuner | +| V | Vacuum tube | +| VDR, MOV | Voltage-dependent resistor (varistor) | +| VFD | Vacuum fluorescent display | +| VT | Voltage transformer | +| W | Wire | +| X, XTAL, Y | Crystal oscillator, ceramic resonator | diff --git a/dist/schemascii-0.2.1-py3-none-any.whl b/dist/schemascii-0.2.1-py3-none-any.whl new file mode 100644 index 0000000..20eff88 Binary files /dev/null and b/dist/schemascii-0.2.1-py3-none-any.whl differ diff --git a/dist/schemascii-0.2.1.tar.gz b/dist/schemascii-0.2.1.tar.gz new file mode 100644 index 0000000..d7768e7 Binary files /dev/null and b/dist/schemascii-0.2.1.tar.gz differ diff --git a/dist/schemascii-0.2.2-py3-none-any.whl b/dist/schemascii-0.2.2-py3-none-any.whl new file mode 100644 index 0000000..1185ff4 Binary files /dev/null and b/dist/schemascii-0.2.2-py3-none-any.whl differ diff --git a/dist/schemascii-0.2.2.tar.gz b/dist/schemascii-0.2.2.tar.gz new file mode 100644 index 0000000..386e456 Binary files /dev/null and b/dist/schemascii-0.2.2.tar.gz differ 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/format.md b/format.md index 3046a91..ca749b0 100644 --- a/format.md +++ b/format.md @@ -51,12 +51,12 @@ Components are able to accept "flags", which are other punctuation characters an To include component values, pin numbers, etc, somewhere in the drawing but not touching any wires or other components, you can write component values - these are formatted as the reference designator (same as in circuit) followed by a `:` and the value parameter (which stops at the first whitespace). *I usually put these in a "BOM section" below the circuit itself but you could also put them right next to the component.* -For simple components, this is usually just a value rating, but *without* the units (only the Metric prefix). For more specific components (mostly semiconductor devices) this is usually the part number. +For simple components, this is usually just a value rating, but *without* the units (only the Metric prefix). For more specific components (mostly semiconductor devices) this is usually the part number and/or some string to determine how to draw the component. Examples: * `C33:2.2u` -- "2.2 µF" -* `Q1001:TIP102` -- just the part number; printed verbatim +* `Q1001:pnp:TIP102` -- "pnp", "npn", "pfet", or "nfet" to determine what kind of transistor, plus the part number; printed verbatim * `L51:0.33` -- this is rewritten to "330 mH" so that it has no decimal point. * `F3:1500m` -- rewritten: "1.5 A" * `D7:1N4001` -- again, part number 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 5743437..91486a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "schemascii" -version = "0.2.0" +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 9206c36..96d7308 100644 --- a/schemascii/__init__.py +++ b/schemascii/__init__.py @@ -8,7 +8,7 @@ from .utils import XML from .errors import * -__version__ = "0.2.0" +__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 c10a18c..46fd3bf 100644 --- a/schemascii/components_render.py +++ b/schemascii/components_render.py @@ -2,93 +2,108 @@ from cmath import phase, rect from math import pi from warnings import warn -from .utils import (Cbox, Terminal, BOMData, XML, Side, - polylinegon, id_text, make_text_point, - bunch_o_lines, deep_transform, make_plus, make_variable, - sort_counterclockwise, light_arrows) +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 + RENDERERS = {} 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 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 -def resistor( - box: Cbox, - terminals: list[Terminal], - bom_data: BOMData, - **options): - "Draw a resistor" +@component("R", "RV", "VR") +@n_terminal(2) +@no_ambiguous +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 vec = t1 - t2 mid = (t1 + t2) / 2 @@ -97,104 +112,162 @@ 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) - text_pt = make_text_point(t1, t2, **options) - return (polylinegon(points, **options) - + make_variable(mid, angle, "V" in box.type, **options) - + id_text( - box, bom_data, terminals, (("Ω", False), ("W", False)), - text_pt, **options)) - - -# Register it -component("R", "RV", "VR")(n_terminal(2)(no_ambiguous(resistor))) + 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): - "Draw a capacitor" +def capacitor(box: Cbox, terminals: list[Terminal], bom_data: BOMData, **options): + """Draw a capacitor, variable capacitor, etc. + bom:farads[,volts] + flags:+=positive""" t1, t2 = terminals[0].pt, terminals[1].pt 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) - text_pt = make_text_point(t1, t2, **options) - 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)), - text_pt, **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): + """Draw an inductor (coil, choke, etc) + bom:henries""" + t1, t2 = terminals[0].pt, terminals[1].pt + vec = t1 - t2 + mid = (t1 + t2) / 2 + length = abs(vec) + angle = phase(vec) + scale = options["scale"] + 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"], + ) + + make_variable(mid, angle, "V" in box.type, **options) + + id_text( + 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): - "Draw a battery cell" +def battery(box: Cbox, terminals: list[Terminal], bom_data: BOMData, **options): + """Draw a battery cell. + bom:volts[,amp-hours] + flags:+=positive""" t1, t2 = terminals[0].pt, terminals[1].pt 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) - text_pt = make_text_point(t1, t2, **options) - return (id_text( - box, bom_data, terminals, (("V", False), ("Ah", False)), - text_pt, **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): - "Draw a diode or LED" +def diode(box: Cbox, terminals: list[Terminal], bom_data: BOMData, **options): + """Draw a diode or LED. + bom:part-number + flags:+=positive""" t1, t2 = terminals[0].pt, terminals[1].pt 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) - text_pt = make_text_point(t1, t2, **options) - light_emitting = "LED", "IR" - return ((light_arrows(mid, angle, True, **options) - if box.type in light_emitting else "") - + id_text(box, bom_data, terminals, None, text_pt, **options) - + bunch_o_lines(lines, **options) - + polylinegon(triangle, True, **options)) + (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) + ) SIDE_TO_ANGLE_MAP = { Side.RIGHT: pi, - Side.TOP: pi / 2, + Side.TOP: pi / 2, Side.LEFT: 0, Side.BOTTOM: 3 * pi / 2, } @@ -203,11 +276,10 @@ def diode( @component("U", "IC") @no_ambiguous def integrated_circuit( - box: Cbox, - terminals: list[Terminal], - bom_data: BOMData, - **options): - "Draw an IC" + box: Cbox, terminals: list[Terminal], bom_data: BOMData, **options +): + """Draw an IC. + bom:part-number[,pin1-label[,pin2-label[,...]]]""" label_style = options["label"] scale = options["scale"] sz = (box.p2 - box.p1) * scale @@ -220,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"), @@ -233,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( @@ -242,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 @@ -250,45 +324,227 @@ 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): - "Draw a jack connector or plug" +def jack(box: Cbox, terminals: list[Terminal], bom_data: BOMData, **options): + """Draw a jack connector or plug. + 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): + """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(":") + 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: + ae, se, ctl = sort_for_flags(terminals, box, "s", "d", "g") + else: + ae, se, ctl = sort_for_flags(terminals, box, "e", "c", "b") + ap, sp = ae.pt, se.pt + diff = sp - ap + if diff.real == 0: + mid = complex(ap.real, ctl.pt.imag) + elif diff.imag == 0: + mid = complex(ctl.pt.real, ap.imag) + else: + # 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 = 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(0.8, theta)), # Lead in + (sp, mid - rect(0.8, theta)), # Lead out + ] + 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(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(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(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) + + +@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 @@ -301,34 +557,28 @@ def jack( # + 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', - # inductor style 1 (humps) - 'L': 'M0-1A.1.1 0 010-.6.1.1 0 010-.2.1.1 0 010 .2.1.1 0 010 .6.1.1 0 010 1 .1.1 0 000 .6.1.1 0 000 .2.1.1 0 000-.2.1.1 0 000-.6.1.1 0 000-1Z', - # inductor style 2 (coil) - # 'L': 'M0-1C1-1 1-.2 0-.2S-1-.8 0-.8 1 0 0 0-1-.6 0-.6 1 .2 0 .2-1-.4 0-.4 1 .4 0 .4-1-.2 0-.2 1 .6 0 .6-1 0 0 0 1 .8 0 .8-1 .2 0 .2 1 1 0 1C1 1 1 .2 0 .2S-1 .8 0 .8 1 0 0 0-1 .6 0 .6 1-.2 0-.2-1 .4 0 .4 1-.4 0-.4-1 .2 0 .2 1-.6 0-.6-1 0 0 0 1-.8 0-.8-1-.2 0-.2 1-1 0-1Z', + "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 9b3855c..a37d629 100644 --- a/schemascii/inline_config.py +++ b/schemascii/inline_config.py @@ -5,6 +5,7 @@ def get_inline_configs(grid: Grid) -> dict: + "Extract all the inline config options into a dict and blank them out." out = {} for y, line in enumerate(grid.lines): for m in INLINE_CONFIG_RE.finditer(line): @@ -21,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 8f1a713..4b07dcd 100644 --- a/schemascii/utils.py +++ b/schemascii/utils.py @@ -4,13 +4,14 @@ from math import pi from cmath import phase, rect from typing import Callable -from .metric import format_metric_unit import re +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): @@ -21,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: @@ -48,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 @@ -86,38 +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+)", lambda m: fix_number(float(m.group())), v) + 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 @@ -125,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: @@ -154,30 +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: - 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 "" @@ -194,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)), @@ -205,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: @@ -218,82 +255,105 @@ 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]]: "Return points to make an arrow from p1 pointing to p2." angle = phase(p2 - p1) - tick_len = min(0.25, abs(p2 - p1)) + 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[Side.LEFT], key=lambda t: t.pt.imag), - sorted(partitioned[Side.BOTTOM], key=lambda t: t.pt.real), - sorted(partitioned[Side.RIGHT], key=lambda t: -t.pt.imag), - sorted(partitioned[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: + "Return true if the terminals are clockwise order." + sort = sort_counterclockwise(terminals) + for _ in range(len(sort)): + if sort == terminals: + return True + sort = sort[1:] + [sort[0]] + return False + + +def sort_for_flags(terminals: list[Terminal], box: Cbox, *flags: list[str]) -> list[Terminal]: + """Sorts out the terminals in the specified order using the flags. + Raises and error if the flags are absent.""" + out = () + for flag in flags: + matching_terminals = list(filter(lambda t: t.flag == flag, terminals)) + if len(matching_terminals) > 1: + raise TerminalsError( + f"Multiple terminals with the same flag {flag} " + 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 + 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/docs.py b/scripts/docs.py new file mode 100755 index 0000000..631d7ca --- /dev/null +++ b/scripts/docs.py @@ -0,0 +1,60 @@ +#! /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 +# 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") + + +def group_components_by_func(): + items = groupby(list(RENDERERS.items()), lambda x: x[1]) + out = {} + for x, g in items: + out[x] = [p[0] for p in g] + return out + + +def parse_docstring(d): + out = [None, None, None] + if fs := re.search(r"flags:(.*?)$", d, re.M): + out[2] = [f.split("=") for f in fs.group(1).split(",")] + d = d.replace(fs.group(), "") + if b := re.search(r"bom:(.*?)$", d, re.M): + out[1] = b.group(1) + d = d.replace(b.group(), "") + out[0] = d.strip() + return out + + +def main(): + content = TOP + for func, rds in group_components_by_func().items(): + data = parse_docstring(func.__doc__) + content += "| " + ", ".join(f"`{x}`" for x in rds) + " | " + content += data[0].replace("\n", "
") + " | " + content += "`" + data[1] + "` | " + content += "
".join(f"`{x[0]}` = {x[1]}" for x in (data[2] or [])) + content += " |\n" + with open("supported-components.md", "w") as f: + f.write(content) + + +if __name__ == '__main__': + main() 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/release.py b/scripts/release.py similarity index 98% rename from release.py rename to scripts/release.py index 9d67f5a..bfc6ab6 100755 --- a/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 new file mode 100644 index 0000000..7138a45 --- /dev/null +++ b/supported-components.md @@ -0,0 +1,22 @@ +# Supported Schemascii Components + + + +| Reference Designators | Description | BOM Syntax | Supported Flags | +|:--:|:--|:--:|:--| +| `R`, `RV`, `VR` | Resistor, Variable resistor, etc. | `ohms[,watts]` | | +| `C`, `CV`, `VC` | Draw a capacitor, variable capacitor, etc. | `farads[,volts]` | `+` = positive | +| `L`, `VL`, `LV` | Draw an inductor (coil, choke, etc) | `henries` | | +| `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[,{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 0b036fe..a07ca88 100644 --- a/test_data/test1.txt +++ b/test_data/test1.txt @@ -1,4 +1,5 @@ ------LED1+----- -LED1:red ------VR1###------ -VR1:10k \ 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