From 5e28e015e077e70653f771184b2d881b0d78f01e Mon Sep 17 00:00:00 2001 From: ispyisail Date: Fri, 2 Oct 2026 22:28:11 +1300 Subject: [PATCH 1/2] Scripting API and MCP: route a conductor around the symbols in its way A new conductor gets the default two or three straight segments, which run through whatever symbol or wire lies between its terminals; scripts then fix it segment by segment with moveConductorSegment(). ConductorRouter finds an orthogonal path on the folio grid that leaves and enters each terminal in its own direction, keeps clear of every element's rectangle, stays inside the border, and charges for bends and for running along or crossing other wires. qet.routeConductor() and qet.routeConductorBetween() apply it through Conductor::setPathPoints(), which pushes the same ChangeConductorCommand a handle drag does, so the path is saved, survives a reload, and one undo restores the default. Where no route exists the wire keeps its path and the call says so. qet-mcp: add_conductor takes "route": "avoid", and a route_conductor op reroutes an existing conductor (by terminal or by uuid). The router methods are required only by an edit that routes. Co-Authored-By: Claude Opus 5.5 (1M context) --- cmake/qet_compilation_vars.cmake | 2 + misc/qet-mcp/README.md | 19 ++ misc/qet-mcp/qet_mcp.py | 46 +++- misc/qet-mcp/test_qet_mcp.py | 90 ++++++++ sources/conductorrouter.cpp | 304 ++++++++++++++++++++++++++ sources/conductorrouter.h | 77 +++++++ sources/qetgraphicsitem/conductor.cpp | 40 ++++ sources/qetgraphicsitem/conductor.h | 1 + sources/scripting/qetscriptapi.cpp | 114 ++++++++++ sources/scripting/qetscriptapi.h | 6 + tests/qttest/CMakeLists.txt | 10 + tests/qttest/tst_conductorrouter.cpp | 212 ++++++++++++++++++ 12 files changed, 919 insertions(+), 2 deletions(-) create mode 100644 sources/conductorrouter.cpp create mode 100644 sources/conductorrouter.h create mode 100644 tests/qttest/tst_conductorrouter.cpp diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 66a810712..efe2f1992 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -179,6 +179,8 @@ set(QET_SRC_FILES ${QET_DIR}/sources/wiringlistexport.cpp ${QET_DIR}/sources/wirehops.h ${QET_DIR}/sources/wirehops.cpp + ${QET_DIR}/sources/conductorrouter.h + ${QET_DIR}/sources/conductorrouter.cpp ${QET_DIR}/sources/ui/wiringlistdialog.h ${QET_DIR}/sources/ui/wiringlistdialog.cpp ${QET_DIR}/sources/conductornumexport.h diff --git a/misc/qet-mcp/README.md b/misc/qet-mcp/README.md index cd53ec880..16928640c 100644 --- a/misc/qet-mcp/README.md +++ b/misc/qet-mcp/README.md @@ -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 diff --git a/misc/qet-mcp/qet_mcp.py b/misc/qet-mcp/qet_mcp.py index 257b1c294..ca9b597ba 100755 --- a/misc/qet-mcp/qet_mcp.py +++ b/misc/qet-mcp/qet_mcp.py @@ -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"), @@ -1495,7 +1499,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. @@ -1514,7 +1527,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 " @@ -1745,6 +1758,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 " @@ -1830,6 +1851,18 @@ def _build_script(operations: list, output: str) -> str: lines.append(f" var e{i} = qetMcpConductorEnd({i}, {args[0]}, {conductor_js});") call = f"(e{i} ? {call} : false)" lines.append(f" var v{i} = {call};") + 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) @@ -3524,6 +3557,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 " diff --git a/misc/qet-mcp/test_qet_mcp.py b/misc/qet-mcp/test_qet_mcp.py index 0f7871168..95bf7b918 100644 --- a/misc/qet-mcp/test_qet_mcp.py +++ b/misc/qet-mcp/test_qet_mcp.py @@ -281,6 +281,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 @@ -343,6 +372,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"}], @@ -3806,6 +3837,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' -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("')) + # 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(". +*/ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "conductorrouter.h" + +#include +#include +#include +#include +#include + +namespace { + +using ConductorRouter::Direction; + +constexpr qreal eps = 1e-6; + ///Larger than any folio needs: a 2000 x 2000 grid +constexpr int max_nodes = 4000000; + + ///What each thing costs, in scene units of length. A bend is worth + ///three grid steps of extra wire, so the search prefers a slightly + ///longer route to a more crooked one, as a person drawing would. +constexpr qreal bend_steps = 3.0; + ///Running along another wire costs this much more per unit of length +constexpr qreal along_factor = 3.0; + ///Each crossing of another wire costs two grid steps +constexpr qreal cross_steps = 2.0; + +QPointF step(Direction d) +{ + switch (d) { + case Direction::North: return {0, -1}; + case Direction::East: return {1, 0}; + case Direction::South: return {0, 1}; + case Direction::West: return {-1, 0}; + } + return {0, 0}; +} + +Direction opposite(Direction d) +{ + return static_cast((static_cast(d) + 2) % 4); +} + +bool strictlyInside(const QPointF &p, const QRectF &r) +{ + return p.x() > r.left() + eps && p.x() < r.right() - eps + && p.y() > r.top() + eps && p.y() < r.bottom() - eps; +} + +bool insideAny(const QPointF &p, const QList &rects) +{ + for (const QRectF &r : rects) + if (strictlyInside(p, r)) return true; + return false; +} + + ///A wire segment, kept as the fixed coordinate and the range along the + ///other axis +struct Span { qreal at, from, to; }; + + ///The first point of a route after a terminal: one grid step out in the + ///terminal's direction, snapped to the grid the way + ///Conductor::extendTerminal() snaps it, then on until it is clear of + ///every obstacle -- of the terminal's own symbol above all. +bool exitPoint(const QPointF &dock, Direction d, const ConductorRouter::Request &r, + const QList &obstacles, QPointF &out) +{ + const QPointF s = step(d); + QPointF p = dock; + if (s.x() != 0) + p.setX(std::round((dock.x() + s.x() * r.grid) / r.grid) * r.grid); + else + p.setY(std::round((dock.y() + s.y() * r.grid) / r.grid) * r.grid); + for (int i = 0; i < 200; ++i) { + if (!insideAny(p, obstacles)) { + out = p; + return true; + } + p += s * r.grid; + } + return false; +} + + ///Sorted, without duplicates: the grid lines inside [low, high], plus + ///the extra coordinates (the two exit points, which need not be on the + ///grid when a terminal is not) +QVector axis(qreal low, qreal high, qreal grid, std::initializer_list extra) +{ + QVector v; + for (qreal c = std::ceil(low / grid) * grid; c <= high + eps; c += grid) + v << c; + for (qreal c : extra) v << c; + std::sort(v.begin(), v.end()); + v.erase(std::unique(v.begin(), v.end(), + [](qreal a, qreal b) { return std::abs(a - b) < eps; }), + v.end()); + return v; +} + +int indexOf(const QVector &v, qreal c) +{ + auto it = std::lower_bound(v.begin(), v.end(), c - eps); + return (it != v.end() && std::abs(*it - c) < eps) ? int(it - v.begin()) : -1; +} + + ///The cost of the edge along one axis from a to b at fixed coordinate at, + ///or infinity if it crosses an obstacle. horizontal says which axis. +qreal edgeCost(bool horizontal, qreal at, qreal a, qreal b, + const QList &obstacles, + const QVector &along, const QVector &across, + qreal grid) +{ + for (const QRectF &r : obstacles) { + const qreal lo = horizontal ? r.left() : r.top(); + const qreal hi = horizontal ? r.right() : r.bottom(); + const qreal flo = horizontal ? r.top() : r.left(); + const qreal fhi = horizontal ? r.bottom(): r.right(); + if (at > flo + eps && at < fhi - eps + && std::min(b, hi) - std::max(a, lo) > eps) + return std::numeric_limits::infinity(); + } + qreal cost = b - a; + for (const Span &s : along) { + if (std::abs(s.at - at) > 0.5) continue; + const qreal overlap = std::min(b, s.to) - std::max(a, s.from); + if (overlap > eps) cost += overlap * along_factor; + } + for (const Span &s : across) { + // Half-open (a, b], so a crossing on a grid node is counted + // once, by the edge that ends on it; and only through the + // other wire's interior, not at its end, which is a junction. + if (s.at > a + eps && s.at <= b + eps + && at > s.from + 0.5 && at < s.to - 0.5) + cost += cross_steps * grid; + } + return cost; +} + +} // namespace + +ConductorRouter::Result ConductorRouter::route(const Request &r) +{ + Result result; + if (r.grid <= 0) { + result.error = QStringLiteral("the grid step must be positive"); + return result; + } + + QList obstacles; + for (const QRectF &o : r.obstacles) + obstacles << o.normalized().adjusted(-r.margin, -r.margin, r.margin, r.margin); + + QPointF s1, s2; + if (!exitPoint(r.start, r.start_direction, r, obstacles, s1) + || !exitPoint(r.end, r.end_direction, r, obstacles, s2)) { + result.error = QStringLiteral("a terminal has no way out of the symbols around it"); + return result; + } + + // The area searched: everything involved, with room to go round + // it, kept on the folio when there is one. + QRectF box = QRectF(s1, s2).normalized(); + for (const QRectF &o : obstacles) box = box.united(o); + box.adjust(-3 * r.grid, -3 * r.grid, 3 * r.grid, 3 * r.grid); + if (r.bounds.isValid()) box = box.intersected(r.bounds); + box = box.united(QRectF(s1, s2).normalized()); + + const QVector xs = axis(box.left(), box.right(), r.grid, {s1.x(), s2.x()}); + const QVector ys = axis(box.top(), box.bottom(), r.grid, {s1.y(), s2.y()}); + const int nx = xs.size(), ny = ys.size(); + if (qint64(nx) * ny > max_nodes) { + result.error = QStringLiteral("the area to search is too large"); + return result; + } + + QVector horizontal_wires, vertical_wires; + for (const QVector &w : r.wires) { + for (int i = 0; i + 1 < w.size(); ++i) { + const QPointF a = w.at(i), b = w.at(i + 1); + if (std::abs(a.y() - b.y()) < 0.5 && std::abs(a.x() - b.x()) > eps) + horizontal_wires << Span{a.y(), std::min(a.x(), b.x()), std::max(a.x(), b.x())}; + else if (std::abs(a.x() - b.x()) < 0.5 && std::abs(a.y() - b.y()) > eps) + vertical_wires << Span{a.x(), std::min(a.y(), b.y()), std::max(a.y(), b.y())}; + } + } + + // The cost of the edge from each node to the next one east + // (h_cost) and south (v_cost); infinity where blocked or none. + const qreal inf = std::numeric_limits::infinity(); + std::vector h_cost(size_t(nx) * ny, inf), v_cost(size_t(nx) * ny, inf); + for (int j = 0; j < ny; ++j) + for (int i = 0; i + 1 < nx; ++i) + h_cost[size_t(j) * nx + i] = edgeCost(true, ys[j], xs[i], xs[i + 1], obstacles, + horizontal_wires, vertical_wires, r.grid); + for (int i = 0; i < nx; ++i) + for (int j = 0; j + 1 < ny; ++j) + v_cost[size_t(j) * nx + i] = edgeCost(false, xs[i], ys[j], ys[j + 1], obstacles, + vertical_wires, horizontal_wires, r.grid); + + const int start_node = indexOf(ys, s1.y()) * nx + indexOf(xs, s1.x()); + const int goal_node = indexOf(ys, s2.y()) * nx + indexOf(xs, s2.x()); + const qreal bend = bend_steps * r.grid; + // The wire arrives at the second terminal moving opposite to the + // way it points. + const Direction arrival = opposite(r.end_direction); + + // Dijkstra over (node, direction of travel): the direction is what + // lets a bend be charged for. + const size_t states = size_t(nx) * ny * 4; + std::vector dist(states, inf); + std::vector previous(states, -1); + using Entry = std::pair; + std::priority_queue, std::greater> queue; + const int first = start_node * 4 + int(r.start_direction); + dist[size_t(first)] = 0; + queue.push({0, first}); + + qreal best = inf; + int best_state = -1; + while (!queue.empty()) { + const auto [cost, state] = queue.top(); + queue.pop(); + if (cost > dist[size_t(state)] || cost >= best) { + if (cost >= best) break; + continue; + } + const int node = state / 4; + const Direction d = static_cast(state % 4); + if (node == goal_node && d != r.end_direction) { + const qreal total = cost + (d == arrival ? 0 : bend); + if (total < best) { best = total; best_state = state; } + } + const int i = node % nx, j = node / nx; + for (int k = 0; k < 4; ++k) { + const Direction nd = static_cast(k); + if (nd == opposite(d)) continue; + int ni = i, nj = j; + qreal edge = inf; + switch (nd) { + case Direction::East: if (i + 1 < nx) { ni = i + 1; edge = h_cost[size_t(j) * nx + i]; } break; + case Direction::West: if (i > 0) { ni = i - 1; edge = h_cost[size_t(j) * nx + ni]; } break; + case Direction::South: if (j + 1 < ny) { nj = j + 1; edge = v_cost[size_t(j) * nx + i]; } break; + case Direction::North: if (j > 0) { nj = j - 1; edge = v_cost[size_t(nj) * nx + i]; } break; + } + if (edge == inf) continue; + const qreal next_cost = cost + edge + (nd == d ? 0 : bend); + const int next = (nj * nx + ni) * 4 + k; + if (next_cost < dist[size_t(next)]) { + dist[size_t(next)] = next_cost; + previous[size_t(next)] = state; + queue.push({next_cost, next}); + } + } + } + + if (best_state < 0) { + result.error = QStringLiteral("no path around the symbols was found"); + return result; + } + + QList chain; + for (int s = best_state; s >= 0; s = previous[size_t(s)]) { + const int node = s / 4; + chain.prepend(QPointF(xs[node % nx], ys[node / nx])); + } + + // Keep only the corners. The two exit points stay even when + // straight on, as Conductor::generateConductorPath() keeps them: + // the segment from a terminal to its exit point is the one the + // application holds fixed when a wire is edited by hand. + QList corners; + for (int k = 0; k < chain.size(); ++k) { + const QPointF p = chain.at(k); + if (k > 0 && k + 1 < chain.size()) { + const QPointF a = chain.at(k - 1), b = chain.at(k + 1); + const bool straight = (std::abs(a.x() - p.x()) < eps && std::abs(b.x() - p.x()) < eps) + || (std::abs(a.y() - p.y()) < eps && std::abs(b.y() - p.y()) < eps); + if (straight) continue; + } + corners << p; + } + + result.points << r.start; + for (const QPointF &p : corners) + if (result.points.last() != p) result.points << p; + if (result.points.last() != r.end) result.points << r.end; + return result; +} diff --git a/sources/conductorrouter.h b/sources/conductorrouter.h new file mode 100644 index 000000000..1fa6b65fe --- /dev/null +++ b/sources/conductorrouter.h @@ -0,0 +1,77 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef CONDUCTORROUTER_H +#define CONDUCTORROUTER_H + +#include +#include +#include +#include +#include + +/** + @brief The ConductorRouter namespace + Finds a path for a conductor that goes around obstacles -- the bounding + rectangles of the symbols on the folio -- instead of the default two or + three straight segments, which run through whatever lies between the two + terminals. Kept free of any QGraphicsItem so the geometry can be tested + on its own. + + The path is made of horizontal and vertical segments only, like every + conductor. It leaves each terminal in the terminal's own direction, + runs on the folio grid, and is the cheapest one found by a search that + charges for length, for every bend, and for running along or crossing + the wires already drawn. +*/ +namespace ConductorRouter +{ + ///The direction a terminal points, outward from its symbol. + ///Same order as Qet::Orientation. + enum class Direction { North, East, South, West }; + + struct Request + { + QPointF start; ///< the first terminal's docking point + Direction start_direction = Direction::North; + QPointF end; ///< the second terminal's docking point + Direction end_direction = Direction::North; + ///Areas no segment may cross. A margin is added to each. + QList obstacles; + ///The other wires on the folio, each as its list of points. + ///Running along one or crossing one costs extra. + QList> wires; + ///The route stays inside this, when it is valid (the folio). + QRectF bounds; + qreal grid = 10.0; + ///Added around each obstacle, so a route keeps clear of it + qreal margin = 5.0; + }; + + struct Result + { + ///From start to end, both included; empty if none was found + QList points; + ///Why there is no route, when points is empty + QString error; + }; + + Result route(const Request &request); +} + +#endif // CONDUCTORROUTER_H diff --git a/sources/qetgraphicsitem/conductor.cpp b/sources/qetgraphicsitem/conductor.cpp index 4d658129f..80a8c7124 100644 --- a/sources/qetgraphicsitem/conductor.cpp +++ b/sources/qetgraphicsitem/conductor.cpp @@ -1389,6 +1389,46 @@ bool Conductor::moveSegment(int index, qreal dx, qreal dy) return true; } +/** + @brief Conductor::setPathPoints + Replace this conductor's path by the one through @p scene_points, and + push one undo step for it -- the same ChangeConductorCommand a manual + handle drag or moveSegment() pushes, so the path is saved as a + modified one and survives a reload. + @param scene_points in scene coordinates, from terminal1's docking + point to terminal2's, every segment horizontal or vertical + @return false, changing nothing, if the points do not make such a path +*/ +bool Conductor::setPathPoints(const QList &scene_points) +{ + if (scene_points.size() < 2 || !terminal1 || !terminal2) return false; + const auto near = [](const QPointF &a, const QPointF &b) { + return qAbs(a.x() - b.x()) < 0.5 && qAbs(a.y() - b.y()) < 0.5; + }; + if (!near(scene_points.first(), terminal1->dockConductor()) || + !near(scene_points.last(), terminal2->dockConductor())) + return false; + QList points; + for (int i = 0; i < scene_points.size(); ++i) { + const QPointF p = scene_points.at(i); + if (i && qAbs(p.x() - scene_points.at(i - 1).x()) > 0.01 + && qAbs(p.y() - scene_points.at(i - 1).y()) > 0.01) + return false; + points << mapFromScene(p); + } + // A two-point path has nothing to segment between: let the + // application draw it, as for a conductor with no profile. + if (points.size() < 3) return false; + + before_mov_text_pos_ = m_text_item->pos(); + pointsToSegments(points); + modified_path = true; + segmentsToPath(); + calculateTextItemPosition(); + saveProfile(); + return true; +} + /** @brief Conductor::length @return the length of this conductor diff --git a/sources/qetgraphicsitem/conductor.h b/sources/qetgraphicsitem/conductor.h index 46976129a..2cbe76f27 100644 --- a/sources/qetgraphicsitem/conductor.h +++ b/sources/qetgraphicsitem/conductor.h @@ -116,6 +116,7 @@ class Conductor : public QGraphicsObject QVector handlerPoints() const; const QList segmentsList() const; bool moveSegment(int index, qreal dx, qreal dy); + bool setPathPoints(const QList &scene_points); void setPropertyToPotential( const ConductorProperties &property, diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index 54b0d9054..63fdd0098 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -42,6 +42,7 @@ #include "../qetgraphicsitem/conductor.h" #include "../qetgraphicsitem/conductortextitem.h" #include "../conductorsegment.h" +#include "../conductorrouter.h" #include "../qetgraphicsitem/diagramimageitem.h" // See diagrameventaddpdf.h: a missing QtPdf module (or Qt < 6.4) is not @@ -1194,6 +1195,119 @@ bool QetScriptApi::moveConductorSegment(int folioIndex, const QString &elementUu return conductor->moveSegment(segmentIndex, dx, dy); } +namespace { +ConductorRouter::Direction routerDirection(Qet::Orientation o) +{ + switch (o) { + case Qet::North: return ConductorRouter::Direction::North; + case Qet::East: return ConductorRouter::Direction::East; + case Qet::South: return ConductorRouter::Direction::South; + case Qet::West: return ConductorRouter::Direction::West; + } + return ConductorRouter::Direction::North; +} +} // namespace + +/** + @brief QetScriptApi::applyRoute + Redraw @p conductor around the symbols on its folio (ConductorRouter), + as one undo step through Conductor::setPathPoints() -- the same + ChangeConductorCommand a handle drag pushes, so the path is saved and + survives a reload. Obstacles are every element's own rectangle, its + texts left out; the other conductors are not obstacles but cost extra + to run along or cross. + @return "routed", or "no-route" with the reason logged when there is + no such path -- the conductor then keeps the path it had. Not a + failure: the wire exists and joins the right terminals either way. +*/ +QString QetScriptApi::applyRoute(Conductor *conductor, const QString &caller) +{ + Diagram *diagram = conductor->diagram(); + if (!diagram) return QString(); + + ConductorRouter::Request request; + request.start = conductor->terminal1->dockConductor(); + request.start_direction = routerDirection(conductor->terminal1->orientation()); + request.end = conductor->terminal2->dockConductor(); + request.end_direction = routerDirection(conductor->terminal2->orientation()); + request.grid = Diagram::xGrid; + request.bounds = diagram->border_and_titleblock.insideBorderRect(); + for (Element *e : diagram->elements()) + request.obstacles << e->mapRectToScene(e->boundingRect()); + for (Conductor *other : diagram->conductors()) { + if (other == conductor) continue; + QVector wire; + const QList segs = other->segmentsList(); + for (ConductorSegment *seg : segs) { + if (wire.isEmpty()) wire << other->mapToScene(seg->firstPoint()); + wire << other->mapToScene(seg->secondPoint()); + } + request.wires << wire; + } + + const ConductorRouter::Result route = ConductorRouter::route(request); + if (route.points.isEmpty()) { + log(QStringLiteral("qet.%1: %2; the conductor keeps its path") + .arg(caller, route.error)); + return QStringLiteral("no-route"); + } + if (!conductor->setPathPoints(route.points)) { + log(QStringLiteral("qet.%1: the route found does not join the two terminals; " + "the conductor keeps its path").arg(caller)); + return QStringLiteral("no-route"); + } + return QStringLiteral("routed"); +} + +/** + @brief QetScriptApi::routeConductor + Redraw the conductor on a terminal so it goes around the symbols in + its way instead of through them -- see applyRoute(). Addressed as the + other conductor calls address one. + @return "routed", "no-route" (path unchanged, reason logged), or an + empty string if there is no such conductor or the project is read-only +*/ +QString QetScriptApi::routeConductor(int folioIndex, const QString &elementUuid, + int terminalIndex) +{ + if (!m_project) return QString(); + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.routeConductor: project is read-only")); + return QString(); + } + Conductor *conductor = findConductor(folioIndex, elementUuid, terminalIndex, + QStringLiteral("routeConductor")); + if (!conductor) return QString(); + return applyRoute(conductor, QStringLiteral("routeConductor")); +} + +/** + @brief QetScriptApi::routeConductorBetween + routeConductor() for the conductor joining two given terminals, which + names it even where either terminal carries other conductors too -- + the case just after addConductor() onto a terminal already wired. +*/ +QString QetScriptApi::routeConductorBetween(int folioIndex, + const QString &elementUuidA, int terminalIndexA, + const QString &elementUuidB, int terminalIndexB) +{ + if (!m_project) return QString(); + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.routeConductorBetween: project is read-only")); + return QString(); + } + const QString caller = QStringLiteral("routeConductorBetween"); + Terminal *a = findTerminal(folioIndex, elementUuidA, terminalIndexA, caller); + Terminal *b = findTerminal(folioIndex, elementUuidB, terminalIndexB, caller); + if (!a || !b) return QString(); + for (Conductor *c : a->conductors()) { + if (c->terminal1 == b || c->terminal2 == b) + return applyRoute(c, caller); + } + log(QStringLiteral("qet.%1: no conductor joins those two terminals").arg(caller)); + return QString(); +} + QString QetScriptApi::elementLinkType(int folioIndex, const QString &elementUuid) const { Element *element = findElement(folioIndex, elementUuid); diff --git a/sources/scripting/qetscriptapi.h b/sources/scripting/qetscriptapi.h index a458149da..a4da697de 100644 --- a/sources/scripting/qetscriptapi.h +++ b/sources/scripting/qetscriptapi.h @@ -418,6 +418,11 @@ class QetScriptApi : public QObject Q_INVOKABLE bool moveConductorSegment(int folioIndex, const QString &elementUuid, int terminalIndex, int segmentIndex, double dx, double dy); + Q_INVOKABLE QString routeConductor(int folioIndex, const QString &elementUuid, + int terminalIndex); + Q_INVOKABLE QString routeConductorBetween(int folioIndex, + const QString &elementUuidA, int terminalIndexA, + const QString &elementUuidB, int terminalIndexB); // -- cross-references: master/slave and report links -- Q_INVOKABLE QString elementLinkType(int folioIndex, const QString &elementUuid) const; @@ -596,6 +601,7 @@ class QetScriptApi : public QObject Element *findElement(int folioIndex, const QString &elementUuid) const; Terminal *findTerminal(int folioIndex, const QString &elementUuid, int terminalIndex, const QString &caller); + QString applyRoute(Conductor *conductor, const QString &caller); Conductor *findConductor(int folioIndex, const QString &elementUuid, int terminalIndex, const QString &caller); QList sortedTexts(int folioIndex) const; diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 3b1085990..e92e86f53 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -402,6 +402,16 @@ target_link_libraries(tst_wirehops PRIVATE Qt::Test Qt::Gui) target_compile_definitions(tst_wirehops PRIVATE "QET_TEST_BINARY_PATH=\"$\"") +# The routing behind qet.routeConductor(): conductorrouter.cpp needs only +# Qt Core, so the geometry is tested without a scene. +add_executable( + tst_conductorrouter + tst_conductorrouter.cpp + ${QET_DIR}/sources/conductorrouter.cpp) +add_test(NAME tst_conductorrouter COMMAND tst_conductorrouter) +target_include_directories(tst_conductorrouter PRIVATE ${QET_DIR}/sources) +target_link_libraries(tst_conductorrouter PRIVATE Qt::Test) + # qet.conductorUuids() / qet.conductorEnds(): a script lists a folio's # conductors by uuid and finds each one's two ends. Runs a script through # the real binary's --run on fixtures/qet_bug_repro_resaved.qet, so only diff --git a/tests/qttest/tst_conductorrouter.cpp b/tests/qttest/tst_conductorrouter.cpp new file mode 100644 index 000000000..6807d4149 --- /dev/null +++ b/tests/qttest/tst_conductorrouter.cpp @@ -0,0 +1,212 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "conductorrouter.h" + +#include + +/** + The geometry of ConductorRouter on its own: that a route is made of + horizontal and vertical segments, leaves and enters each terminal in + the terminal's direction, and goes around obstacles rather than + through them. +*/ +class TestConductorRouter : public QObject +{ + Q_OBJECT + + using Direction = ConductorRouter::Direction; + + static bool crosses(const QList &points, const QRectF &r) + { + for (int i = 0; i + 1 < points.size(); ++i) { + const QPointF a = points.at(i), b = points.at(i + 1); + const QRectF seg = QRectF(a, b).normalized(); + const qreal left = qMax(seg.left(), r.left()), right = qMin(seg.right(), r.right()); + const qreal top = qMax(seg.top(), r.top()), bottom = qMin(seg.bottom(), r.bottom()); + // a segment is a degenerate rectangle: it crosses the + // interior when it overlaps it on one axis and lies + // strictly inside it on the other + if (seg.width() == 0 && seg.left() > r.left() && seg.left() < r.right() && bottom > top) + return true; + if (seg.height() == 0 && seg.top() > r.top() && seg.top() < r.bottom() && right > left) + return true; + } + return false; + } + + static void checkShape(const QList &p, const ConductorRouter::Request &r) + { + QVERIFY(p.size() >= 3); + QCOMPARE(p.first(), r.start); + QCOMPARE(p.last(), r.end); + for (int i = 0; i + 1 < p.size(); ++i) + QVERIFY2(p.at(i).x() == p.at(i + 1).x() || p.at(i).y() == p.at(i + 1).y(), + "a segment is neither horizontal nor vertical"); + } + + static QPointF unit(Direction d) + { + switch (d) { + case Direction::North: return {0, -1}; + case Direction::East: return {1, 0}; + case Direction::South: return {0, 1}; + case Direction::West: return {-1, 0}; + } + return {}; + } + + static QPointF direction(QPointF from, QPointF to) + { + const QPointF d = to - from; + return {d.x() > 0 ? 1. : d.x() < 0 ? -1. : 0., d.y() > 0 ? 1. : d.y() < 0 ? -1. : 0.}; + } + +private slots: + void straightWhenNothingIsInTheWay() + { + ConductorRouter::Request r; + r.start = {100, 100}; + r.start_direction = Direction::East; + r.end = {300, 100}; + r.end_direction = Direction::West; + const auto result = ConductorRouter::route(r); + checkShape(result.points, r); + // no bend at all: start, the two exit points, end + for (const QPointF &p : result.points) QCOMPARE(p.y(), 100.); + } + + void goesAroundASymbol() + { + ConductorRouter::Request r; + r.start = {100, 100}; + r.start_direction = Direction::East; + r.end = {300, 100}; + r.end_direction = Direction::West; + const QRectF symbol(170, 60, 60, 80); + r.obstacles << symbol; + const auto result = ConductorRouter::route(r); + QVERIFY2(result.error.isEmpty(), qPrintable(result.error)); + checkShape(result.points, r); + QVERIFY(!crosses(result.points, symbol.adjusted(-r.margin, -r.margin, r.margin, r.margin))); + } + + // The test that the check above can fail: a straight line through + // the symbol is reported as crossing it. + void crossingCheckCanFail() + { + QVERIFY(crosses({{100, 100}, {300, 100}}, QRectF(170, 60, 60, 80))); + } + + void leavesAndEntersInTheTerminalsDirections_data() + { + QTest::addColumn("from"); + QTest::addColumn("to"); + for (int a = 0; a < 4; ++a) + for (int b = 0; b < 4; ++b) + QTest::addRow("%d-%d", a, b) << a << b; + } + + void leavesAndEntersInTheTerminalsDirections() + { + QFETCH(int, from); + QFETCH(int, to); + ConductorRouter::Request r; + r.start = {100, 100}; + r.start_direction = Direction(from); + r.end = {250, 180}; + r.end_direction = Direction(to); + // each terminal's own symbol, on the side opposite the way + // the terminal points + const QPointF s = unit(r.start_direction), e = unit(r.end_direction); + const QRectF own_start(r.start - s * 40 - QPointF(20, 20), QSizeF(40, 40)); + const QRectF own_end(r.end - e * 40 - QPointF(20, 20), QSizeF(40, 40)); + r.obstacles << own_start << own_end; + const auto result = ConductorRouter::route(r); + QVERIFY2(result.error.isEmpty(), qPrintable(result.error)); + checkShape(result.points, r); + const auto &p = result.points; + QCOMPARE(direction(p.at(0), p.at(1)), s); + QCOMPARE(direction(p.at(p.size() - 1), p.at(p.size() - 2)), e); + QVERIFY(!crosses(p, own_start)); + QVERIFY(!crosses(p, own_end)); + } + + void prefersNotToRunAlongAnotherWire() + { + ConductorRouter::Request r; + r.start = {100, 100}; + r.start_direction = Direction::East; + r.end = {300, 100}; + r.end_direction = Direction::West; + // a wire lying exactly on the straight route + r.wires << QVector{{110, 100}, {290, 100}}; + const auto result = ConductorRouter::route(r); + checkShape(result.points, r); + bool on_it = false; + for (int i = 1; i + 2 < result.points.size(); ++i) + if (result.points.at(i).y() == 100 && result.points.at(i + 1).y() == 100) + on_it = true; + QVERIFY(!on_it); + } + + void staysOnTheFolio() + { + ConductorRouter::Request r; + r.start = {100, 30}; + r.start_direction = Direction::East; + r.end = {300, 30}; + r.end_direction = Direction::West; + r.obstacles << QRectF(170, 0, 60, 200); + r.bounds = QRectF(0, 0, 500, 400); + const auto result = ConductorRouter::route(r); + QVERIFY2(result.error.isEmpty(), qPrintable(result.error)); + for (const QPointF &p : result.points) QVERIFY(r.bounds.contains(p)); + } + + void reportsWhenThereIsNoWay() + { + ConductorRouter::Request r; + r.start = {100, 100}; + r.start_direction = Direction::East; + r.end = {300, 100}; + r.end_direction = Direction::West; + // a wall from the top of the folio to the bottom + r.obstacles << QRectF(170, -10, 60, 520); + r.bounds = QRectF(0, 0, 500, 500); + const auto result = ConductorRouter::route(r); + QVERIFY(result.points.isEmpty()); + QVERIFY(!result.error.isEmpty()); + } + + void terminalsOffTheGrid() + { + ConductorRouter::Request r; + r.start = {103, 97}; + r.start_direction = Direction::South; + r.end = {287, 213}; + r.end_direction = Direction::North; + r.obstacles << QRectF(120, 140, 200, 30); + const auto result = ConductorRouter::route(r); + QVERIFY2(result.error.isEmpty(), qPrintable(result.error)); + checkShape(result.points, r); + } +}; + +QTEST_MAIN(TestConductorRouter) +#include "tst_conductorrouter.moc" From ca42080ebe75867f90a7b9522294e332a1e294f4 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Fri, 2 Oct 2026 23:21:35 +1300 Subject: [PATCH 2/2] Route a conductor in milliseconds on a crowded folio The router costed every grid edge against every symbol and every wire segment on the folio. On the 191-symbol, 366-wire Polonez example that is about 145 million tests and 700 ms per route, so a qet_edit routing some 40 wires there ran into the scripting run's 30-second limit. Each symbol and wire now visits only the grid edges it can touch, found by binary search, and applies the same exact test to them as before, in the same order. Rerouting every conductor of the 23 example projects gives the same 11,018 saved segments as before, now 22 ms per route on Polonez instead of 700. Co-Authored-By: Claude Opus 5.5 (1M context) --- sources/conductorrouter.cpp | 96 ++++++++++++++++++++++++++----------- 1 file changed, 67 insertions(+), 29 deletions(-) diff --git a/sources/conductorrouter.cpp b/sources/conductorrouter.cpp index 93912bdd0..d3d800453 100644 --- a/sources/conductorrouter.cpp +++ b/sources/conductorrouter.cpp @@ -119,35 +119,77 @@ int indexOf(const QVector &v, qreal c) return (it != v.end() && std::abs(*it - c) < eps) ? int(it - v.begin()) : -1; } - ///The cost of the edge along one axis from a to b at fixed coordinate at, - ///or infinity if it crosses an obstacle. horizontal says which axis. -qreal edgeCost(bool horizontal, qreal at, qreal a, qreal b, - const QList &obstacles, - const QVector &along, const QVector &across, - qreal grid) + ///[first, last) of the sorted v: the values strictly between low and high +std::pair openRange(const QVector &v, qreal low, qreal high) { + const int first = int(std::upper_bound(v.begin(), v.end(), low) - v.begin()); + const int last = int(std::lower_bound(v.begin(), v.end(), high) - v.begin()); + return {first, std::max(first, last)}; +} + + ///The cost of every edge along one axis, or infinity where an obstacle + ///blocks it. Edge (k, i) runs from pos[i] to pos[i + 1] at the fixed + ///coordinate lines[k], and is stored at [k * pos.size() + i]; + ///horizontal says which axis pos is. Each obstacle and wire visits only + ///the edges it can touch, so the cost grows with the folio, not with + ///the folio times everything drawn on it. +std::vector edgeCosts(bool horizontal, + const QVector &pos, const QVector &lines, + const QList &obstacles, + const QVector &along, const QVector &across, + qreal grid) +{ + const int np = pos.size(), nl = lines.size(); + std::vector cost(size_t(np) * nl, std::numeric_limits::infinity()); + for (int k = 0; k < nl; ++k) + for (int i = 0; i + 1 < np; ++i) + cost[size_t(k) * np + i] = pos[i + 1] - pos[i]; + + // The edges i that can overlap (low, high): pos[i + 1] > low and + // pos[i] < high. The exact test is applied to each one found. + const auto edges = [&](qreal low, qreal high) { + const int first = int(std::upper_bound(pos.begin(), pos.end(), low) - pos.begin()) - 1; + const int last = int(std::lower_bound(pos.begin(), pos.end(), high) - pos.begin()); + return std::pair{std::max(0, first), std::min(np - 1, last)}; + }; + for (const QRectF &r : obstacles) { const qreal lo = horizontal ? r.left() : r.top(); const qreal hi = horizontal ? r.right() : r.bottom(); const qreal flo = horizontal ? r.top() : r.left(); const qreal fhi = horizontal ? r.bottom(): r.right(); - if (at > flo + eps && at < fhi - eps - && std::min(b, hi) - std::max(a, lo) > eps) - return std::numeric_limits::infinity(); + const auto [k0, k1] = openRange(lines, flo, fhi); + const auto [i0, i1] = edges(lo, hi); + for (int k = k0; k < k1; ++k) { + if (!(lines[k] > flo + eps && lines[k] < fhi - eps)) continue; + for (int i = i0; i < i1; ++i) + if (std::min(pos[i + 1], hi) - std::max(pos[i], lo) > eps) + cost[size_t(k) * np + i] = std::numeric_limits::infinity(); + } } - qreal cost = b - a; for (const Span &s : along) { - if (std::abs(s.at - at) > 0.5) continue; - const qreal overlap = std::min(b, s.to) - std::max(a, s.from); - if (overlap > eps) cost += overlap * along_factor; + const int k0 = int(std::lower_bound(lines.begin(), lines.end(), s.at - 0.5) - lines.begin()); + const auto [i0, i1] = edges(s.from, s.to); + for (int k = k0; k < nl && lines[k] <= s.at + 0.5; ++k) { + if (std::abs(s.at - lines[k]) > 0.5) continue; + for (int i = i0; i < i1; ++i) { + const qreal overlap = std::min(pos[i + 1], s.to) - std::max(pos[i], s.from); + if (overlap > eps) cost[size_t(k) * np + i] += overlap * along_factor; + } + } } for (const Span &s : across) { // Half-open (a, b], so a crossing on a grid node is counted // once, by the edge that ends on it; and only through the // other wire's interior, not at its end, which is a junction. - if (s.at > a + eps && s.at <= b + eps - && at > s.from + 0.5 && at < s.to - 0.5) - cost += cross_steps * grid; + const auto [k0, k1] = openRange(lines, s.from, s.to); + const auto [i0, i1] = edges(s.at - 1.0, s.at + 1.0); + for (int k = k0; k < k1; ++k) { + if (!(lines[k] > s.from + 0.5 && lines[k] < s.to - 0.5)) continue; + for (int i = i0; i < i1; ++i) + if (s.at > pos[i] + eps && s.at <= pos[i + 1] + eps) + cost[size_t(k) * np + i] += cross_steps * grid; + } } return cost; } @@ -200,18 +242,14 @@ ConductorRouter::Result ConductorRouter::route(const Request &r) } } - // The cost of the edge from each node to the next one east - // (h_cost) and south (v_cost); infinity where blocked or none. + // The cost of the edge from each node to the next one east, + // h_cost[j * nx + i], and south, v_cost[i * ny + j]; infinity + // where blocked or none. const qreal inf = std::numeric_limits::infinity(); - std::vector h_cost(size_t(nx) * ny, inf), v_cost(size_t(nx) * ny, inf); - for (int j = 0; j < ny; ++j) - for (int i = 0; i + 1 < nx; ++i) - h_cost[size_t(j) * nx + i] = edgeCost(true, ys[j], xs[i], xs[i + 1], obstacles, - horizontal_wires, vertical_wires, r.grid); - for (int i = 0; i < nx; ++i) - for (int j = 0; j + 1 < ny; ++j) - v_cost[size_t(j) * nx + i] = edgeCost(false, xs[i], ys[j], ys[j + 1], obstacles, - vertical_wires, horizontal_wires, r.grid); + const std::vector h_cost = edgeCosts(true, xs, ys, obstacles, + horizontal_wires, vertical_wires, r.grid); + const std::vector v_cost = edgeCosts(false, ys, xs, obstacles, + vertical_wires, horizontal_wires, r.grid); const int start_node = indexOf(ys, s1.y()) * nx + indexOf(xs, s1.x()); const int goal_node = indexOf(ys, s2.y()) * nx + indexOf(xs, s2.x()); @@ -255,8 +293,8 @@ ConductorRouter::Result ConductorRouter::route(const Request &r) switch (nd) { case Direction::East: if (i + 1 < nx) { ni = i + 1; edge = h_cost[size_t(j) * nx + i]; } break; case Direction::West: if (i > 0) { ni = i - 1; edge = h_cost[size_t(j) * nx + ni]; } break; - case Direction::South: if (j + 1 < ny) { nj = j + 1; edge = v_cost[size_t(j) * nx + i]; } break; - case Direction::North: if (j > 0) { nj = j - 1; edge = v_cost[size_t(nj) * nx + i]; } break; + case Direction::South: if (j + 1 < ny) { nj = j + 1; edge = v_cost[size_t(i) * ny + j]; } break; + case Direction::North: if (j > 0) { nj = j - 1; edge = v_cost[size_t(i) * ny + nj]; } break; } if (edge == inf) continue; const qreal next_cost = cost + edge + (nd == d ? 0 : bend);