1 Commits
Author SHA1 Message Date
janikandClaude Fable 5 19327c23b1 Release 1.4.1: a Bonded checkbox in the terminal tables
tests / ubuntu-latest · py3.11 (push) Successful in 54s
tests / ubuntu-latest · py3.13 (push) Successful in 58s
tests / archlinux:latest (push) Successful in 41s
tests / debian:12 (push) Successful in 1m10s
tests / fedora:latest (push) Successful in 42m6s
tests / ubuntu:24.04 (push) Successful in 1m15s
tests / NixOS (FHS wrapper from docs/NIXOS.md) (push) Skipped
Build PCM package / build (push) Successful in 11s
The config's per-terminal bonded key becomes editable in the dialog
(previously file-only, shown as a text suffix): checked, the
terminal's contacts short into one internally joined lug - the total
value stays prescribed, the per-contact split is a solve outcome.
Same-name rectangle groups seed it checked as before; unchecking one
falls back to the per-cell area share, and checking a single-contact
terminal gives it an equipotential-lug contact instead of uniform
injection. Save config... writes the flag back (removed when
unchecked - false is the schema default). With this, every
non-structural terminal option (active, values, v_oc, contact layer,
bonded, comment) is table-editable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 17:20:35 +07:00
11 changed files with 100 additions and 34 deletions
+12 -6
View File
@@ -368,12 +368,18 @@ terminal for what-if runs without deleting anything: an unchecked row
takes no part in the solve, may leave its value cells blank, and is takes no part in the solve, may leave its value cells blank, and is
still saved (as `"active": false`) so it can be re-enabled later — the still saved (as `"active": false`) so it can be re-enabled later — the
totals line counts disabled rows. A free-text **Comment** column totals line counts disabled rows. A free-text **Comment** column
annotates each terminal and is saved along with it. Rectangles that **share one name annotates each terminal and is saved along with it. A **Bonded** checkbox per row
become a single *bonded* terminal** — one table row, one total toggles the lug model (the config's `bonded` key): checked, the
current, and the per-rectangle split is a solve outcome (a multi-pin terminal's contacts are shorted into one internally joined lug — the
package whose pins are joined by internal metal: the total draw is total value stays prescribed, the per-contact split is a solve
known, which pin carries how much is exactly what the solve outcome — and a single-contact terminal checked gets an
determines). OK solves; equipotential-lug contact instead of uniform injection. Rectangles
that **share one name become a single terminal with Bonded seeded
checked** — one table row, one total current, per-rectangle split
solved (a multi-pin package whose pins are joined by internal metal:
the total draw is known, which pin carries how much is exactly what
the solve determines); uncheck it to fall back to the area-share
split. OK solves;
**Save config…** writes the whole setup to a config file whose name **Save config…** writes the whole setup to a config file whose name
you pick per save (pre-filled with the loaded config, else you pick per save (pre-filled with the loaded config, else
`fill_res_config.json`) so the values survive between runs — named rectangles are saved as live `rect:NAME` `fill_res_config.json`) so the values survive between runs — named rectangles are saved as live `rect:NAME`
+12
View File
@@ -0,0 +1,12 @@
Dialog completeness: every per-terminal config option is now editable
in the terminal tables.
- A Bonded checkbox per row (the config's "bonded" key, previously
file-only and shown as a text suffix): checked, the terminal's
contacts short into one internally joined lug - the total value
stays prescribed, the per-contact split becomes a solve outcome.
Same-name rectangle groups seed it checked (unchanged default);
unchecking one falls back to the per-cell area share, and checking
a single-contact terminal gives it an equipotential-lug contact
instead of uniform injection. Save config... writes the flag back
(removed when unchecked - false is the schema default).
+1 -1
View File
@@ -4,4 +4,4 @@ __version__ is the runtime source of truth (metadata.json and
pyproject.toml are not deployed with the plugin); a test keeps the pyproject.toml are not deployed with the plugin); a test keeps the
three in sync. three in sync.
""" """
__version__ = "1.4.0" __version__ = "1.4.1"
+7 -2
View File
@@ -769,8 +769,9 @@ def save_classic_config(path: Path, selection) -> None:
def updated_terminals_json(raw_terminals: list, rows: list) -> list: def updated_terminals_json(raw_terminals: list, rows: list) -> list:
"""Config-backed PDN save: each raw terminal object is deep-copied """Config-backed PDN save: each raw terminal object is deep-copied
verbatim (parts, "_"-prefixed keys preserved) and only the verbatim (parts, "_"-prefixed keys preserved) and only the
dialog-editable values - I / R_out / V_oc and the terminal-level dialog-editable values - I / R_out / V_oc, the bonded flag and the
contact layer - are written back POSITIONALLY: the dialog never terminal-level contact layer - are written back POSITIONALLY: the
dialog never
reorders its tables, so index i is the same terminal in both lists. reorders its tables, so index i is the same terminal in both lists.
A supply row's v_oc of None REMOVES the key (restoring the A supply row's v_oc of None REMOVES the key (restoring the
defaults-to-v_nominal semantics); a contact of "auto" removes the defaults-to-v_nominal semantics); a contact of "auto" removes the
@@ -800,6 +801,10 @@ def updated_terminals_json(raw_terminals: list, rows: list) -> list:
t["contact"] = contact t["contact"] = contact
else: else:
t.pop("contact", None) t.pop("contact", None)
if getattr(row, "bonded", False):
t["bonded"] = True
else:
t.pop("bonded", None) # false is the schema default
if getattr(row, "active", True): if getattr(row, "active", True):
t.pop("active", None) # true is the schema default t.pop("active", None) # true is the schema default
else: else:
+25 -11
View File
@@ -122,10 +122,13 @@ class PdnTerminalRow:
i_draw_a: float | None = None # loads; None = not entered yet i_draw_a: float | None = None # loads; None = not entered yet
r_out_ohm: float | None = None # supplies; None = not entered yet r_out_ohm: float | None = None # supplies; None = not entered yet
v_oc: float | None = None # supplies; None = v_nominal v_oc: float | None = None # supplies; None = v_nominal
bonded: bool = False # multi-contact lug: the TOTAL value bonded: bool = False # checkbox: short the contacts into
# applies, the per-contact split is a # one lug - the TOTAL value applies,
# solve outcome (display/data only - # the per-contact split is a solve
# not editable in the table) # outcome. Seeded True for same-name
# rectangle groups and from the
# config's "bonded" key; a single
# contact checked = equipotential lug
contact: str = "all" # terminal-level layer scope: "auto" contact: str = "all" # terminal-level layer scope: "auto"
# (per contact part - config-backed # (per contact part - config-backed
# rows only), "all", or a layer name # rows only), "all", or a layer name
@@ -304,8 +307,10 @@ class _Dialog(QDialog):
# the role/layer mapping lives in the table titles now # the role/layer mapping lives in the table titles now
hints.append("name from a text item inside the " hints.append("name from a text item inside the "
"rectangle; empty V_oc = V nominal") "rectangle; empty V_oc = V nominal")
hints.append("Layer = the copper the terminal contacts; " hints.append("Bonded = contacts joined into one lug (the "
"values take SI suffixes (50m = 0.05)") "per-contact split is solved); Layer = the "
"copper the terminal contacts; values take SI "
"suffixes (50m = 0.05)")
hint = QLabel("".join(hints)) hint = QLabel("".join(hints))
hint.setWordWrap(True) hint.setWordWrap(True)
hint.setStyleSheet("color: gray; font-size: 10px;") hint.setStyleSheet("color: gray; font-size: 10px;")
@@ -532,10 +537,11 @@ class _Dialog(QDialog):
for role, cols in ( for role, cols in (
("supply", ("supply",
["Active", "Name", "Component", "R_out [Ω]", ["Active", "Name", "Component", "R_out [Ω]",
"V_oc [V]", "Layer", "Contact parts", "Comment"]), "V_oc [V]", "Bonded", "Layer", "Contact parts",
"Comment"]),
("load", ("load",
["Active", "Name", "Component", "I draw [A]", "Layer", ["Active", "Name", "Component", "I draw [A]",
"Contact parts", "Comment"])): "Bonded", "Layer", "Contact parts", "Comment"])):
n = sum(1 for r in pdn.rows if r.role == role) n = sum(1 for r in pdn.rows if r.role == role)
panel = QWidget() panel = QWidget()
pv = QVBoxLayout(panel) pv = QVBoxLayout(panel)
@@ -562,7 +568,8 @@ class _Dialog(QDialog):
self._pdn_map.append((t, i)) self._pdn_map.append((t, i))
values = ([row.r_out_ohm, row.v_oc] if row.role == "supply" values = ([row.r_out_ohm, row.v_oc] if row.role == "supply"
else [row.i_draw_a]) else [row.i_draw_a])
last = t.columnCount() - 1 # ... | Layer | parts | Comment # tail columns: ... | Bonded | Layer | parts | Comment
last = t.columnCount() - 1
cells = [(1, row.name, False), (2, row.component, False)] cells = [(1, row.name, False), (2, row.component, False)]
for col, value in enumerate(values, start=3): for col, value in enumerate(values, start=3):
cells.append((col, "" if value is None else f"{value:g}", cells.append((col, "" if value is None else f"{value:g}",
@@ -579,6 +586,11 @@ class _Dialog(QDialog):
box.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable) box.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
box.setCheckState(Qt.Checked if row.active else Qt.Unchecked) box.setCheckState(Qt.Checked if row.active else Qt.Unchecked)
t.setItem(i, 0, box) t.setItem(i, 0, box)
bond = QTableWidgetItem("")
bond.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
bond.setCheckState(Qt.Checked if row.bonded
else Qt.Unchecked)
t.setItem(i, last - 3, bond)
combo = QComboBox() combo = QComboBox()
idx = len(self._pdn_layer_combos) idx = len(self._pdn_layer_combos)
self._pdn_layer_combos.append(combo) self._pdn_layer_combos.append(combo)
@@ -695,12 +707,14 @@ class _Dialog(QDialog):
new = PdnTerminalRow(name=row.name, role=row.role, new = PdnTerminalRow(name=row.name, role=row.role,
resolved=row.resolved, resolved=row.resolved,
component=row.component, component=row.component,
bonded=row.bonded,
from_config=row.from_config) from_config=row.from_config)
box = t.item(r, 0) box = t.item(r, 0)
checked = (box is None checked = (box is None
or box.checkState() == Qt.Checked) or box.checkState() == Qt.Checked)
new.active = checked and not self._row_hidden(i) new.active = checked and not self._row_hidden(i)
bond = t.item(r, t.columnCount() - 4)
new.bonded = (bond is not None
and bond.checkState() == Qt.Checked)
new.comment = cell(t.columnCount() - 1) new.comment = cell(t.columnCount() - 1)
combo = self._pdn_layer_combos[i] combo = self._pdn_layer_combos[i]
new.contact = (combo.currentData() if combo.count() new.contact = (combo.currentData() if combo.count()
+6 -5
View File
@@ -237,9 +237,10 @@ def main() -> None:
def marker_desc(mt): def marker_desc(mt):
if len(mt.electrodes) == 1: if len(mt.electrodes) == 1:
return rect_desc(mt.electrodes[0]) return rect_desc(mt.electrodes[0])
# same-named rectangles grouped into one bonded lug # same-named rectangles grouped into one terminal (the
# Bonded checkbox shows/controls the lug behavior)
return (f"{len(mt.electrodes)}× " return (f"{len(mt.electrodes)}× "
f"{rect_desc(mt.electrodes[0])} — bonded") f"{rect_desc(mt.electrodes[0])}")
def live_row(mt, hint, tn): def live_row(mt, hint, tn):
return dialog.PdnTerminalRow( return dialog.PdnTerminalRow(
@@ -253,8 +254,7 @@ def main() -> None:
n_cfg = len(terminals) n_cfg = len(terminals)
rows = [dialog.PdnTerminalRow( rows = [dialog.PdnTerminalRow(
name=t.label, role=t.role, name=t.label, role=t.role,
resolved=(group_label(t.electrodes) resolved=group_label(t.electrodes),
+ (" — bonded" if t.bonded else "")),
component=hint, component=hint,
i_draw_a=(t.i_draw_a if t.role == "load" i_draw_a=(t.i_draw_a if t.role == "load"
else None), else None),
@@ -418,7 +418,7 @@ def main() -> None:
if row.i_draw_a is not None else 0.0), if row.i_draw_a is not None else 0.0),
r_out_ohm=(row.r_out_ohm r_out_ohm=(row.r_out_ohm
if row.r_out_ohm is not None else 0.0), if row.r_out_ohm is not None else 0.0),
v_oc=row.v_oc, bonded=mt.bonded, v_oc=row.v_oc, bonded=row.bonded,
component=row.component, comment=row.comment) component=row.component, comment=row.comment)
if pdn_cfg: if pdn_cfg:
cfg_rows = selection.pdn_rows[:len(terminals)] cfg_rows = selection.pdn_rows[:len(terminals)]
@@ -448,6 +448,7 @@ def main() -> None:
else: else:
t.r_out_ohm = row.r_out_ohm t.r_out_ohm = row.r_out_ohm
t.v_oc = row.v_oc t.v_oc = row.v_oc
t.bonded = row.bonded
t.component = row.component t.component = row.component
t.comment = row.comment t.comment = row.comment
terminals = ( terminals = (
+1 -1
View File
@@ -17,7 +17,7 @@
}, },
"versions": [ "versions": [
{ {
"version": "1.4.0", "version": "1.4.1",
"status": "stable", "status": "stable",
"kicad_version": "10.0", "kicad_version": "10.0",
"runtime": "ipc" "runtime": "ipc"
+1 -1
View File
@@ -3,7 +3,7 @@
# the dependency list there in sync with [project.dependencies]. # the dependency list there in sync with [project.dependencies].
[project] [project]
name = "fill-resistance" name = "fill-resistance"
version = "1.4.0" version = "1.4.1"
description = "DC resistance of copper zone fills and traces between two contacts (KiCad 10 plugin)" description = "DC resistance of copper zone fills and traces between two contacts (KiCad 10 plugin)"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
requires-python = ">=3.11" requires-python = ">=3.11"
+14
View File
@@ -468,6 +468,20 @@ def test_active_and_comment_in_the_save_builders():
assert "active" not in uj[1] and uj[1]["i_draw_a"] == 2.0 assert "active" not in uj[1] and uj[1]["i_draw_a"] == 2.0
def test_updated_terminals_json_moves_the_bonded_flag():
raw = [{"name": "pkg", "role": "load", "parts": ["U7"],
"i_draw_a": 1.8, "bonded": True},
{"name": "heat", "role": "load", "parts": ["U9"],
"i_draw_a": 2.0}]
rows = [PdnTerminalRow(name="pkg", role="load", resolved="",
i_draw_a=1.8, bonded=False),
PdnTerminalRow(name="heat", role="load", resolved="",
i_draw_a=2.0, bonded=True)]
uj = updated_terminals_json(raw, rows)
assert "bonded" not in uj[0] # unchecked removes the key
assert uj[1]["bonded"] is True
def test_inactive_row_saves_and_reloads(tmp_path): def test_inactive_row_saves_and_reloads(tmp_path):
path = tmp_path / "fill_res_config.json" path = tmp_path / "fill_res_config.json"
rows = [PdnTerminalRow(name="VIN", role="supply", resolved="", rows = [PdnTerminalRow(name="VIN", role="supply", resolved="",
+20 -6
View File
@@ -212,8 +212,8 @@ def test_tables_split_by_role(app):
assert dlg.pdn_sup_table.item(0, 1).text() == "src" assert dlg.pdn_sup_table.item(0, 1).text() == "src"
assert dlg.pdn_load_table.item(0, 1).text() == "snk" assert dlg.pdn_load_table.item(0, 1).text() == "snk"
# supplies carry R_out + V_oc columns, loads only I draw # supplies carry R_out + V_oc columns, loads only I draw
assert dlg.pdn_sup_table.columnCount() == 8 assert dlg.pdn_sup_table.columnCount() == 9
assert dlg.pdn_load_table.columnCount() == 7 assert dlg.pdn_load_table.columnCount() == 8
def test_table_titles_name_the_marker_layers(app): def test_table_titles_name_the_marker_layers(app):
@@ -292,10 +292,12 @@ def test_cell_flags(app):
for col in (1, 2, last - 1): for col in (1, 2, last - 1):
assert t.item(0, col).flags() & Qt.ItemIsEnabled assert t.item(0, col).flags() & Qt.ItemIsEnabled
assert not (t.item(0, col).flags() & Qt.ItemIsEditable) assert not (t.item(0, col).flags() & Qt.ItemIsEditable)
# the Active column is a checkbox, not an editable cell # Active and Bonded are checkboxes, not editable cells
assert t.item(0, 0).flags() & Qt.ItemIsUserCheckable for col in (0, last - 3):
assert not (t.item(0, 0).flags() & Qt.ItemIsEditable) assert t.item(0, col).flags() & Qt.ItemIsUserCheckable
assert not (t.item(0, col).flags() & Qt.ItemIsEditable)
assert t.item(0, 0).checkState() == Qt.Checked assert t.item(0, 0).checkState() == Qt.Checked
assert t.item(0, last - 3).checkState() == Qt.Unchecked
# the Layer column holds a combo, not a text item # the Layer column holds a combo, not a text item
assert t.cellWidget(0, last - 2) is not None assert t.cellWidget(0, last - 2) is not None
@@ -449,16 +451,28 @@ def test_comment_column_roundtrip(app):
assert sel.pdn_rows[1].comment == "worst case" assert sel.pdn_rows[1].comment == "worst case"
def test_bonded_flag_survives_the_table_roundtrip(app): def test_bonded_checkbox_seeds_and_toggles(app):
setup = PdnSetup(rows=[ setup = PdnSetup(rows=[
PdnTerminalRow(name="src", role="supply", resolved="r"), PdnTerminalRow(name="src", role="supply", resolved="r"),
PdnTerminalRow(name="pkg", role="load", resolved="2× rects", PdnTerminalRow(name="pkg", role="load", resolved="2× rects",
bonded=True)], source="markers") bonded=True)], source="markers")
dlg = _dlg(app, pdn=setup, classic_reason="x") dlg = _dlg(app, pdn=setup, classic_reason="x")
_fill_pdn(dlg) _fill_pdn(dlg)
# seeds: the grouped load checked, the single supply not
bcol_s = dlg.pdn_sup_table.columnCount() - 4
bcol_l = dlg.pdn_load_table.columnCount() - 4
assert dlg.pdn_sup_table.item(0, bcol_s).checkState() == Qt.Unchecked
assert dlg.pdn_load_table.item(0, bcol_l).checkState() == Qt.Checked
sel = dlg._build_selection() sel = dlg._build_selection()
assert sel.pdn_rows[0].bonded is False assert sel.pdn_rows[0].bonded is False
assert sel.pdn_rows[1].bonded is True assert sel.pdn_rows[1].bonded is True
# editable both ways: unbond the group (area share), bond the
# single supply (equipotential lug contact)
dlg.pdn_load_table.item(0, bcol_l).setCheckState(Qt.Unchecked)
dlg.pdn_sup_table.item(0, bcol_s).setCheckState(Qt.Checked)
sel = dlg._build_selection()
assert sel.pdn_rows[0].bonded is True
assert sel.pdn_rows[1].bonded is False
def test_pdn_values_reach_the_selection(app): def test_pdn_values_reach_the_selection(app):
Generated
+1 -1
View File
@@ -216,7 +216,7 @@ wheels = [
[[package]] [[package]]
name = "fill-resistance" name = "fill-resistance"
version = "1.4.0" version = "1.4.1"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "kicad-python" }, { name = "kicad-python" },