mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-03 09:14:12 +02:00
Merge remote-tracking branch 'upstream/master' into feature/mcp-folio-presets
# Conflicts: # misc/qet-mcp/README.md # misc/qet-mcp/qet_mcp.py
This commit is contained in:
@@ -29,6 +29,9 @@ and 32 pixels, unchanged:
|
||||
ico/breeze/22/<name>.svg
|
||||
ico/breeze/32/<name>.svg
|
||||
|
||||
The settings page icons in BREEZE_PAGES are copied from the one size
|
||||
listed for each, to ico/breeze/<size>/<name>.svg.
|
||||
|
||||
Many Breeze names are symbolic links to another file. The copy holds the
|
||||
linked file's content under the name BREEZE gives, so list the link's
|
||||
target in BREEZE and several QET icons share one copy. Every name needs
|
||||
@@ -39,7 +42,7 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from make_icon_themes import BREEZE, BREEZE_SIZES, ICO # noqa: E402
|
||||
from make_icon_themes import BREEZE, BREEZE_PAGES, BREEZE_SIZES, ICO # noqa: E402
|
||||
|
||||
CATEGORIES = ["actions", "places", "mimetypes", "status", "devices", "apps", "preferences"]
|
||||
OUT = ICO / "breeze"
|
||||
@@ -69,6 +72,14 @@ def main():
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(source.read_bytes())
|
||||
wanted.add(target)
|
||||
for name, size in BREEZE_PAGES.values():
|
||||
source = find(checkout, name, size)
|
||||
if not source:
|
||||
sys.exit(f"{name}: no {size} pixel icon in {checkout}")
|
||||
target = OUT / str(size) / f"{name}.svg"
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(source.read_bytes())
|
||||
wanted.add(target)
|
||||
# Drop icons no longer listed, so ico/breeze/ holds only what the theme uses.
|
||||
for old in sorted(OUT.glob("*/*.svg")):
|
||||
if old not in wanted:
|
||||
|
||||
@@ -94,9 +94,13 @@ Breeze art:
|
||||
another multiple of 16 (32, 64, 128...) the smooth 16 pixel design
|
||||
44 pixels and scalable the smooth 22 pixel design
|
||||
|
||||
The Scale=2 folders follow the table at twice their size. Four icons
|
||||
The Scale=2 folders follow the table at twice their size. Five icons
|
||||
have a 32 pixel design, used in place of both smooth vectors.
|
||||
|
||||
A settings page icon (PAGES) is colored art shown at 64 and 128 pixels:
|
||||
one SVG in the scalable folder serves it. The printer and settings pages
|
||||
use the Breeze art QET's PNGs were exported from (BREEZE_PAGES).
|
||||
|
||||
An SVG is recolored for the dark theme through its text color (see
|
||||
dark_svg). A colored SVG, one without a text color, gets no dark copy
|
||||
and is inherited from the light theme, as a colored PNG is.
|
||||
@@ -110,7 +114,7 @@ touched; the dark theme inherits them from the light one.
|
||||
Qt inherits by name, not by size: once a name has any file in the dark
|
||||
theme, the parent theme is never consulted for that name, and a size the
|
||||
dark theme lacks is served by scaling the nearest dark file. An icon that
|
||||
is line art at 22 pixels and colored at 128 (the printer) would then come
|
||||
is line art at 22 pixels and colored at 128 would then come
|
||||
out as the 22 pixel copy scaled up on a dark palette. So for every name
|
||||
the dark theme holds, the .qrc also aliases the light files of the sizes
|
||||
the dark theme does not have, when they read on the dark window (3:1,
|
||||
@@ -173,6 +177,7 @@ SVGS = [
|
||||
"scalable/align-vertical-center.svg",
|
||||
"scalable/align-vertical-top.svg",
|
||||
"scalable/diagram.svg",
|
||||
"scalable/draw-fillet.svg",
|
||||
"scalable/folio-delete.svg",
|
||||
"scalable/folio-new.svg",
|
||||
"scalable/folio-properties.svg",
|
||||
@@ -197,7 +202,6 @@ BREEZE = {
|
||||
"applications-development-translation": "amarok_change_language",
|
||||
"arrow-left": "go-previous",
|
||||
"arrow-right": "go-next",
|
||||
"circle": "draw-circle",
|
||||
"configure": "configure",
|
||||
"dialog-cancel": "dialog-cancel",
|
||||
"dialog-ok": "dialog-ok-apply",
|
||||
@@ -233,7 +237,6 @@ BREEZE = {
|
||||
"edit-table-insert-row-above": "edit-table-insert-row-above",
|
||||
"edit-table-insert-row-under": "edit-table-insert-row-under",
|
||||
"edit-undo": "edit-undo",
|
||||
"ellipse": "draw-ellipse",
|
||||
"flip": "object-flip-vertical",
|
||||
"folder": "folder",
|
||||
"folder-new": "folder-new",
|
||||
@@ -283,7 +286,6 @@ BREEZE = {
|
||||
"two_pages": "view-pages-facing",
|
||||
"user-busy": "im-user-busy",
|
||||
"user-online": "im-user-online",
|
||||
"view-fit-window": "zoom-fit-best",
|
||||
"view-fullscreen": "view-fullscreen",
|
||||
"view-pim-journal": "view-calendar-journal",
|
||||
"view-refresh": "view-refresh",
|
||||
@@ -299,6 +301,19 @@ BREEZE = {
|
||||
# Sizes Breeze draws its action icons at, the folders of ico/breeze/.
|
||||
BREEZE_SIZES = [16, 22, 32]
|
||||
|
||||
# Settings page icons: colored art shown at 64 and 128 pixels only, so one
|
||||
# scalable SVG serves every size. QET name to Breeze name and the Breeze
|
||||
# size it is copied from (ico/breeze/<size>/<name>.svg). QET's PNGs of
|
||||
# these were exports of the same Breeze art.
|
||||
BREEZE_PAGES = {
|
||||
"printer": ("printer", 64),
|
||||
"settings": ("systemsettings", 48),
|
||||
}
|
||||
|
||||
# Page icons, QET name to SVG, relative to ico/.
|
||||
PAGES = {name: f"breeze/{size}/{breeze}.svg" for name, (breeze, size) in BREEZE_PAGES.items()}
|
||||
PAGES["configure-shortcuts"] = "128x128/configure-shortcuts.svg"
|
||||
|
||||
# Where the generator puts the 22 pixel Breeze art moved onto a 24 pixel
|
||||
# canvas (see on_24_canvas).
|
||||
BREEZE_24 = ICO / "generated" / "breeze-24"
|
||||
@@ -599,7 +614,7 @@ def main():
|
||||
for size in SIZES:
|
||||
folder = ICO / size
|
||||
for png in sorted(folder.glob("*.png")):
|
||||
if png.stem in BREEZE or png.stem in TRACED or png.stem in grid:
|
||||
if png.stem in BREEZE or png.stem in TRACED or png.stem in grid or png.stem in PAGES:
|
||||
continue
|
||||
rel = f"{size}/{png.name}"
|
||||
names = [png.stem]
|
||||
@@ -624,8 +639,9 @@ def main():
|
||||
changed += write_if_changed(target, dark_image)
|
||||
dark.append(f"themes/qet-dark/{size}/{name}.png")
|
||||
|
||||
for rel in SVGS:
|
||||
name = (ICO / rel).name.replace("-symbolic", "")
|
||||
svgs = [((ICO / rel).name.replace("-symbolic", ""), rel) for rel in SVGS]
|
||||
svgs += [(f"{name}.svg", rel) for name, rel in PAGES.items()]
|
||||
for name, rel in svgs:
|
||||
light.append((f"themes/qet/scalable/{name}", rel))
|
||||
text = dark_svg((ICO / rel).read_text(encoding="utf-8"))
|
||||
if text is None:
|
||||
|
||||
@@ -532,6 +532,25 @@ Python, plus the hang guard on `addConductor` and the database refresh in
|
||||
the file until it is saved once: since #1107 QElectroTech works one out
|
||||
from the wire's two ends on load and writes it on the next save, so it
|
||||
appears after a first `qet_edit`.
|
||||
- **Wires can be routed around symbols.** By default a new conductor gets
|
||||
QElectroTech's own two or three straight segments, which run through
|
||||
whatever symbol or wire lies between the two terminals. Give
|
||||
`add_conductor` `"route": "avoid"` to redraw it around the symbols
|
||||
instead, or use `route_conductor` (addressed like `move_conductor_segment`,
|
||||
by `element` + `terminal` or by `"conductor": "{uuid}"`) to redraw one
|
||||
already drawn. The route leaves and enters each terminal in its own
|
||||
direction, runs on the folio grid, stays inside the folio's border, and is
|
||||
the cheapest found by a search that charges for length, for each bend
|
||||
and, less heavily, for running along or crossing another wire. Obstacles
|
||||
are each symbol's own rectangle plus half a grid step; texts, images,
|
||||
shapes and tables are not obstacles. The path is saved as a hand-edited
|
||||
one, so it survives a reload and one undo puts the default back. Where
|
||||
no route exists, the wire keeps its path: `route_conductor` returns
|
||||
`"no-route"` and both ops say so in `note` -- it is not a failure, and
|
||||
the run goes on. Like a hand-edited path, it is stretched rather than
|
||||
rerouted when a symbol is moved afterwards; route again after moving
|
||||
things. Needs `qet.routeConductor()` / `qet.routeConductorBetween()` in
|
||||
the build, and only an edit that routes requires them.
|
||||
- **A terminal can be named by its uuid**: `terminal`, `from_terminal` and
|
||||
`to_terminal` take the terminal's uuid (as `qet_element_info` lists it)
|
||||
in place of its index, on the op's own element (for `add_conductor`, on
|
||||
@@ -557,6 +576,15 @@ Python, plus the hang guard on `addConductor` and the database refresh in
|
||||
791.25 pt` for tabloid landscape from a new folio. (The PDF export makes
|
||||
the page the frame and title block plus its one-pixel line, at 96 pixels
|
||||
an inch: 0.75 pt a pixel.) Needs `qet.folioPresets()` in the build.
|
||||
- **The `wiring` export names unnamed terminals.** Most shipped symbols
|
||||
leave their terminals unnamed, so `from_terminal`/`to_terminal` are often
|
||||
empty. Each row also ends with `from_terminal_index`,
|
||||
`from_terminal_uuid`, `to_terminal_index` and `to_terminal_uuid`: the
|
||||
index `add_conductor` takes, and the uuid the `.qet` names the terminal
|
||||
by (as `qet_edit` accepts in place of the index). The index is empty for
|
||||
a terminal sharing its point with another, where the order is undefined;
|
||||
the uuid tells those apart. `wiring_list_view` carries the same four
|
||||
columns for `qet_query`.
|
||||
- **`qet_export` isolates its launch.** SingleApplication keys its socket
|
||||
on `applicationFilePath()`, so a second launch of the same binary path
|
||||
forwards its request to an already-running instance and returns *that*
|
||||
|
||||
+44
-2
@@ -1361,6 +1361,10 @@ OPS = {
|
||||
"move_conductor_segment": ("moveConductorSegment", [("folio", "folio"), ("element", "elmt"),
|
||||
("terminal", "term"), ("segment", "num"),
|
||||
("dx", "num"), ("dy", "num")]),
|
||||
# Redraws one conductor around the symbols in its way; the result is
|
||||
# "routed", or "no-route" when there is none, the path then unchanged.
|
||||
"route_conductor": ("routeConductor", [("folio", "folio"), ("element", "elmt"),
|
||||
("terminal", "term")]),
|
||||
"link_elements": ("linkElements", [("folio", "folio"), ("element", "elmt"),
|
||||
("to_folio", "folio"), ("to", "elmt")]),
|
||||
"link_plc_io": ("linkElements", [("folio", "folio"), ("element", "elmt"),
|
||||
@@ -1502,7 +1506,16 @@ FOLIO_MAKING_OPS = ("add_folio", "insert_folio")
|
||||
|
||||
# The ops that address one conductor by element + terminal, and so also
|
||||
# take "conductor": "{uuid}" (qet_conductors reports each one's uuid).
|
||||
CONDUCTOR_UUID_OPS = ("set_conductor", "move_conductor_segment", "delete_conductor")
|
||||
CONDUCTOR_UUID_OPS = ("set_conductor", "move_conductor_segment", "delete_conductor",
|
||||
"route_conductor")
|
||||
|
||||
# add_conductor's "route": "default" is the application's own two or three
|
||||
# segments; "avoid" then redraws the new conductor around the symbols.
|
||||
ROUTE_MODES = ["default", "avoid"]
|
||||
|
||||
# Routing arrived after the other drawing verbs, so it is required only by
|
||||
# an edit that routes: every other edit still runs on a build without it.
|
||||
_ROUTE_METHODS = {"routeConductor", "routeConductorBetween"}
|
||||
|
||||
# Accepted by set_conductor. The names are the project file's own, so what
|
||||
# a script sets is what qet_conductors reports back.
|
||||
@@ -1521,7 +1534,7 @@ CONDUCTOR_DEFAULT_PROPERTIES = ["onetextperfolio"] + CONDUCTOR_PROPERTIES
|
||||
# verbs. Probed in the script rather than assumed, because the failure mode
|
||||
# otherwise is a TypeError on line N of a generated file the caller never
|
||||
# sees, reported as "the edit failed".
|
||||
_REQUIRED_METHODS = sorted({m for m, _ in OPS.values() if m} |
|
||||
_REQUIRED_METHODS = sorted(({m for m, _ in OPS.values() if m} - _ROUTE_METHODS) |
|
||||
{"save", "folioCount", "conductorCount", "elementCount"})
|
||||
|
||||
_MARKER = "QETEDIT "
|
||||
@@ -1752,6 +1765,14 @@ def _build_script(operations: list, output: str) -> str:
|
||||
raise ValueError(f"operation {i}: unknown conductor property "
|
||||
f"{op.get('property')!r}; expected one of "
|
||||
f"{', '.join(CONDUCTOR_PROPERTIES)}")
|
||||
route = op.get("route", "default") if name == "add_conductor" else "default"
|
||||
if route not in ROUTE_MODES:
|
||||
raise ValueError(f"operation {i}: unknown route {route!r}; "
|
||||
f"expected one of {', '.join(ROUTE_MODES)}")
|
||||
if name == "route_conductor":
|
||||
uuid_methods.add("routeConductor")
|
||||
if route == "avoid":
|
||||
uuid_methods.add("routeConductorBetween")
|
||||
if name == "set_folio" and op.get("property") not in FOLIO_PROPERTIES:
|
||||
raise ValueError(f"operation {i}: unknown folio property "
|
||||
f"{op.get('property')!r}; expected one of "
|
||||
@@ -1857,6 +1878,18 @@ def _build_script(operations: list, output: str) -> str:
|
||||
f"' rows of ' + qet.folioBorder({f}, 'row-height') + '; frame ' + "
|
||||
f"Math.ceil(Number(qet.folioBorder({f}, 'width')) + 1) * 0.75 + ' x ' + "
|
||||
f"Math.ceil(Number(qet.folioBorder({f}, 'height')) + 1) * 0.75 + ' pt'}}));")
|
||||
if route == "avoid":
|
||||
# The wire exists either way; a route not found leaves it on the
|
||||
# default path, which is a note on the op, not a failure of it.
|
||||
lines.append(f" if (v{i} === true) {{ var r{i} = qet.routeConductorBetween("
|
||||
f"{', '.join(args)}); if (r{i} !== 'routed') qet.log({_js(_MARKER)} + "
|
||||
f"JSON.stringify({{kind: 'op_note', index: {i}, note: 'no route "
|
||||
f"around the symbols was found; the conductor keeps the default "
|
||||
f"path'}})); }}")
|
||||
if name == "route_conductor":
|
||||
lines.append(f" if (v{i} === 'no-route') qet.log({_js(_MARKER)} + "
|
||||
f"JSON.stringify({{kind: 'op_note', index: {i}, note: 'no route "
|
||||
f"around the symbols was found; the conductor keeps its path'}}));")
|
||||
if ident is not None:
|
||||
lines.append(f" R[{_js(ident)}] = v{i};")
|
||||
refs.add(ident)
|
||||
@@ -3551,6 +3584,15 @@ TOOLS = [
|
||||
"movable ones, then a static segment, in that order from the "
|
||||
"first terminal; call with a guessed index and read \"succeeded\" "
|
||||
"to check it landed on a movable one. "
|
||||
"add_conductor takes an optional \"route\": \"avoid\" to "
|
||||
"redraw the new wire around the symbols in its way instead of "
|
||||
"QElectroTech's default two or three straight segments; "
|
||||
"route_conductor does the same to an existing conductor "
|
||||
"(addressed like move_conductor_segment, or by \"conductor\") "
|
||||
"and returns \"routed\" or \"no-route\". Where no route "
|
||||
"exists the wire keeps its path and the op's note says so -- "
|
||||
"not a failure. Route after placing everything: moving a "
|
||||
"symbol later stretches the routed path, it does not reroute it. "
|
||||
"link_elements takes a folio for each end, since a master "
|
||||
"and its slave are usually on different ones. "
|
||||
"delete_conductor removes only the conductor on the named "
|
||||
|
||||
@@ -22,6 +22,7 @@ away: a check that has never been seen to fail is not evidence.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -290,6 +291,35 @@ class EditValidation(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, "saved before conductors carried a uuid"):
|
||||
self.build([{"op": "delete_conductor", "folio": 0, "conductor": ""}])
|
||||
|
||||
def test_route_ops(self):
|
||||
"""route_conductor and add_conductor's "route": "avoid" call the
|
||||
router; neither makes a missing route a failure of the run, and the
|
||||
router methods are required only by an edit that routes."""
|
||||
E, F = "{11111111-2222-4333-8444-555555555555}", "{21111111-2222-4333-8444-555555555555}"
|
||||
U = "{31111111-2222-4333-8444-555555555555}"
|
||||
plain = self.build([{"op": "add_conductor", "folio": 0, "from": E, "from_terminal": 1,
|
||||
"to": F, "to_terminal": 0}])
|
||||
self.assertNotIn("routeConductor", plain)
|
||||
self.assertEqual(plain, self.build([{"op": "add_conductor", "folio": 0, "from": E,
|
||||
"from_terminal": 1, "to": F, "to_terminal": 0,
|
||||
"route": "default"}]))
|
||||
s = self.build([{"op": "add_conductor", "folio": 0, "from": E, "from_terminal": 1,
|
||||
"to": F, "to_terminal": 0, "route": "avoid"}])
|
||||
self.assertIn(f'if (v0 === true) {{ var r0 = qet.routeConductorBetween(0, "{E}", 1, "{F}", 0);', s)
|
||||
self.assertIn('"routeConductorBetween"', s)
|
||||
self.assertNotIn('"routeConductor",', s)
|
||||
# the op's own result is still the conductor being added
|
||||
self.assertIn("result: v0", s)
|
||||
s = self.build([{"op": "route_conductor", "folio": 2, "conductor": U}])
|
||||
self.assertIn("(e0 ? qet.routeConductor(2, e0.element, e0.terminal) : false)", s)
|
||||
self.assertIn("if (v0 === 'no-route')", s)
|
||||
self.assertIn('"routeConductor"', s)
|
||||
s = self.build([{"op": "route_conductor", "folio": 0, "element": E, "terminal": 1}])
|
||||
self.assertIn(f'qet.routeConductor(0, "{E}", 1)', s)
|
||||
with self.assertRaisesRegex(ValueError, "unknown route"):
|
||||
self.build([{"op": "add_conductor", "folio": 0, "from": E, "from_terminal": 1,
|
||||
"to": F, "to_terminal": 0, "route": "around"}])
|
||||
|
||||
def test_terminal_by_uuid(self):
|
||||
"""A terminal named by uuid is turned into its index at run time, on
|
||||
the element it belongs to: the op's element, or each add_conductor
|
||||
@@ -352,6 +382,8 @@ class EditValidation(unittest.TestCase):
|
||||
"move_conductor_segment": el + [{"op": "move_conductor_segment", "folio": "$f",
|
||||
"element": "$e", "terminal": 0, "segment": 1,
|
||||
"dx": 10, "dy": 0}],
|
||||
"route_conductor": el + [{"op": "route_conductor", "folio": "$f", "element": "$e",
|
||||
"terminal": 0}],
|
||||
"delete_conductor": el + [{"op": "delete_conductor", "folio": "$f", "element": "$e", "terminal": 0}],
|
||||
"link_elements": two + [{"op": "link_elements", "folio": "$f", "element": "$e",
|
||||
"to_folio": "$f", "to": "$e2"}],
|
||||
@@ -3698,6 +3730,44 @@ class Integration(unittest.TestCase):
|
||||
note = r["operations"][1].get("note", "")
|
||||
self.assertIn("from_terminal: no terminal", note)
|
||||
self.assertIn("to_terminal: no terminal", note)
|
||||
def test_wiring_export_names_unnamed_terminals(self):
|
||||
"""--export-wiring gives each end the index add_conductor took and
|
||||
the uuid the file names the terminal by, even where the terminal
|
||||
has no name. This strip's file lists its terminals in another order
|
||||
than the index (top to bottom, then left to right), so a column
|
||||
holding the file's order would fail here."""
|
||||
strip = ("common://10_electric/10_allpole/130_terminals_terminal_strips/"
|
||||
"90_terminal_strips_diagram/90-10-0211.elmt")
|
||||
base = self.sb.new()
|
||||
r = self.ok(self.sb.edit(base, [
|
||||
{"op": "add_element", "id": "a", "folio": 0, "path": strip, "x": 100, "y": 200},
|
||||
{"op": "add_element", "id": "b", "folio": 0, "path": strip, "x": 400, "y": 200},
|
||||
{"op": "add_conductor", "folio": 0, "from": "$a", "from_terminal": 2,
|
||||
"to": "$b", "to_terminal": 3},
|
||||
{"op": "add_conductor", "folio": 0, "from": "$a", "from_terminal": 1,
|
||||
"to": "$b", "to_terminal": 0}]))
|
||||
a_uuid, b_uuid = (o["result"] for o in r["operations"][:2])
|
||||
out = self.sb.p("wiring.csv")
|
||||
e = m.tool_export(BINARY, r["output"], "wiring", out)
|
||||
self.assertTrue(e["ok"], e)
|
||||
with open(out, encoding="utf-8-sig", newline="") as fh:
|
||||
rows = list(csv.DictReader(fh, delimiter=";"))
|
||||
self.assertEqual(len(rows), 2)
|
||||
# the conductor in the file: which uuid it names each end by
|
||||
in_file = {c.get("uuid"): {(c.get("element1"), c.get("terminal1")),
|
||||
(c.get("element2"), c.get("terminal2"))}
|
||||
for c in ET.parse(r["output"]).getroot().iter("conductor")
|
||||
if c.get("element1")}
|
||||
got = set()
|
||||
for row in rows:
|
||||
self.assertEqual((row["from_terminal"], row["to_terminal"]), ("", ""))
|
||||
ends = {(int(row["from_terminal_index"]), row["from_terminal_uuid"]),
|
||||
(int(row["to_terminal_index"]), row["to_terminal_uuid"])}
|
||||
self.assertEqual({u for _, u in ends},
|
||||
{t for _, t in in_file[row["conductor_uuid"]]})
|
||||
got.add(frozenset(i for i, _ in ends))
|
||||
self.assertEqual(got, {frozenset({2, 3}), frozenset({1, 0})})
|
||||
|
||||
def test_folio_ref_survives_an_insert_before_it(self):
|
||||
"""add_folio names a folio "$f"; inserting another at position 0
|
||||
moves it from index 1 to 2. "$f" must still name it."""
|
||||
@@ -3829,6 +3899,65 @@ class Integration(unittest.TestCase):
|
||||
"terminal": 0, "segment": 99, "dx": 1, "dy": 1}])
|
||||
self.assertFalse(r["ok"])
|
||||
|
||||
def _column_with_a_symbol_between(self):
|
||||
"""Three coils in a column; a wire from the bottom of the first to
|
||||
the top of the third runs straight through the middle one by
|
||||
default."""
|
||||
return [{"op": "add_folio", "id": "f"},
|
||||
{"op": "add_element", "id": "a", "folio": "$f", "path": COIL, "x": 200, "y": 100},
|
||||
{"op": "add_element", "id": "b", "folio": "$f", "path": COIL, "x": 200, "y": 250},
|
||||
{"op": "add_element", "id": "c", "folio": "$f", "path": COIL, "x": 200, "y": 400}]
|
||||
|
||||
@staticmethod
|
||||
def _crosses_the_middle_coil(xml):
|
||||
"""Whether the saved path enters the middle coil. The path starts
|
||||
at the first coil's A2 terminal, (200, 120); relative to that the
|
||||
middle coil (40 x 60, hotspot 17,32, placed at 200,250) covers
|
||||
x -17..23, y 98..158."""
|
||||
segs = re.findall(r'<segment length="([-0-9.]+)" orientation="(\w+)"', xml)
|
||||
x = y = 0.0
|
||||
for length, orientation in segs:
|
||||
nx, ny = (x + float(length), y) if orientation == "horizontal" else (x, y + float(length))
|
||||
lo_x, hi_x, lo_y, hi_y = min(x, nx), max(x, nx), min(y, ny), max(y, ny)
|
||||
if lo_x < 23 and hi_x > -17 and lo_y < 158 and hi_y > 98:
|
||||
return True
|
||||
x, y = nx, ny
|
||||
return False
|
||||
|
||||
def test_route_avoid_goes_around_the_symbol_between(self):
|
||||
base = self.sb.new()
|
||||
ops = self._column_with_a_symbol_between()
|
||||
wire = {"op": "add_conductor", "folio": "$f", "from": "$a", "from_terminal": 1,
|
||||
"to": "$c", "to_terminal": 0}
|
||||
# the arm that must differ: the default path is straight, saved
|
||||
# without segments, and so through the middle coil
|
||||
plain = self.ok(self.sb.edit(base, ops + [wire], out="plain.qet"))
|
||||
self.assertNotIn("<segment", Path(plain["output"]).read_text(encoding="utf-8"))
|
||||
r = self.ok(self.sb.edit(base, ops + [{**wire, "route": "avoid"}], out="routed.qet"))
|
||||
self.assertNotIn("note", r["operations"][-1])
|
||||
xml = Path(r["output"]).read_text(encoding="utf-8")
|
||||
self.assertIn("<segment", xml, "a routed path is saved as segments")
|
||||
self.assertFalse(self._crosses_the_middle_coil(xml))
|
||||
# the check can fail: the straight default path, as segments
|
||||
self.assertTrue(self._crosses_the_middle_coil(
|
||||
'<segment length="260" orientation="vertical"/>'))
|
||||
# one undo puts the default path back
|
||||
u = self.ok(self.sb.edit(base, ops + [{**wire, "route": "avoid"}, {"op": "undo"}],
|
||||
out="undone.qet"))
|
||||
self.assertNotIn("<segment", Path(u["output"]).read_text(encoding="utf-8"))
|
||||
|
||||
def test_route_conductor_reroutes_an_existing_wire(self):
|
||||
base = self.sb.new()
|
||||
ops = self._column_with_a_symbol_between() + [
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$a", "from_terminal": 1,
|
||||
"to": "$c", "to_terminal": 0},
|
||||
{"op": "route_conductor", "folio": "$f", "element": "$a", "terminal": 1}]
|
||||
r = self.ok(self.sb.edit(base, ops))
|
||||
self.assertEqual(r["operations"][-1]["result"], "routed")
|
||||
xml = Path(r["output"]).read_text(encoding="utf-8")
|
||||
self.assertIn("<segment", xml)
|
||||
self.assertFalse(self._crosses_the_middle_coil(xml))
|
||||
|
||||
# ---- search and replace ----
|
||||
|
||||
def test_search_and_replace_element_info_across_folios(self):
|
||||
|
||||
Reference in New Issue
Block a user