From 19327c23b167958d79861c1b906a1399c71e67e7 Mon Sep 17 00:00:00 2001 From: janik Date: Thu, 27 Aug 2026 17:20:35 +0700 Subject: [PATCH] Release 1.4.1: a Bonded checkbox in the terminal tables 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 --- README.md | 18 ++++++++++++------ docs/release-notes/v1.4.1.md | 12 ++++++++++++ fill_resistance/__init__.py | 2 +- fill_resistance/configfile.py | 9 +++++++-- fill_resistance/dialog.py | 36 ++++++++++++++++++++++++----------- fill_resistance/main.py | 11 ++++++----- metadata.json | 2 +- pyproject.toml | 2 +- tests/test_configfile.py | 14 ++++++++++++++ tests/test_dialog.py | 26 +++++++++++++++++++------ uv.lock | 2 +- 11 files changed, 100 insertions(+), 34 deletions(-) create mode 100644 docs/release-notes/v1.4.1.md diff --git a/README.md b/README.md index cb0d1ed..b34cbfa 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/docs/release-notes/v1.4.1.md b/docs/release-notes/v1.4.1.md new file mode 100644 index 0000000..3ed9a37 --- /dev/null +++ b/docs/release-notes/v1.4.1.md @@ -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). diff --git a/fill_resistance/__init__.py b/fill_resistance/__init__.py index 182204a..6020de0 100644 --- a/fill_resistance/__init__.py +++ b/fill_resistance/__init__.py @@ -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" diff --git a/fill_resistance/configfile.py b/fill_resistance/configfile.py index f93edc8..c45a990 100644 --- a/fill_resistance/configfile.py +++ b/fill_resistance/configfile.py @@ -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: diff --git a/fill_resistance/dialog.py b/fill_resistance/dialog.py index 5f720b1..69f1ea1 100644 --- a/fill_resistance/dialog.py +++ b/fill_resistance/dialog.py @@ -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() diff --git a/fill_resistance/main.py b/fill_resistance/main.py index fa603fb..5533d11 100644 --- a/fill_resistance/main.py +++ b/fill_resistance/main.py @@ -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 = ( diff --git a/metadata.json b/metadata.json index 72c7e2b..5ce102c 100644 --- a/metadata.json +++ b/metadata.json @@ -17,7 +17,7 @@ }, "versions": [ { - "version": "1.4.0", + "version": "1.4.1", "status": "stable", "kicad_version": "10.0", "runtime": "ipc" diff --git a/pyproject.toml b/pyproject.toml index 93a4957..616fa11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/tests/test_configfile.py b/tests/test_configfile.py index d8467e9..ebfe462 100644 --- a/tests/test_configfile.py +++ b/tests/test_configfile.py @@ -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="", diff --git a/tests/test_dialog.py b/tests/test_dialog.py index 2af619f..efbb89b 100644 --- a/tests/test_dialog.py +++ b/tests/test_dialog.py @@ -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): diff --git a/uv.lock b/uv.lock index b972382..313187d 100644 --- a/uv.lock +++ b/uv.lock @@ -216,7 +216,7 @@ wheels = [ [[package]] name = "fill-resistance" -version = "1.4.0" +version = "1.4.1" source = { virtual = "." } dependencies = [ { name = "kicad-python" },