diff --git a/dist/schemascii-0.3.0-py3-none-any.whl b/dist/schemascii-0.3.0-py3-none-any.whl new file mode 100644 index 0000000..ef829a3 Binary files /dev/null and b/dist/schemascii-0.3.0-py3-none-any.whl differ diff --git a/dist/schemascii-0.3.0.tar.gz b/dist/schemascii-0.3.0.tar.gz new file mode 100644 index 0000000..3794ccf Binary files /dev/null and b/dist/schemascii-0.3.0.tar.gz differ diff --git a/dist/schemascii-0.3.1-py3-none-any.whl b/dist/schemascii-0.3.1-py3-none-any.whl new file mode 100644 index 0000000..158f25e Binary files /dev/null and b/dist/schemascii-0.3.1-py3-none-any.whl differ diff --git a/dist/schemascii-0.3.1.tar.gz b/dist/schemascii-0.3.1.tar.gz new file mode 100644 index 0000000..acc9f32 Binary files /dev/null and b/dist/schemascii-0.3.1.tar.gz differ diff --git a/dist/schemascii-0.3.2-py3-none-any.whl b/dist/schemascii-0.3.2-py3-none-any.whl new file mode 100644 index 0000000..744e369 Binary files /dev/null and b/dist/schemascii-0.3.2-py3-none-any.whl differ diff --git a/dist/schemascii-0.3.2.tar.gz b/dist/schemascii-0.3.2.tar.gz new file mode 100644 index 0000000..be1aab1 Binary files /dev/null and b/dist/schemascii-0.3.2.tar.gz differ diff --git a/index.html b/index.html index 1389912..7d2e0e7 100644 --- a/index.html +++ b/index.html @@ -24,15 +24,32 @@ resize: vertical; } + textarea, + pre { + font-family: "Fira Code", monospace; + } + #errors { color: red; } + a, + a:visited { + color: blue; + } + @media screen and (prefers-color-scheme: dark) { - body, textarea { + + body, + textarea { color: white; background: black; } + + a, + a:visited { + color: magenta; + } } @@ -40,6 +57,7 @@

Schemascii Playground

+

using schemascii version

Schemascii Source

@@ -48,76 +66,16 @@

Schemascii Source

CSS

-

Result

+

Result

Messages


         

Errors


         

More Information

-

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

+

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

- + diff --git a/pyproject.toml b/pyproject.toml index 28d28ff..91486a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "schemascii" -version = "0.2.4" +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 d4433f0..96d7308 100644 --- a/schemascii/__init__.py +++ b/schemascii/__init__.py @@ -8,7 +8,7 @@ from .utils import XML from .errors import * -__version__ = "0.2.4" +__version__ = "0.3.2" def render(filename: str, text: str = None, **options) -> str: diff --git a/schemascii/utils.py b/schemascii/utils.py index bc0bd08..4b07dcd 100644 --- a/schemascii/utils.py +++ b/schemascii/utils.py @@ -22,11 +22,16 @@ class Side(IntEnum): BOTTOM = 3 -def colinear(points: list[complex]) -> bool: +def colinear(*points: complex) -> bool: "Returns true if all the points are in the same line." return len(set(phase(p - points[0]) for p in points[1:])) == 1 +def force_int(p: complex) -> complex: + "Force the coordinates of the complex number to lie on the integer grid." + return complex(round(p.real), round(p.imag)) + + def sharpness_score(points: list[complex]) -> float: """Returns a number indicating how twisty the line is -- higher means the corners are sharper.""" @@ -49,27 +54,45 @@ def intersecting(a, b, p, q): return sort_a <= sort_p <= sort_b or sort_p <= sort_b <= sort_q -# UNUSED as of yet -def merge_colinear( - points: list[tuple[complex, complex]] -) -> list[tuple[complex, complex]]: - "Merges line segments that are colinear." - points = list(set(points)) - out = [] - a, b = points[0] - while points: - print(points) - for pq in points[1:]: - p, q = pq - if not (colinear((a, b, p, q)) and intersecting(a, b, p, q)): +def take_next_group(links: list[tuple[complex, complex]]) -> list[tuple[complex, complex]]: + """Pops the longes possible link off of the `links` list and returns it, + mutating the input list.""" + best = [links.pop()] + while True: + for pair in links: + if best[0][0] == pair[1]: + best.insert(0, pair) + links.remove(pair) + elif best[0][0] == pair[0]: + best.insert(0, (pair[1], pair[0])) + links.remove(pair) + elif best[-1][1] == pair[0]: + best.append(pair) + links.remove(pair) + elif best[-1][1] == pair[1]: + best.append((pair[1], pair[0])) + links.remove(pair) + else: continue - points.remove(pq) - a, b = sorted((a, b, p, q), key=lambda x: x.real)[::3] break else: - out.append((a, b)) - (a, b), points = points[0], points[1:] - return out + break + return best + + +def merge_colinear(links: list[tuple[complex, complex]]): + "Merges line segments that are colinear. Mutates the input list." + i = 1 + while True: + if i == len(links): + break + elif links[i][0] == links[i][1]: + links.remove(links[i]) + elif links[i-1][1] == links[i][0] and colinear(links[i-1][0], links[i][0], links[i][1]): + links[i-1] = (links[i-1][0], links[i][1]) + links.remove(links[i]) + else: + i += 1 def iterate_line(p1: complex, p2: complex, step: float = 1.0): @@ -77,7 +100,7 @@ def iterate_line(p1: complex, p2: complex, step: float = 1.0): vec = p2 - p1 point = p1 while abs(vec) > abs(point - p1): - yield point + yield force_int(point) point += rect(step, phase(vec)) yield point @@ -93,7 +116,8 @@ def deep_transform(data, origin: complex, theta: float): 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: @@ -101,7 +125,10 @@ def fix_number(n: float) -> str: Otherwise round it to 2 digits.""" if n.is_integer(): return str(int(n)) - return str(round(n, 2)) + n = round(n, 2) + if n.is_integer(): + return str(int(n)) + return str(n) class XMLClass: @@ -114,7 +141,8 @@ def mk_tag(*contents: str, **attrs: str) -> str: if isinstance(v, float): v = fix_number(v) elif isinstance(v, str): - v = re.sub(r"\d+(\.\d+)", lambda m: fix_number(float(m.group())), v) + v = re.sub(r"\b\d+(\.\d+)\b", + lambda m: fix_number(float(m.group())), v) out += f'{k.removesuffix("_").replace("__", "-")}="{v}" ' out = out.rstrip() + ">" + "".join(contents) return out + f"" @@ -126,7 +154,7 @@ def mk_tag(*contents: str, **attrs: str) -> str: del XMLClass -def polylinegon(points: list[complex], is_polygon: bool = False, **options): +def polylinegon(points: list[complex], is_polygon: bool = False, **options) -> str: "Turn the list of points into a or ." scale = options["scale"] w = options["stroke_width"] @@ -155,24 +183,16 @@ def find_dots(points: list[tuple[complex, complex]]) -> list[complex]: return [pt for pt, count in seen.items() if count > 3] -def bunch_o_lines(points: list[tuple[complex, complex]], **options): - "Return a for each pair of points." - out = "" - scale = options["scale"] - w = options["stroke_width"] - c = options["stroke"] - for p1, p2 in points: - if abs(p1 - p2) == 0: - continue - out += XML.polyline( - points=f"{p1.real * scale}," - f"{p1.imag * scale} " - f"{p2.real * scale}," - f"{p2.imag * scale}", - stroke=c, - stroke__width=w, - ) - return out +def bunch_o_lines(pairs: list[tuple[complex, complex]], **options) -> str: + "Collapse the pairs of points and return the smallest number of s." + lines = [] + while pairs: + group = take_next_group(pairs) + merge_colinear(group) + # make it a polyline + pts = [group[0][0]] + [p[1] for p in group] + lines.append(pts) + return "".join(polylinegon(line, **options) for line in lines) def id_text( @@ -182,7 +202,7 @@ def id_text( unit: str | list[str] | None, point: complex | None = None, **options, -): +) -> str: "Format the component ID and value around the point." if options["nolabels"]: return "" @@ -215,9 +235,11 @@ def id_text( 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, @@ -238,9 +260,7 @@ def make_text_point(t1: complex, t2: complex, **options) -> complex: return text_pt -def make_plus( - terminals: list[Terminal], center: complex, theta: float, **options -) -> str: +def make_plus(terminals: list[Terminal], center: complex, theta: float, **options) -> str: "Make a + sign if the terminals indicate the component is polarized." if all(t.flag != "+" for t in terminals): return "" @@ -268,9 +288,7 @@ def arrow_points(p1: complex, p2: complex) -> list[tuple[complex, complex]]: ] -def make_variable( - center: complex, theta: float, is_variable: bool = True, **options -) -> str: +def make_variable(center: complex, theta: float, is_variable: bool = True, **options) -> str: "Draw a 'variable' arrow across the component." if not is_variable: return "" @@ -319,9 +337,7 @@ 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 = () diff --git a/schemascii/wires.py b/schemascii/wires.py index ecc1130..ef5cb94 100644 --- a/schemascii/wires.py +++ b/schemascii/wires.py @@ -1,16 +1,14 @@ from cmath import phase, rect from math import pi from .grid import Grid -from .utils import iterate_line, merge_colinear, XML, find_dots +from .utils import force_int, iterate_line, bunch_o_lines, XML, find_dots # cSpell:ignore dydx DIRECTIONS = [1, -1, 1j, -1j] -def next_in_dir( - grid: Grid, point: complex, dydx: complex -) -> tuple[complex, complex] | None: +def next_in_dir(grid: Grid, point: complex, dydx: complex) -> tuple[complex, complex] | None: """Follows the wire starting at the point in the specified direction, until some interesting change (a corner, junction, or end). Returns the tuple (new, old).""" @@ -79,18 +77,16 @@ def blank_wire(grid: Grid, p1: complex, p2: complex): "Blank out the wire from p1 to p2." # Crazy math!! way = int(phase(p1 - p2) / pi % 1.0 * 2) - side = rect(1, phase(p1 - p2) + pi / 2) + side = force_int(rect(1, phase(p1 - p2) + pi / 2)) + # way: 0: Horizontal, 1: Vertical + # Don't mask out wire crosses + cross = ["|()", "-"][way] + swap = "|-"[way] for px in iterate_line(p1, p2): - old = grid.get(px) - # way: 0: Horizontal, 1: Vertical - # Don't mask out wire crosses - keep = ["|()", "-"][way] - swap = "|-"[way] - if old not in keep: - if grid.get(px + side) in keep and grid.get(px - side) in keep: - grid.setmask(px, swap) - else: - grid.setmask(px) + if grid.get(px + side) in cross and grid.get(px - side) in cross: + grid.setmask(px, swap) + else: + grid.setmask(px) def next_wire(grid: Grid, **options) -> str | None: @@ -115,17 +111,7 @@ def next_wire(grid: Grid, **options) -> str | None: raise RuntimeError("0-length wire") dots = find_dots(line_pieces) return XML.g( - *( - XML.line( - x1=p1.real * scale, - y1=p1.imag * scale, - x2=p2.real * scale, - y2=p2.imag * scale, - stroke__width=stroke_width, - stroke=color, - ) - for p1, p2 in line_pieces - ), + bunch_o_lines(line_pieces, **options), *( XML.circle( cx=pt.real * scale, diff --git a/schemascii_example.css b/schemascii_example.css index 22754ad..379ae3e 100644 --- a/schemascii_example.css +++ b/schemascii_example.css @@ -1,67 +1,59 @@ svg.schemascii { background: black; -} - -svg.schemascii .wire line { - stroke: var(--sch-color, blue); - stroke-width: 2; - stroke-linecap: round; - transition-duration: 0.2s; -} - -svg.schemascii .wire circle { - fill: var(--sch-color, blue); - transition-duration: 0.2s; -} - -svg.schemascii :is(.wire, .component):hover { - --sch-color: lime; -} - -svg.schemascii .component :is(polyline, path, line, polygon, rect, circle):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; + } } } 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..db6ac5e --- /dev/null +++ b/scripts/web_startup.js @@ -0,0 +1,119 @@ +// cSpell:ignore pyodide pyproject +var pyodide; +var console = document.getElementById("console"); +var errors = document.getElementById("errors"); +var css_box = document.getElementById("css"); +var source = document.getElementById("schemascii"); +var download_button = document.getElementById("download"); +var ver_switcher = document.getElementById("version"); +var style_elem = document.getElementById("custom-css"); +var output = document.getElementById("output"); + +var schemascii; +var monkeysrc; + +var ver_map; + +async function main() { + try { + info("Loading Python... "); + pyodide = await loadPyodide({ stdout: info, stderr: error }); + info("done\nInstalling micropip..."); + await pyodide.loadPackage("micropip", { errorCallback: error, messageCallback: () => { } }); + info("done\nFetching versions... "); + monkeysrc = await fetch("scripts/monkeypatch.py").then(r => r.text()); + var foo = await fetch("https://api.github.com/repos/dragoncoder047/schemascii/contents/dist").then(r => r.json()); + foo = foo.filter(x => x.name.endsWith(".whl")).map(x => x.path); + ver_map = Object.fromEntries(foo.map(x => [/\/schemascii-([\d.]+)-/.exec(x)[1], x])) + var all_versions = Object.keys(ver_map); + //all_versions.push("DEV"); + for (var v of all_versions) { + var o = document.createElement("option"); + o.textContent = o.value = v; + ver_switcher.append(o); + } + var latest_version = await fetch("pyproject.toml").then(r => r.text()).then(r => /version = "([\d.]+)"/.exec(r)[1]); + ver_switcher.value = latest_version; + info(`["${all_versions.join('", "')}"]\nlatest=${latest_version}\n`); + await switch_version(); + + css_box.addEventListener("input", debounce(sync_css)); + source.addEventListener("input", debounce(catched(render))); + download_button.addEventListener("click", download); + ver_switcher.addEventListener("change", acatched(switch_version)); + + css_box.value = await fetch("schemascii_example.css").then(r => r.text()); + sync_css(); + source.removeAttribute("disabled"); + css_box.removeAttribute("disabled"); + console.textContent = "Ready"; + } catch (e) { + error(`\nFATAL ERROR:\n${e.stack}\n`); + throw e; + } +} +function monkeypatch() { + pyodide.runPython(monkeysrc); +} +function info(line) { + console.textContent += line; +} +function error(text) { + errors.textContent += text; +} +function debounce(fun) { + var timeout; + return function () { + if (timeout) clearTimeout(timeout); + timeout = setTimeout(fun.bind(this, arguments), 100); + }; +} +function catched(fun) { + return function () { + try { + fun.call(this, arguments); + } catch (e) { + error(e.stack); + } + }; +} +async function acatched(fun) { + return async function() { + try { + await fun.call(this, arguments); + } catch (e) { + error(e.stack); + } + }; +} +function sync_css() { + style_elem.innerHTML = css_box.value; +} +function render() { + console.textContent = ""; + errors.textContent = ""; + output.innerHTML = schemascii.patched_render(source.value); +} + +async function switch_version() { + source.setAttribute("disabled", true); + info("Installing Schemascii version " + ver_switcher.value + "... ") + await pyodide.pyimport("micropip").install(ver_map[ver_switcher.value]); + monkeypatch(); + schemascii = pyodide.runPython("import schemascii\nschemascii"); + info("done\n"); + output.innerHTML = ""; + source.removeAttribute("disabled"); +} + +function download() { + if (!output.innerHTML) return; + var a = document.createElement("a"); + a.setAttribute("href", URL.createObjectURL(new Blob([output.innerHTML], {"type": "application/svg+xml"}))); + a.setAttribute("download", `schemascii_playground_${new Date().toISOString()}_no_css.svg`); + a.click(); +} + +main(); + +// fetch("https://github.com/dragoncoder047/schemascii/zipball/main/").then(r => r.arrayBuffer()).then(b => pyodide.unpackArchive(b, "zip")); diff --git a/test_data/test1.txt b/test_data/test1.txt index 71f203b..a07ca88 100644 --- a/test_data/test1.txt +++ b/test_data/test1.txt @@ -1,10 +1,5 @@ -!padding=30! -G3----------L1------G4 - -G2------L2###########-------G1 -L1:220u -L2:330u -G1:earth -G2:chassis -G3:signal -G4:common +---------------------* + | +---------------------|------ + | +---------------------* diff --git a/test_data/test_charge_pump.txt.svg b/test_data/test_charge_pump.txt.svg index 115782c..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 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