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
still saved (as `"active": false`) so it can be re-enabled later — the
totals line counts disabled rows. A free-text **Comment** column
annotates each terminal and is saved along with it. Rectangles that **share one name
become a single *bonded* terminal** — one table row, one total
current, and the per-rectangle split is a solve outcome (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). OK solves;
annotates each terminal and is saved along with it. A **Bonded** checkbox per row
toggles the lug model (the config's `bonded` key): checked, the
terminal's contacts are shorted into one internally joined lug — the
total value stays prescribed, the per-contact split is a solve
outcome — and a single-contact terminal checked gets an
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
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`
+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
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:
"""Config-backed PDN save: each raw terminal object is deep-copied
verbatim (parts, "_"-prefixed keys preserved) and only the
dialog-editable values - I / R_out / V_oc and the terminal-level
contact layer - are written back POSITIONALLY: the dialog never
dialog-editable values - I / R_out / V_oc, the bonded flag and the
terminal-level contact layer - are written back POSITIONALLY: the
dialog never
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
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
else:
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):
t.pop("active", None) # true is the schema default
else:
+25 -11
View File
@@ -122,10 +122,13 @@ class PdnTerminalRow:
i_draw_a: float | None = None # loads; None = not entered yet
r_out_ohm: float | None = None # supplies; None = not entered yet
v_oc: float | None = None # supplies; None = v_nominal
bonded: bool = False # multi-contact lug: the TOTAL value
# applies, the per-contact split is a
# solve outcome (display/data only -
# not editable in the table)
bonded: bool = False # checkbox: short the contacts into
# one lug - the TOTAL value applies,
# the per-contact split is a solve
# 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"
# (per contact part - config-backed
# rows only), "all", or a layer name
@@ -304,8 +307,10 @@ class _Dialog(QDialog):
# the role/layer mapping lives in the table titles now
hints.append("name from a text item inside the "
"rectangle; empty V_oc = V nominal")
hints.append("Layer = the copper the terminal contacts; "
"values take SI suffixes (50m = 0.05)")
hints.append("Bonded = contacts joined into one lug (the "
"per-contact split is solved); Layer = the "
"copper the terminal contacts; values take SI "
"suffixes (50m = 0.05)")
hint = QLabel("".join(hints))
hint.setWordWrap(True)
hint.setStyleSheet("color: gray; font-size: 10px;")
@@ -532,10 +537,11 @@ class _Dialog(QDialog):
for role, cols in (
("supply",
["Active", "Name", "Component", "R_out [Ω]",
"V_oc [V]", "Layer", "Contact parts", "Comment"]),
"V_oc [V]", "Bonded", "Layer", "Contact parts",
"Comment"]),
("load",
["Active", "Name", "Component", "I draw [A]", "Layer",
"Contact parts", "Comment"])):
["Active", "Name", "Component", "I draw [A]",
"Bonded", "Layer", "Contact parts", "Comment"])):
n = sum(1 for r in pdn.rows if r.role == role)
panel = QWidget()
pv = QVBoxLayout(panel)
@@ -562,7 +568,8 @@ class _Dialog(QDialog):
self._pdn_map.append((t, i))
values = ([row.r_out_ohm, row.v_oc] if row.role == "supply"
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)]
for col, value in enumerate(values, start=3):
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.setCheckState(Qt.Checked if row.active else Qt.Unchecked)
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()
idx = len(self._pdn_layer_combos)
self._pdn_layer_combos.append(combo)
@@ -695,12 +707,14 @@ class _Dialog(QDialog):
new = PdnTerminalRow(name=row.name, role=row.role,
resolved=row.resolved,
component=row.component,
bonded=row.bonded,
from_config=row.from_config)
box = t.item(r, 0)
checked = (box is None
or box.checkState() == Qt.Checked)
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)
combo = self._pdn_layer_combos[i]
new.contact = (combo.currentData() if combo.count()
+6 -5
View File
@@ -237,9 +237,10 @@ def main() -> None:
def marker_desc(mt):
if len(mt.electrodes) == 1:
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)}× "
f"{rect_desc(mt.electrodes[0])} — bonded")
f"{rect_desc(mt.electrodes[0])}")
def live_row(mt, hint, tn):
return dialog.PdnTerminalRow(
@@ -253,8 +254,7 @@ def main() -> None:
n_cfg = len(terminals)
rows = [dialog.PdnTerminalRow(
name=t.label, role=t.role,
resolved=(group_label(t.electrodes)
+ (" — bonded" if t.bonded else "")),
resolved=group_label(t.electrodes),
component=hint,
i_draw_a=(t.i_draw_a if t.role == "load"
else None),
@@ -418,7 +418,7 @@ def main() -> None:
if row.i_draw_a is not None else 0.0),
r_out_ohm=(row.r_out_ohm
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)
if pdn_cfg:
cfg_rows = selection.pdn_rows[:len(terminals)]
@@ -448,6 +448,7 @@ def main() -> None:
else:
t.r_out_ohm = row.r_out_ohm
t.v_oc = row.v_oc
t.bonded = row.bonded
t.component = row.component
t.comment = row.comment
terminals = (
+1 -1
View File
@@ -17,7 +17,7 @@
},
"versions": [
{
"version": "1.4.0",
"version": "1.4.1",
"status": "stable",
"kicad_version": "10.0",
"runtime": "ipc"
+1 -1
View File
@@ -3,7 +3,7 @@
# the dependency list there in sync with [project.dependencies].
[project]
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)"
license = "GPL-3.0-or-later"
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
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):
path = tmp_path / "fill_res_config.json"
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_load_table.item(0, 1).text() == "snk"
# supplies carry R_out + V_oc columns, loads only I draw
assert dlg.pdn_sup_table.columnCount() == 8
assert dlg.pdn_load_table.columnCount() == 7
assert dlg.pdn_sup_table.columnCount() == 9
assert dlg.pdn_load_table.columnCount() == 8
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):
assert t.item(0, col).flags() & Qt.ItemIsEnabled
assert not (t.item(0, col).flags() & Qt.ItemIsEditable)
# the Active column is a checkbox, not an editable cell
assert t.item(0, 0).flags() & Qt.ItemIsUserCheckable
assert not (t.item(0, 0).flags() & Qt.ItemIsEditable)
# Active and Bonded are checkboxes, not editable cells
for col in (0, last - 3):
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, last - 3).checkState() == Qt.Unchecked
# the Layer column holds a combo, not a text item
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"
def test_bonded_flag_survives_the_table_roundtrip(app):
def test_bonded_checkbox_seeds_and_toggles(app):
setup = PdnSetup(rows=[
PdnTerminalRow(name="src", role="supply", resolved="r"),
PdnTerminalRow(name="pkg", role="load", resolved="2× rects",
bonded=True)], source="markers")
dlg = _dlg(app, pdn=setup, classic_reason="x")
_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()
assert sel.pdn_rows[0].bonded is False
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):
Generated
+1 -1
View File
@@ -216,7 +216,7 @@ wheels = [
[[package]]
name = "fill-resistance"
version = "1.4.0"
version = "1.4.1"
source = { virtual = "." }
dependencies = [
{ name = "kicad-python" },