Port names contributed by ``port`` symbols, in clockwise order starting from the top-left. This is only the *default* order shown to the user, who may reorder it; the chosen order is what defines the subcircuit node list. A ``ground`` (node 0) is global, never a port, so it is excluded
(components: list, wires: list)
| 203 | |
| 204 | |
| 205 | def schematic_ports(components: list, wires: list) -> list[str]: |
| 206 | """Port names contributed by ``port`` symbols, in clockwise order starting |
| 207 | from the top-left. This is only the *default* order shown to the user, who |
| 208 | may reorder it; the chosen order is what defines the subcircuit node list. |
| 209 | |
| 210 | A ``ground`` (node 0) is global, never a port, so it is excluded even if a |
| 211 | port symbol happens to sit on it. |
| 212 | """ |
| 213 | import math |
| 214 | |
| 215 | ports: list[tuple[float, float, str]] = [] |
| 216 | for comp in components: |
| 217 | if comp.symbol_name != "port": |
| 218 | continue |
| 219 | name = comp.params.get("name", "").strip() |
| 220 | if not name or name == "0": |
| 221 | continue |
| 222 | p = comp.mapToScene(QPointF(0.0, 0.0)) |
| 223 | ports.append((p.x(), p.y(), name)) |
| 224 | |
| 225 | if not ports: |
| 226 | return [] |
| 227 | |
| 228 | cx = sum(p[0] for p in ports) / len(ports) |
| 229 | cy = sum(p[1] for p in ports) / len(ports) |
| 230 | |
| 231 | def _clock(p) -> float: |
| 232 | # Angle measured clockwise from straight up (12 o'clock), y grows down. |
| 233 | ang = math.atan2(p[0] - cx, -(p[1] - cy)) |
| 234 | return ang if ang >= 0 else ang + 2 * math.pi |
| 235 | |
| 236 | seen: set[str] = set() |
| 237 | ordered: list[str] = [] |
| 238 | for _x, _y, n in sorted(ports, key=_clock): |
| 239 | if n not in seen: # same-name ports are one net → one port node |
| 240 | seen.add(n) |
| 241 | ordered.append(n) |
| 242 | return ordered |