Return a standalone SVG holding a default block symbol for ``defn``. ``placement`` is the **visual** clockwise-from-top-left pin order (default: the ``.subckt`` node order). It only moves where each named pin is drawn — ``data-nodes`` stays in the fixed ``.subckt`` order, since a SLiCA
(defn: SubcktDef, placement: list | None = None,
extra_w: float = 0.0, extra_h: float = 0.0)
| 143 | |
| 144 | |
| 145 | def box_symbol_svg(defn: SubcktDef, placement: list | None = None, |
| 146 | extra_w: float = 0.0, extra_h: float = 0.0) -> str: |
| 147 | """Return a standalone SVG holding a default block symbol for ``defn``. |
| 148 | |
| 149 | ``placement`` is the **visual** clockwise-from-top-left pin order (default: |
| 150 | the ``.subckt`` node order). It only moves where each named pin is drawn — |
| 151 | ``data-nodes`` stays in the fixed ``.subckt`` order, since a SLiCAP ``X`` |
| 152 | instance lists its nodes positionally against the ``.subckt`` header. So |
| 153 | reordering pins never changes the netlist, only the look. |
| 154 | |
| 155 | ``extra_w`` / ``extra_h`` grow (or, when negative, shrink) each half-dimension |
| 156 | relative to the auto-fit minimum — the Place dialog's width/height +/- buttons. |
| 157 | Shrinking is allowed down to an absolute floor; below the auto-fit size names |
| 158 | may overlap, which is then the user's call. The box stays on the grid. |
| 159 | """ |
| 160 | ports = list(defn.ports) # data-nodes / netlist order |
| 161 | placement = list(placement) if placement else list(ports) |
| 162 | n_top, n_right, n_bottom, n_left = _side_counts(len(placement)) |
| 163 | |
| 164 | hw_min, hh_min = _min_half(placement) |
| 165 | half_w = max(_FLOOR, hw_min + extra_w) |
| 166 | half_h = max(_FLOOR, hh_min + extra_h) |
| 167 | |
| 168 | # Clockwise slots (top L→R, right T→B, bottom R→L, left B→T): each is |
| 169 | # (edge_x, edge_y, node_x, node_y). placement[i] occupies slot i. |
| 170 | slots: list[tuple[float, float, float, float]] = [] |
| 171 | for x in _spread(n_top, half_w): |
| 172 | slots.append((x, -half_h, x, -half_h - _STUB)) |
| 173 | for y in _spread(n_right, half_h): |
| 174 | slots.append((half_w, y, half_w + _STUB, y)) |
| 175 | for x in reversed(_spread(n_bottom, half_w)): |
| 176 | slots.append((x, half_h, x, half_h + _STUB)) |
| 177 | for y in reversed(_spread(n_left, half_h)): |
| 178 | slots.append((-half_w, y, -half_w - _STUB, y)) |
| 179 | pos = {placement[i]: slots[i] for i in range(len(placement))} |
| 180 | |
| 181 | def _f(v: float) -> str: # tidy number formatting |
| 182 | return f"{v:g}" |
| 183 | |
| 184 | # The box + pin stubs + node markers only. The block name (model label) and |
| 185 | # the pin names are drawn by ComponentItem so they stay upright under any |
| 186 | # rotation/flip — never baked into the (rotating) symbol art. |
| 187 | lines = [ |
| 188 | f'<rect x="{_f(-half_w)}" y="{_f(-half_h)}" ' |
| 189 | f'width="{_f(2 * half_w)}" height="{_f(2 * half_h)}" ' |
| 190 | f'fill="none" stroke="black" stroke-width="1"/>', |
| 191 | "", |
| 192 | "<!-- Pins -->", |
| 193 | ] |
| 194 | for port in ports: |
| 195 | ex, ey, nx, ny = pos[port] |
| 196 | lines.append( |
| 197 | f'<line x1="{_f(ex)}" y1="{_f(ey)}" x2="{_f(nx)}" y2="{_f(ny)}" ' |
| 198 | f'stroke="black" stroke-width="1"/>' |
| 199 | ) |
| 200 | lines.append("") |
| 201 | for port in ports: |
| 202 | _ex, _ey, nx, ny = pos[port] |
no test coverage detected