From 74e6d760bc6bff6a8325c4cfceafd64faf209dc9 Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Mon, 20 Mar 2023 17:14:10 -0400 Subject: [PATCH 01/46] fix bug with dropping leading zeros on flags --- .vscode/settings.json | 5 ++++- schemascii/components_render.py | 2 +- schemascii/utils.py | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 883f778..e18146e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,8 @@ "rendec", "schemascii", "tspan" - ] + ], + "python.linting.pylintEnabled": false, + "python.linting.flake8Enabled": true, + "python.linting.enabled": true } \ No newline at end of file diff --git a/schemascii/components_render.py b/schemascii/components_render.py index 7142bf0..bef8d5b 100644 --- a/schemascii/components_render.py +++ b/schemascii/components_render.py @@ -162,7 +162,7 @@ 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}" + data += f"a1 1 0 01 {-dxdy.real} {dxdy.imag}" return (XML.path( d=data, stroke=options["stroke"], diff --git a/schemascii/utils.py b/schemascii/utils.py index 60dd48b..610c3c8 100644 --- a/schemascii/utils.py +++ b/schemascii/utils.py @@ -98,11 +98,11 @@ def deep_transform(data, origin: complex, theta: float): 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)) + return str(round(n, 2)) class XMLClass: From 1b24080b93d8a72e479caa97faf02aed534ec223 Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Thu, 23 Mar 2023 16:11:56 -0400 Subject: [PATCH 02/46] Update and rename cdoc.py to documentation.py --- scripts/{cdoc.py => documentation.py} | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) rename scripts/{cdoc.py => documentation.py} (80%) diff --git a/scripts/cdoc.py b/scripts/documentation.py similarity index 80% rename from scripts/cdoc.py rename to scripts/documentation.py index 0be1791..4975038 100755 --- a/scripts/cdoc.py +++ b/scripts/documentation.py @@ -4,13 +4,15 @@ 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 +# cSpell:ignore siht etareneg redner iicsa stpircs nettirwrevo ylpmis segnahc +# cSpell:ignore mehcs daetsn detareneg yllacitamotua codc stnenopmoc lliw ruo TOP = ("# Supported Schemascii Components\n\n\n\n| Reference Designators | Description | BOM Syntax | Supported Flags |" "\n|:--:|:--|:--:|:--|\n") From e06fad255b5d89db66bf0a1a19ee81fec536b638 Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Fri, 24 Mar 2023 14:24:35 +0000 Subject: [PATCH 03/46] change docs.py --- scripts/{documentation.py => docs.py} | 21 +++++++++++++-------- supported-components.md | 4 +++- 2 files changed, 16 insertions(+), 9 deletions(-) rename scripts/{documentation.py => docs.py} (72%) diff --git a/scripts/documentation.py b/scripts/docs.py similarity index 72% rename from scripts/documentation.py rename to scripts/docs.py index 4975038..631d7ca 100755 --- a/scripts/documentation.py +++ b/scripts/docs.py @@ -1,21 +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 +# 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") + + "\n-->\n\n| Reference Designators | Description | " + + "BOM Syntax | Supported Flags |" + + "\n|:--:|:--|:--:|:--|\n") def group_components_by_func(): diff --git a/supported-components.md b/supported-components.md index 46270e0..cd01f0a 100644 --- a/supported-components.md +++ b/supported-components.md @@ -3,7 +3,9 @@ | Reference Designators | Description | BOM Syntax | Supported Flags | From 44df3c9840c769448c7cd5c63440e610077a8899 Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Fri, 24 Mar 2023 15:15:24 +0000 Subject: [PATCH 04/46] allow component 0 implicit, alt glyphs for jack --- schemascii/components.py | 7 ++-- schemascii/components_render.py | 59 ++++++++++++++++++++------------- supported-components.md | 2 +- 3 files changed, 41 insertions(+), 27 deletions(-) diff --git a/schemascii/components.py b/schemascii/components.py index 542bcbd..d7a1c37 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,13 +14,14 @@ 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:])) + ident, m.group(3)[1:])) else: components.append(Cbox(complex(m.start(), i), complex(m.end() - 1, i), - m.group(1), m.group(2))) + m.group(1), ident)) for z in range(*m.span(0)): grid.setmask(complex(z, i)) return components, boms diff --git a/schemascii/components_render.py b/schemascii/components_render.py index bef8d5b..c2974e0 100644 --- a/schemascii/components_render.py +++ b/schemascii/components_render.py @@ -5,7 +5,8 @@ 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) + sort_counterclockwise, light_arrows, sort_for_flags, + is_clockwise) from .errors import TerminalsError, BOMError, UnsupportedComponentError # pylint: disable=unbalanced-tuple-unpacking @@ -309,26 +310,35 @@ def jack( 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]) + 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, 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 ( - 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") + bunch_o_lines(arrow_points(t1, t2), **options) + id_text(box, bom_data, terminals, None, sc_text_pt, **options)) @@ -343,7 +353,8 @@ def transistor( """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:")): + 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(":") @@ -380,7 +391,8 @@ def transistor( arr = arr[1], arr[0] out_lines.extend([ *arrow_points(*arr), - (mid - rect(.8, theta), mid - rect(.8, theta) + rect(.7, thetaquarter)), + (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), @@ -393,7 +405,8 @@ def transistor( arr = arr[1], arr[0] out_lines.extend([ *arrow_points(*arr), - (mid - rect(.8, theta), mid - rect(.4, theta) + rect(1, thetaquarter)), + (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)), ]) @@ -412,7 +425,7 @@ 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', # jumper pads diff --git a/supported-components.md b/supported-components.md index cd01f0a..e76a3ab 100644 --- a/supported-components.md +++ b/supported-components.md @@ -16,5 +16,5 @@ and run scripts/docs.py to re-generate this file. | `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 | From 3aec23e6c1be188f4c3ec22d34230801ac5c34d8 Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Fri, 24 Mar 2023 16:44:20 +0000 Subject: [PATCH 05/46] some better jack renderind --- schemascii/components_render.py | 2 +- schemascii/edgemarks.py | 9 +---- schemascii/utils.py | 16 ++++++-- scripts/release.py | 2 +- test_data/all_gates_3t.txt | 67 +++++++++++++++++++++++++++++++++ 5 files changed, 83 insertions(+), 13 deletions(-) create mode 100644 test_data/all_gates_3t.txt diff --git a/schemascii/components_render.py b/schemascii/components_render.py index c2974e0..50290b6 100644 --- a/schemascii/components_render.py +++ b/schemascii/components_render.py @@ -315,7 +315,7 @@ def jack( 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, SIDE_TO_ANGLE_MAP[terminals[0].side]) + 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) diff --git a/schemascii/edgemarks.py b/schemascii/edgemarks.py index aea494e..3bdbf4f 100644 --- a/schemascii/edgemarks.py +++ b/schemascii/edgemarks.py @@ -37,13 +37,8 @@ def flags(p: complex, s: Side) -> Flag | None: c = grid.get(p) 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 diff --git a/schemascii/utils.py b/schemascii/utils.py index 610c3c8..c96b486 100644 --- a/schemascii/utils.py +++ b/schemascii/utils.py @@ -145,6 +145,9 @@ 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: @@ -202,6 +205,14 @@ def id_text( 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,10 +220,7 @@ 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"]) 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/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 | +| | | | *--* | +| | | *--------* +| | | | From 18178a7ac07f513883337eb1c9ff3a470ddea6ef Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Tue, 28 Mar 2023 16:22:53 +0000 Subject: [PATCH 06/46] add ground --- schemascii/components_render.py | 36 +++++++++++++++++++++++++++++++++ test_data/test1.txt | 10 ++++++--- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/schemascii/components_render.py b/schemascii/components_render.py index 50290b6..2c88395 100644 --- a/schemascii/components_render.py +++ b/schemascii/components_render.py @@ -415,6 +415,42 @@ def transistor( 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), (-.5+1j, .5+1j)] + match icon_type: + case "earth": + points += [ + (-.33+1.25j, .33+1.25j), + (-.16+1.5j, .16+1.5j)] + case "chassis": + points += [ + (-.5+1j, -.25+1.5j), + (1j, .25+1.5j), + (.5+1j, .75+1.5j)] + case "signal": + points += [ + (-.5+1j, 1.5j), + (.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) + + # code for drawing stuff # https://github.com/pfalstad/circuitjs1/tree/master/src/com/lushprojects/circuitjs1/client # https://github.com/KenKundert/svg_schematic/blob/0abb5dc/svg_schematic.py diff --git a/test_data/test1.txt b/test_data/test1.txt index 8c9080e..71f203b 100644 --- a/test_data/test1.txt +++ b/test_data/test1.txt @@ -1,6 +1,10 @@ !padding=30! -----------L1------ +G3----------L1------G4 -------L2###########------- +G2------L2###########-------G1 L1:220u -L2:330u \ No newline at end of file +L2:330u +G1:earth +G2:chassis +G3:signal +G4:common From 3bfd9257a1bf76a3b149e837d6f453745d6ca803 Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Wed, 24 May 2023 11:55:48 +0000 Subject: [PATCH 07/46] formatter --- .vscode/settings.json | 2 +- schemascii/__init__.py | 48 ++-- schemascii/__main__.py | 26 +- schemascii/components.py | 56 ++-- schemascii/components_render.py | 446 +++++++++++++++++--------------- schemascii/configs.py | 44 ++-- schemascii/edgemarks.py | 38 ++- schemascii/grid.py | 31 ++- schemascii/inline_config.py | 10 +- schemascii/metric.py | 29 ++- schemascii/utils.py | 182 ++++++------- schemascii/wires.py | 43 +-- 12 files changed, 521 insertions(+), 434 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index e18146e..2157f82 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,6 @@ "tspan" ], "python.linting.pylintEnabled": false, - "python.linting.flake8Enabled": true, + "python.linting.flake8Enabled": false, "python.linting.enabled": true } \ No newline at end of file diff --git a/schemascii/__init__.py b/schemascii/__init__.py index 24a839f..29b2001 100644 --- a/schemascii/__init__.py +++ b/schemascii/__init__.py @@ -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 d7a1c37..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,20 +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' + ident = m.group(2) or "0" if m.group(3): - boms.append(BOMData(m.group(1), - ident, 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), ident)) + 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]]: @@ -49,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): @@ -94,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 2c88395..4ee60b1 100644 --- a/schemascii/components_render.py +++ b/schemascii/components_render.py @@ -2,11 +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 @@ -16,52 +30,50 @@ 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}") + 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 @@ -69,22 +81,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 @@ -92,11 +100,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 @@ -107,24 +111,26 @@ 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""" @@ -132,26 +138,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 @@ -164,25 +178,29 @@ def inductor( 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"]) + 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""" @@ -190,27 +208,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""" @@ -218,23 +241,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, } @@ -243,10 +274,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"] @@ -261,12 +290,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"), @@ -274,7 +303,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( @@ -283,7 +313,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 @@ -291,24 +322,24 @@ 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[,{circle/input/output}]""" scale = options["scale"] @@ -316,14 +347,14 @@ def jack( 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 = "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(",")) + 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) @@ -333,34 +364,32 @@ def jack( r=scale / 4, stroke__width=options["stroke_width"], stroke=options["stroke"], - fill="transparent") - + id_text(box, bom_data, terminals, None, sc_text_pt, **options)) + 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)) + 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}") + 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(":") 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") @@ -374,79 +403,84 @@ 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): +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), (-.5+1j, .5+1j)] + points = [(0, 1j), (-0.5 + 1j, 0.5 + 1j)] match icon_type: case "earth": - points += [ - (-.33+1.25j, .33+1.25j), - (-.16+1.5j, .16+1.5j)] + points += [(-0.33 + 1.25j, 0.33 + 1.25j), (-0.16 + 1.5j, 0.16 + 1.5j)] case "chassis": points += [ - (-.5+1j, -.25+1.5j), - (1j, .25+1.5j), - (.5+1j, .75+1.5j)] + (-0.5 + 1j, -0.25 + 1.5j), + (1j, 0.25 + 1.5j), + (0.5 + 1j, 0.75 + 1.5j), + ] case "signal": - points += [ - (-.5+1j, 1.5j), - (.5+1j, 1.5j)] + points += [(-0.5 + 1j, 1.5j), (0.5 + 1j, 1.5j)] case "common": pass case _: - raise BOMError( - f"Unknown ground symbol type: {icon_type}") + raise BOMError(f"Unknown ground symbol type: {icon_type}") points = deep_transform(points, terminals[0].pt, pi / 2) return bunch_o_lines(points, **options) @@ -463,28 +497,26 @@ def ground( # if they aren't the path will be transformed { # 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 3bdbf4f..d9bd19c 100644 --- a/schemascii/edgemarks.py +++ b/schemascii/edgemarks.py @@ -6,39 +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 - grid.setmask(p, '*') + grid.setmask(p, "*") return Flag(p, c, s) + return flags @@ -49,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 c96b486..39e7f7a 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): @@ -24,7 +24,7 @@ class Side(IntEnum): def colinear(points: list[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 sharpness_score(points: list[complex]) -> float: @@ -50,7 +50,9 @@ def intersecting(a, b, p, q): # UNUSED as of yet -def merge_colinear(points: list[tuple[complex, complex]]) -> list[tuple[complex, complex]]: +def merge_colinear( + points: list[tuple[complex, complex]] +) -> list[tuple[complex, complex]]: "Merges line segments that are colinear." points = list(set(points)) out = [] @@ -70,7 +72,7 @@ def merge_colinear(points: list[tuple[complex, complex]]) -> list[tuple[complex, return out -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 @@ -87,14 +89,11 @@ 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: @@ -107,19 +106,19 @@ def fix_number(n: float) -> str: 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"\d+(\.\d+)", 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 @@ -132,13 +131,10 @@ def polylinegon(points: list[complex], is_polygon: bool = False, **options): 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.polyline(points=pts, fill="transparent", stroke__width=w, stroke=c) def find_dots(points: list[tuple[complex, complex]]) -> list[complex]: @@ -161,8 +157,8 @@ def find_dots(points: list[tuple[complex, complex]]) -> list[complex]: def bunch_o_lines(points: list[tuple[complex, complex]], **options): "Return a for each pair of points." - out = '' - scale = options['scale'] + out = "" + scale = options["scale"] w = options["stroke_width"] c = options["stroke"] for p1, p2 in points: @@ -174,17 +170,19 @@ def bunch_o_lines(points: list[tuple[complex, complex]], **options): f"{p2.real * scale}," f"{p2.imag * scale}", stroke=c, - stroke__width=w) + stroke__width=w, + ) return out 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, +): "Format the component ID and value around the point." if options["nolabels"]: return "" @@ -201,28 +199,33 @@ 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" + 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" + 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)), + (XML.tspan(f"{box.type}{box.id}", class_="cmp-id") * bool("L" in label_style)), " " * (bool(data) and "L" in label_style), data * bool("V" in label_style), x=point.real, y=point.imag, text__anchor=textach, font__size=options["scale"], - fill=options["stroke"]) + fill=options["stroke"], + ) def make_text_point(t1: complex, t2: complex, **options) -> complex: @@ -236,18 +239,22 @@ def make_text_point(t1: complex, t2: complex, **options) -> complex: def make_plus( - terminals: list[Terminal], - center: complex, - theta: float, - **options) -> str: + 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]]: @@ -256,59 +263,50 @@ 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: + 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: @@ -321,7 +319,9 @@ def is_clockwise(terminals: list[Terminal]) -> bool: return False -def sort_for_flags(terminals: list[Terminal], box: Cbox, *flags: list[str]) -> list[Terminal]: +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 = () @@ -330,12 +330,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..ecc1130 100644 --- a/schemascii/wires.py +++ b/schemascii/wires.py @@ -9,33 +9,32 @@ def next_in_dir( - grid: Grid, - point: complex, - dydx: complex) -> tuple[complex, complex] | None: + 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 @@ -88,8 +87,7 @@ def blank_wire(grid: Grid, p1: complex, p2: complex): keep = ["|()", "-"][way] swap = "|-"[way] if old not in keep: - if (grid.get(px + side) in keep and - grid.get(px - side) in keep): + if grid.get(px + side) in keep and grid.get(px - side) in keep: grid.setmask(px, swap) else: grid.setmask(px) @@ -103,7 +101,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: @@ -124,17 +122,22 @@ def next_wire(grid: Grid, **options) -> str | None: x2=p2.real * scale, y2=p2.imag * scale, stroke__width=stroke_width, - stroke=color) - for p1, p2 in line_pieces), + stroke=color, + ) + for p1, p2 in line_pieces + ), *( 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 +150,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) From d0d3f2d3ea81d045e16102ffe80817039b6399b1 Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Wed, 24 May 2023 15:10:14 +0000 Subject: [PATCH 08/46] add switches -> 0.2.3 --- dist/schemascii-0.2.3-py3-none-any.whl | Bin 0 -> 46680 bytes dist/schemascii-0.2.3.tar.gz | Bin 0 -> 57008 bytes index.html | 96 +++++++++++++++++++++++++ pyproject.toml | 2 +- schemascii/__init__.py | 2 +- schemascii/components_render.py | 73 +++++++++++++++++-- schemascii/utils.py | 4 +- schemascii_example.css | 9 ++- supported-components.md | 2 + test_data/test_charge_pump.txt.svg | 2 +- test_data/test_switches.txt | 6 ++ 11 files changed, 184 insertions(+), 12 deletions(-) create mode 100644 dist/schemascii-0.2.3-py3-none-any.whl create mode 100644 dist/schemascii-0.2.3.tar.gz create mode 100644 index.html create mode 100644 test_data/test_switches.txt 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 0000000000000000000000000000000000000000..798c11442f169812864081c50732f8f7bec2e66f GIT binary patch literal 46680 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 +S-^ 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
  • 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 literal 0 HcmV?d00001 diff --git a/index.html b/index.html new file mode 100644 index 0000000..47835d9 --- /dev/null +++ b/index.html @@ -0,0 +1,96 @@ + + + + + Schemascii + + + + + + +

    Schemascii Playground

    +
    +
    +
    +

    Schemascii Source

    + +
    +
    +
    +

    CSS

    +
    +
    +
    +
    
    +        
    + + + + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 39cf0ee..7524d87 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.2.3" 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 29b2001..c3fd015 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.2.3" def render(filename: str, text: str = None, **options) -> str: diff --git a/schemascii/components_render.py b/schemascii/components_render.py index 4ee60b1..81c9955 100644 --- a/schemascii/components_render.py +++ b/schemascii/components_render.py @@ -69,7 +69,8 @@ def de_ambiguous( 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}") + 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) @@ -111,7 +112,8 @@ def resistor(box: Cbox, terminals: list[Terminal], bom_data: BOMData, **options) 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) @@ -323,7 +325,8 @@ def integrated_circuit( x=sc_text_pt.real, y=sc_text_pt.imag, text__anchor=( - "start" if (terminal.side in (Side.TOP, Side.BOTTOM)) else "middle" + "start" if (terminal.side in ( + Side.TOP, Side.BOTTOM)) else "middle" ), font__size=options["scale"], fill=options["stroke"], @@ -415,7 +418,8 @@ def transistor(box: Cbox, terminals: list[Terminal], bom_data: BOMData, **option (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) + 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( @@ -436,7 +440,8 @@ def transistor(box: Cbox, terminals: list[Terminal], bom_data: BOMData, **option ] ) else: - arr = mid + rect(0.8, theta), mid + rect(0.4, theta) + rect(1, thetaquarter) + 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( @@ -468,7 +473,8 @@ def ground(box: Cbox, terminals: list[Terminal], bom_data: BOMData, **options): 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)] + 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), @@ -485,6 +491,61 @@ def ground(box: Cbox, terminals: list[Terminal], bom_data: BOMData, **options): 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 # https://github.com/KenKundert/svg_schematic/blob/0abb5dc/svg_schematic.py diff --git a/schemascii/utils.py b/schemascii/utils.py index 39e7f7a..bc0bd08 100644 --- a/schemascii/utils.py +++ b/schemascii/utils.py @@ -133,7 +133,7 @@ def polylinegon(points: list[complex], is_polygon: bool = False, **options): c = options["stroke"] 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.polygon(points=pts, fill=c, class_="filled") return XML.polyline(points=pts, fill="transparent", stroke__width=w, stroke=c) @@ -233,7 +233,7 @@ 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 diff --git a/schemascii_example.css b/schemascii_example.css index 0d2f62b..22754ad 100644 --- a/schemascii_example.css +++ b/schemascii_example.css @@ -18,7 +18,7 @@ svg.schemascii :is(.wire, .component):hover { --sch-color: lime; } -svg.schemascii .component :is(polyline, path, line, polygon, rect, circle) { +svg.schemascii .component :is(polyline, path, line, polygon, rect, circle):not(.filled) { stroke: var(--sch-color, red); stroke-width: 2; stroke-linecap: round; @@ -26,6 +26,13 @@ svg.schemascii .component :is(polyline, path, line, polygon, rect, circle) { fill: transparent; } +svg.schemascii .component :is(polyline, path, line, polygon, rect, circle).filled { + fill: var(--sch-color, red); + stroke: none; + transition-duration: 0.2s; +} + + svg.schemascii .component .plus :is(polyline, path, line) { stroke-width: 1; } diff --git a/supported-components.md b/supported-components.md index e76a3ab..7138a45 100644 --- a/supported-components.md +++ b/supported-components.md @@ -18,3 +18,5 @@ and run scripts/docs.py to re-generate this file. | `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/test_charge_pump.txt.svg b/test_data/test_charge_pump.txt.svg index 5d945b7..115782c 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 From e159cadd13b140651877b02ad5d873be92438f30 Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Wed, 24 May 2023 15:15:06 +0000 Subject: [PATCH 09/46] show error handler on playground --- index.html | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/index.html b/index.html index 47835d9..46fa8d0 100644 --- a/index.html +++ b/index.html @@ -56,18 +56,23 @@

    Schemascii Playground

    var render_button = document.getElementById("render"); var schemascii; async function main() { - info("Loading Python... "); - pyodide = await loadPyodide({ stdout: info, stderr: error }); - info("done\n"); - info("Fetching current Schemascii version... "); - var pyproject_toml = await fetch("pyproject.toml").then(x => x.text()); - var ver = /version = "([\d.]+)"/.exec(pyproject_toml)[1]; - info(ver + "\n"); - info(`Installing schemascii-${ver}... `); - await pyodide.runPythonAsync(`import micropip\nawait micropip.install("https://dragoncoder047.github.io/schemascii/dist/schemascii-${ver}-py3-none-any.whl")`); - schemascii = pyodide.pyimport("schemascii"); - setup(); - console.textContent = "ready\n"; + try { + info("Loading Python... "); + pyodide = await loadPyodide({ stdout: info, stderr: error }); + info("done\n"); + info("Fetching current Schemascii version... "); + var pyproject_toml = await fetch("pyproject.toml").then(x => x.text()); + var ver = /version = "([\d.]+)"/.exec(pyproject_toml)[1]; + info(ver + "\n"); + info(`Installing schemascii-${ver}... `); + await pyodide.runPythonAsync(`import micropip\nawait micropip.install("https://dragoncoder047.github.io/schemascii/dist/schemascii-${ver}-py3-none-any.whl")`); + schemascii = pyodide.pyimport("schemascii"); + setup(); + console.textContent = "ready\n"; + } catch (e) { + error(`\nJS Error:\n${e.stack}\n`); + throw e; + } } function info(line) { console.textContent += line; From 40c189967f97069023623bb3ef93c1c1124a70a2 Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Wed, 24 May 2023 15:17:15 +0000 Subject: [PATCH 10/46] expand it all + fetch default css --- index.html | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index 46fa8d0..58d962a 100644 --- a/index.html +++ b/index.html @@ -30,7 +30,7 @@

    Schemascii Playground

    -
    +

    Schemascii Source

    @@ -67,7 +67,7 @@

    Schemascii Playground

    info(`Installing schemascii-${ver}... `); await pyodide.runPythonAsync(`import micropip\nawait micropip.install("https://dragoncoder047.github.io/schemascii/dist/schemascii-${ver}-py3-none-any.whl")`); schemascii = pyodide.pyimport("schemascii"); - setup(); + await setup(); console.textContent = "ready\n"; } catch (e) { error(`\nJS Error:\n${e.stack}\n`); @@ -80,7 +80,8 @@

    Schemascii Playground

    function error(text) { errors.textContent += text; } - function setup() { + async function setup() { + css.value = await fetch("schemascii_example.css").then(x => x.text()); css.addEventListener("input", () => { style_elem.innerHTML = css.value; }); From 3c1df60aba208609daba65cdeff856db70a14d69 Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Wed, 24 May 2023 15:22:21 +0000 Subject: [PATCH 11/46] wait for micropip to load --- index.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/index.html b/index.html index 58d962a..a6f38bb 100644 --- a/index.html +++ b/index.html @@ -43,7 +43,7 @@

    Schemascii Playground

    
    -        
    +
    
         
         
     
    -
    \ No newline at end of file
    +
    
    From bb539979e811e1c02ca7bd53283f9ac59ab1bdf5 Mon Sep 17 00:00:00 2001
    From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com>
    Date: Wed, 24 May 2023 12:31:59 -0400
    Subject: [PATCH 20/46] implement debounce properly
    
    ---
     index.html | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/index.html b/index.html
    index ffaccb5..65d4062 100644
    --- a/index.html
    +++ b/index.html
    @@ -100,7 +100,7 @@ 

    Errors

    var timeout = null; source.addEventListener("input", () => { if (timeout) clearTimeout(timeout); - setTimeout(async () => { + timeout = setTimeout(async () => { console.textContent = ""; errors.textContent = ""; try { @@ -110,7 +110,7 @@

    Errors

    output.innerHTML = ""; throw e; } - }, 500); + }, 100); }); } main(); From 2895d00c54a0a6159412ccfa39f53b5a1d02dd76 Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Wed, 24 May 2023 12:34:45 -0400 Subject: [PATCH 21/46] prevent senseless input if there is an error --- index.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 65d4062..7ba4351 100644 --- a/index.html +++ b/index.html @@ -42,10 +42,10 @@

    Schemascii Playground

    -

    Schemascii Source

    +

    Schemascii Source

    -

    CSS

    +

    CSS

    Result

    @@ -112,6 +112,8 @@

    Errors

    } }, 100); }); + source.removeAttribute("disabled"); + css.removeAttribute("disabled"); } main(); From 27e2317788de41d1118f0ac4c6527f1389d396ab Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Wed, 24 May 2023 12:37:10 -0400 Subject: [PATCH 22/46] Update index.html --- index.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 7ba4351..1389912 100644 --- a/index.html +++ b/index.html @@ -42,10 +42,10 @@

    Schemascii Playground

    -

    Schemascii Source

    +

    Schemascii Source

    -

    CSS

    +

    CSS

    Result

    @@ -54,6 +54,8 @@

    Messages

    
             

    Errors

    
    +        

    More Information

    +

    https://github.com/dragoncoder047/schemascii/

    - + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e14990c..91486a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "schemascii" -version = "0.3.1" +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 ee060e4..96d7308 100644 --- a/schemascii/__init__.py +++ b/schemascii/__init__.py @@ -8,7 +8,7 @@ from .utils import XML from .errors import * -__version__ = "0.3.1" +__version__ = "0.3.2" def render(filename: str, text: str = None, **options) -> str: diff --git a/scripts/web_startup.py b/scripts/web_startup.py new file mode 100644 index 0000000..9e24293 --- /dev/null +++ b/scripts/web_startup.py @@ -0,0 +1,99 @@ +from pyodide.http import pyfetch as fetch, open_url as get +from pyodide.ffi import create_proxy as event_handler +import asyncio +import functools +import sys +import re +import js +import json +from operator import itemgetter +import warnings +import micropip + + +def debounced(fun): + timeout = None + + @functools.wraps(fun) + def inner(): + nonlocal timeout + if timeout: + js.clearTimeout(timeout) + timeout = js.setTimeout(fun, 100) + + return inner + + +def syncify(fun): + @functools.wraps(fun) + def inner(e): + return asyncio.ensure_future(fun(e)) + return inner + + +@event_handler +@debounced +def sync_css(): + style_elem.innerHTML = css_box.value + + +@event_handler +@debounced +def render_catch_warnings(*args, **kwargs): + import schemascii + console.textContent = "" + errors.textContent = "" + with warnings.catch_warnings(record=True) as captured_warnings: + out = schemascii.render(*args, **kwargs) + for warn in captured_warnings: + print("warning:", warn.message) + return out + + +@event_handler +@syncify +async def switch_version(): + if "schemascii" in sys.modules: + del sys.modules["schemascii"] # Invalidate cache + version = ver_switcher.value + await micropip.install(versions_to_wheel_map[version]) + +output = js.document.getElementById("output") +css_box = js.document.getElementById("css") +console = js.document.getElementById("console") +source = js.document.getElementById("schemascii") +style_elem = js.document.getElementById("custom-css") +errors = js.document.getElementById("errors") +ver_switcher = js.document.getElementById("version") + + +print("Loading all versions... ", end="") +versions_all = json.load( + get("https://api.github.com/repos/dragoncoder047/schemascii/contents/dist")) +versions_to_wheel_map = dict( + zip(map(itemgetter("name"), versions_all), map(itemgetter("path"), versions_all))) +all_versions = list(versions_to_wheel_map.keys()) +all_versions.append("DEV") +latest_version = re.search( + r'''version = "([\d.]+)"''', get("pyproject.toml").read()).group(1) +print(all_versions, "latest =", latest_version) + +for ver in all_versions: + opt = js.document.createElement("option") + opt.value = opt.textContent = ver + ver_switcher.append(opt) + +ver_switcher.value = latest_version +await micropip.install(versions_to_wheel_map[latest_version]) + + +css_source = get("schemascii_example.css").read() +style_elem.textContent = css_source +css_box.value = css_source + +css_box.addEventListener("input", sync_css) +source.addEventListener("input", render_catch_warnings) + +source.removeAttribute("disabled") +css_box.removeAttribute("disabled") +console.textContent = "ready\n" diff --git a/test_data/test_charge_pump.txt.svg b/test_data/test_charge_pump.txt.svg index e72f24f..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 From 6757c2e882e22db54534430abe97f805d7b7b950 Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Fri, 11 Aug 2023 18:35:20 +0000 Subject: [PATCH 34/46] fix version finding bug --- scripts/web_startup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/web_startup.py b/scripts/web_startup.py index 9e24293..1977a97 100644 --- a/scripts/web_startup.py +++ b/scripts/web_startup.py @@ -6,7 +6,6 @@ import re import js import json -from operator import itemgetter import warnings import micropip @@ -68,12 +67,13 @@ async def switch_version(): print("Loading all versions... ", end="") -versions_all = json.load( +foo = json.load( get("https://api.github.com/repos/dragoncoder047/schemascii/contents/dist")) +foo = filter(lambda x: x["name"].endswith(".whl"), foo) +foo = map(lambda x: x["path"], foo) versions_to_wheel_map = dict( - zip(map(itemgetter("name"), versions_all), map(itemgetter("path"), versions_all))) + zip(map(lambda x: re.search(r"""/schemascii-([\d.]+)-""", x).group(1), foo), foo)) all_versions = list(versions_to_wheel_map.keys()) -all_versions.append("DEV") latest_version = re.search( r'''version = "([\d.]+)"''', get("pyproject.toml").read()).group(1) print(all_versions, "latest =", latest_version) From c625572397b7a8ac17b574389175bd3e84e04c9b Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Fri, 11 Aug 2023 18:41:39 +0000 Subject: [PATCH 35/46] mixed up lazy iteration --- scripts/web_startup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/web_startup.py b/scripts/web_startup.py index 1977a97..b4b8ac0 100644 --- a/scripts/web_startup.py +++ b/scripts/web_startup.py @@ -70,7 +70,7 @@ async def switch_version(): foo = json.load( get("https://api.github.com/repos/dragoncoder047/schemascii/contents/dist")) foo = filter(lambda x: x["name"].endswith(".whl"), foo) -foo = map(lambda x: x["path"], foo) +foo = list(map(lambda x: x["path"], foo)) versions_to_wheel_map = dict( zip(map(lambda x: re.search(r"""/schemascii-([\d.]+)-""", x).group(1), foo), foo)) all_versions = list(versions_to_wheel_map.keys()) From f44028cff791a95d228164ee60523895e4f0372d Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Fri, 11 Aug 2023 18:55:18 +0000 Subject: [PATCH 36/46] add download button --- index.html | 2 +- scripts/web_startup.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index 098f57c..eafbbbb 100644 --- a/index.html +++ b/index.html @@ -61,7 +61,7 @@

    Schemascii Source

    CSS

    -

    Result

    +

    Result

    Messages

    
    diff --git a/scripts/web_startup.py b/scripts/web_startup.py
    index b4b8ac0..8a3b93a 100644
    --- a/scripts/web_startup.py
    +++ b/scripts/web_startup.py
    @@ -1,5 +1,3 @@
    -from pyodide.http import pyfetch as fetch, open_url as get
    -from pyodide.ffi import create_proxy as event_handler
     import asyncio
     import functools
     import sys
    @@ -7,6 +5,8 @@
     import js
     import json
     import warnings
    +from pyodide.http import pyfetch as fetch, open_url as get
    +from pyodide.ffi import create_proxy as event_handler
     import micropip
     
     
    @@ -57,6 +57,13 @@ async def switch_version():
         version = ver_switcher.value
         await micropip.install(versions_to_wheel_map[version])
     
    +@event_handler
    +def download_svg():
    +    a = js.document.createElement("a")
    +    a.setAttribute("href", js.URL.createObjectURL(js.Blob.new([output.innerHTML], {"type": "application/svg+xml"})))
    +    a.setAttribute("download", f"schemascii_playground_{js.Date.new.toISOString()}_no_css.svg")
    +    a.click()
    +
     output = js.document.getElementById("output")
     css_box = js.document.getElementById("css")
     console = js.document.getElementById("console")
    @@ -64,6 +71,7 @@ async def switch_version():
     style_elem = js.document.getElementById("custom-css")
     errors = js.document.getElementById("errors")
     ver_switcher = js.document.getElementById("version")
    +download_button = js.document.getElementById("download")
     
     
     print("Loading all versions... ", end="")
    @@ -93,6 +101,7 @@ async def switch_version():
     
     css_box.addEventListener("input", sync_css)
     source.addEventListener("input", render_catch_warnings)
    +download_button.addEventListener("click", download_svg)
     
     source.removeAttribute("disabled")
     css_box.removeAttribute("disabled")
    
    From b323c9a295ad9a3d4d8e071d928b2e89ef7ec598 Mon Sep 17 00:00:00 2001
    From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com>
    Date: Fri, 11 Aug 2023 19:29:51 +0000
    Subject: [PATCH 37/46] kludges on event handlers
    
    ---
     index.html             | 20 +++++++++++++++-
     scripts/web_startup.py | 52 ++++++++----------------------------------
     2 files changed, 29 insertions(+), 43 deletions(-)
    
    diff --git a/index.html b/index.html
    index eafbbbb..7847b02 100644
    --- a/index.html
    +++ b/index.html
    @@ -75,7 +75,11 @@ 

    More Information

    // cSpell:ignore pyodide pyproject var pyodide; var console = document.getElementById("console"); - var errors = document.getElementById("errors") + 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"); async function main() { try { @@ -85,6 +89,13 @@

    More Information

    await pyodide.loadPackage("micropip", { errorCallback: error, messageCallback: () => { } }); info("done\n"); await pyodide.runPythonAsync(await fetch("scripts/web_startup.py").then(r => r.text())); + css_box.addEventListener("input", debounce(pyodide.globals.get("sync_css"))); + source.addEventListener("input", debounce(pyodide.globals.get("render_catch_warnings"))); + download_button.addEventListener("click", pyodide.globals.get("download_svg")); + ver_switcher.addEventListener("change", pyodide.globals.get("switch_version")); + source.removeAttribute("disabled"); + css_box.removeAttribute("disabled"); + console.textContent = "Ready"; } catch (e) { error(`\nFATAL ERROR:\n${e.stack}\n`); throw e; @@ -96,6 +107,13 @@

    More Information

    function error(text) { errors.textContent += text; } + function debounce(fun) { + var timeout; + return function() { + if (timeout) clearTimeout(timeout); + timeout = setTimeout(fun, 100); + } + } main(); diff --git a/scripts/web_startup.py b/scripts/web_startup.py index 8a3b93a..360902b 100644 --- a/scripts/web_startup.py +++ b/scripts/web_startup.py @@ -1,43 +1,17 @@ -import asyncio import functools import sys import re import js import json import warnings -from pyodide.http import pyfetch as fetch, open_url as get -from pyodide.ffi import create_proxy as event_handler +from pyodide.http import open_url as get import micropip -def debounced(fun): - timeout = None - - @functools.wraps(fun) - def inner(): - nonlocal timeout - if timeout: - js.clearTimeout(timeout) - timeout = js.setTimeout(fun, 100) - - return inner - - -def syncify(fun): - @functools.wraps(fun) - def inner(e): - return asyncio.ensure_future(fun(e)) - return inner - - -@event_handler -@debounced def sync_css(): style_elem.innerHTML = css_box.value -@event_handler -@debounced def render_catch_warnings(*args, **kwargs): import schemascii console.textContent = "" @@ -49,21 +23,23 @@ def render_catch_warnings(*args, **kwargs): return out -@event_handler -@syncify -async def switch_version(): +def switch_version(): if "schemascii" in sys.modules: del sys.modules["schemascii"] # Invalidate cache version = ver_switcher.value - await micropip.install(versions_to_wheel_map[version]) + js.eval( + f'''(async () => await pyodide.globals.get("micropip").install({versions_to_wheel_map[version]!r}))();''') + -@event_handler def download_svg(): a = js.document.createElement("a") - a.setAttribute("href", js.URL.createObjectURL(js.Blob.new([output.innerHTML], {"type": "application/svg+xml"}))) - a.setAttribute("download", f"schemascii_playground_{js.Date.new.toISOString()}_no_css.svg") + a.setAttribute("href", js.URL.createObjectURL(js.Blob.new( + [output.innerHTML], {"type": "application/svg+xml"}))) + a.setAttribute( + "download", f"schemascii_playground_{js.Date.new.toISOString()}_no_css.svg") a.click() + output = js.document.getElementById("output") css_box = js.document.getElementById("css") console = js.document.getElementById("console") @@ -98,11 +74,3 @@ def download_svg(): css_source = get("schemascii_example.css").read() style_elem.textContent = css_source css_box.value = css_source - -css_box.addEventListener("input", sync_css) -source.addEventListener("input", render_catch_warnings) -download_button.addEventListener("click", download_svg) - -source.removeAttribute("disabled") -css_box.removeAttribute("disabled") -console.textContent = "ready\n" From cf32b6cf08dd10ff6cbf320f6a22e3de897cb41d Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Fri, 11 Aug 2023 20:46:36 +0000 Subject: [PATCH 38/46] move to js because pyodide is being annoying --- index.html | 46 +---------------- scripts/monkeypatch.py | 17 +++++++ scripts/web_startup.js | 110 +++++++++++++++++++++++++++++++++++++++++ scripts/web_startup.py | 76 ---------------------------- 4 files changed, 128 insertions(+), 121 deletions(-) create mode 100644 scripts/monkeypatch.py create mode 100644 scripts/web_startup.js delete mode 100644 scripts/web_startup.py diff --git a/index.html b/index.html index 7847b02..6f750b2 100644 --- a/index.html +++ b/index.html @@ -71,50 +71,6 @@

    More Information

    https://github.com/dragoncoder047/schemascii/

    - + \ No newline at end of file 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/web_startup.js b/scripts/web_startup.js new file mode 100644 index 0000000..d2b1a0b --- /dev/null +++ b/scripts/web_startup.js @@ -0,0 +1,110 @@ +// 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 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)[0]); + 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)); + + 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.textContent = schemascii.patched_render(source.value); +} + +async function switch_version() { + info("Installing Schemascii version " + ver_switcher.value + "... ") + await pyodide.pyimport("micropip").install(ver_map[ver_switcher.value]); + monkeypatch(); + schemascii = pyodide.runPython("import schemascii; schemascii"); + info("done\n"); +} + +function download() { + 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/scripts/web_startup.py b/scripts/web_startup.py deleted file mode 100644 index 360902b..0000000 --- a/scripts/web_startup.py +++ /dev/null @@ -1,76 +0,0 @@ -import functools -import sys -import re -import js -import json -import warnings -from pyodide.http import open_url as get -import micropip - - -def sync_css(): - style_elem.innerHTML = css_box.value - - -def render_catch_warnings(*args, **kwargs): - import schemascii - console.textContent = "" - errors.textContent = "" - with warnings.catch_warnings(record=True) as captured_warnings: - out = schemascii.render(*args, **kwargs) - for warn in captured_warnings: - print("warning:", warn.message) - return out - - -def switch_version(): - if "schemascii" in sys.modules: - del sys.modules["schemascii"] # Invalidate cache - version = ver_switcher.value - js.eval( - f'''(async () => await pyodide.globals.get("micropip").install({versions_to_wheel_map[version]!r}))();''') - - -def download_svg(): - a = js.document.createElement("a") - a.setAttribute("href", js.URL.createObjectURL(js.Blob.new( - [output.innerHTML], {"type": "application/svg+xml"}))) - a.setAttribute( - "download", f"schemascii_playground_{js.Date.new.toISOString()}_no_css.svg") - a.click() - - -output = js.document.getElementById("output") -css_box = js.document.getElementById("css") -console = js.document.getElementById("console") -source = js.document.getElementById("schemascii") -style_elem = js.document.getElementById("custom-css") -errors = js.document.getElementById("errors") -ver_switcher = js.document.getElementById("version") -download_button = js.document.getElementById("download") - - -print("Loading all versions... ", end="") -foo = json.load( - get("https://api.github.com/repos/dragoncoder047/schemascii/contents/dist")) -foo = filter(lambda x: x["name"].endswith(".whl"), foo) -foo = list(map(lambda x: x["path"], foo)) -versions_to_wheel_map = dict( - zip(map(lambda x: re.search(r"""/schemascii-([\d.]+)-""", x).group(1), foo), foo)) -all_versions = list(versions_to_wheel_map.keys()) -latest_version = re.search( - r'''version = "([\d.]+)"''', get("pyproject.toml").read()).group(1) -print(all_versions, "latest =", latest_version) - -for ver in all_versions: - opt = js.document.createElement("option") - opt.value = opt.textContent = ver - ver_switcher.append(opt) - -ver_switcher.value = latest_version -await micropip.install(versions_to_wheel_map[latest_version]) - - -css_source = get("schemascii_example.css").read() -style_elem.textContent = css_source -css_box.value = css_source From 6b9bf5145f1a6afa6787482bc1fb48d977173801 Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:49:03 -0400 Subject: [PATCH 39/46] typo --- scripts/web_startup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/web_startup.js b/scripts/web_startup.js index d2b1a0b..9547219 100644 --- a/scripts/web_startup.js +++ b/scripts/web_startup.js @@ -30,7 +30,7 @@ async function main() { 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)[0]); + 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(); From 510aba8eb3bce4a6bff6218f199402ae24fbabb4 Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:52:07 -0400 Subject: [PATCH 40/46] another typo --- scripts/web_startup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/web_startup.js b/scripts/web_startup.js index 9547219..729c156 100644 --- a/scripts/web_startup.js +++ b/scripts/web_startup.js @@ -87,7 +87,7 @@ function sync_css() { function render() { console.textContent = ""; errors.textContent = ""; - output.textContent = schemascii.patched_render(source.value); + output.innerHTML = schemascii.patched_render(source.value); } async function switch_version() { From dbbee0dfd2c63b6d9b95eba2448b573382af8215 Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:54:59 -0400 Subject: [PATCH 41/46] forgot to fetch css --- scripts/web_startup.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/web_startup.js b/scripts/web_startup.js index 729c156..a905ab9 100644 --- a/scripts/web_startup.js +++ b/scripts/web_startup.js @@ -39,6 +39,7 @@ async function main() { download_button.addEventListener("click", download); ver_switcher.addEventListener("change", acatched(switch_version)); + css_box.value = await fetch("schemascii_example.css").then(r => r.text()); source.removeAttribute("disabled"); css_box.removeAttribute("disabled"); console.textContent = "Ready"; From 43d70fffa7d6b230242fcf6370b0ace9e4a9a60a Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Fri, 11 Aug 2023 17:01:16 -0400 Subject: [PATCH 42/46] missing objects --- scripts/web_startup.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/web_startup.js b/scripts/web_startup.js index a905ab9..12b455e 100644 --- a/scripts/web_startup.js +++ b/scripts/web_startup.js @@ -6,6 +6,8 @@ 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; @@ -34,6 +36,7 @@ async function main() { 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); @@ -95,11 +98,13 @@ async function switch_version() { info("Installing Schemascii version " + ver_switcher.value + "... ") await pyodide.pyimport("micropip").install(ver_map[ver_switcher.value]); monkeypatch(); - schemascii = pyodide.runPython("import schemascii; schemascii"); + schemascii = pyodide.runPython("import schemascii\nschemascii"); info("done\n"); + output.innerHTML = ""; } 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`); From 52bb36d4eae0274c20bbfea5b01508c3e2d44c0d Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Fri, 11 Aug 2023 17:03:59 -0400 Subject: [PATCH 43/46] guards around installing new version --- scripts/web_startup.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/web_startup.js b/scripts/web_startup.js index 12b455e..00a45e9 100644 --- a/scripts/web_startup.js +++ b/scripts/web_startup.js @@ -95,12 +95,14 @@ function render() { } 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() { From a76158b6dfa26384f8e2fc8bdce52b5efca728bd Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Fri, 11 Aug 2023 17:04:48 -0400 Subject: [PATCH 44/46] sync css to start --- scripts/web_startup.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/web_startup.js b/scripts/web_startup.js index 00a45e9..db6ac5e 100644 --- a/scripts/web_startup.js +++ b/scripts/web_startup.js @@ -43,6 +43,7 @@ async function main() { 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"; From 4889bbad2ffe686034acf2287a912955c5ef03e5 Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Mon, 16 Sep 2024 20:51:55 -0400 Subject: [PATCH 45/46] Use nested CSS --- schemascii_example.css | 108 +++++++++++++++++++---------------------- 1 file changed, 49 insertions(+), 59 deletions(-) diff --git a/schemascii_example.css b/schemascii_example.css index 617c61d..379ae3e 100644 --- a/schemascii_example.css +++ b/schemascii_example.css @@ -1,69 +1,59 @@ svg.schemascii { background: black; -} - -svg.schemascii .wire polyline { - stroke: var(--sch-color, blue); - stroke-width: 2; - stroke-linecap: round; - stroke-linejoin: round; - transition-duration: 0.2s; - fill: transparent; -} - -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):not(.filled) { - stroke: var(--sch-color, red); - stroke-width: 2; - stroke-linecap: round; - transition-duration: 0.2s; - fill: transparent; -} - -svg.schemascii .component :is(polyline, path, line, polygon, rect, circle).filled { - fill: var(--sch-color, red); - stroke: none; - transition-duration: 0.2s; -} - - -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; + } } } From a4e968395668a7d8ba82633ba8bb881eb6d7e16f Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Fri, 16 May 2025 13:55:02 -0400 Subject: [PATCH 46/46] force monospaced font on textareas closes #10 --- index.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index 6f750b2..7d2e0e7 100644 --- a/index.html +++ b/index.html @@ -24,6 +24,11 @@ resize: vertical; } + textarea, + pre { + font-family: "Fira Code", monospace; + } + #errors { color: red; } @@ -73,4 +78,4 @@

    More Information

    - \ 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