Mark low-current copper as polygons on user layers (dialog opt-in)
Build PCM package / build (push) Successful in 7s
tests / ubuntu-latest · py3.11 (push) Successful in 53s
tests / ubuntu-latest · py3.13 (push) Successful in 1m43s
tests / archlinux:latest (push) Successful in 39s
tests / debian:12 (push) Successful in 49s
tests / fedora:latest (push) Successful in 1m1s
tests / ubuntu:24.04 (push) Successful in 1m26s
tests / NixOS (FHS wrapper from docs/NIXOS.md) (push) Has been skipped

New EXPERIMENTAL dialog option (default off): after the solve, copper
whose |J| is below a threshold (default 10% of the mean |J| over all
solved copper cells - mean, not max, since contact-corner spikes would
dwarf a max-relative threshold) is vectorized into filled graphic
polygons on TRIM_LAYERS (User.5..User.8, configurable), one polygon
per region so Edit > Convert can turn one into a rule area by hand.
Areas are printed and the polygons saved to low_current_copper.json.

The mask -> polygon step is the 0.5 contour of the binary field via
contourpy (already in every venv as matplotlib dependency), padded so
regions touching the raster edge close, simplified with
Douglas-Peucker at 0.4 cells: staircase bevels collapse, one-cell-wide
strips survive. Specks under TRIM_MIN_AREA_MM2 are dropped.

Explicitly a suggestion, not a safe cut list (docstring, dialog and
README all say so): copper carries little current BECAUSE the rest
carries it, so removal redistributes |J| - the constant-density
optimizer that iterates this to convergence is future work.

board_io: the create/delete-with-status-surfaced helpers are now
generic (_create_items_checked / _remove_items_checked) and shared
between reference-image overlays and trim polygons.

Tested on the dev stack (3.13) and the Python 3.9 mac-stack venv, 148
passed each; contourpy 1.3.x has identical API on both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
janik
2026-07-24 12:54:25 +07:00
co-authored by Claude Fable 5
parent 4f361d846a
commit 6d8634802d
8 changed files with 507 additions and 25 deletions
+24 -1
View File
@@ -40,6 +40,8 @@ class Selection:
cap_max_drill_mm: float = 0.5
adaptive: bool = True
push_overlays: bool = False # EXPERIMENTAL in-KiCad |J| overlays
trim_enabled: bool = False # EXPERIMENTAL low-current copper marking
trim_pct: float = 10.0 # threshold as % of the mean |J|
class _Dialog(QDialog):
@@ -130,6 +132,19 @@ class _Dialog(QDialog):
self.overlay_check.setChecked(config.PUSH_OVERLAYS)
form.addRow("Overlays:", self.overlay_check)
tfirst, tlast = config.TRIM_LAYERS[0], config.TRIM_LAYERS[-1]
self.trim_check = QCheckBox(
f"experimental: mark copper below the threshold as polygons "
f"on {tfirst}..{tlast} (replaces polygons there; a suggestion "
f"only - removing copper shifts current elsewhere)")
self.trim_check.setChecked(config.TRIM_ENABLED)
form.addRow("Low-current copper:", self.trim_check)
self.trim_edit = QLineEdit(f"{config.TRIM_THRESHOLD_PCT:g}")
self.trim_edit.setEnabled(config.TRIM_ENABLED)
self.trim_check.toggled.connect(self.trim_edit.setEnabled)
form.addRow("Threshold [% of mean |J|]:", self.trim_edit)
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttons.accepted.connect(self._try_accept)
buttons.rejected.connect(self.reject)
@@ -227,6 +242,12 @@ class _Dialog(QDialog):
extra_cu = number(self.extracu_edit, "Extra Cu")
if extra_cu < 0:
raise ValueError("Extra Cu must be ≥ 0 µm.")
trim_pct = config.TRIM_THRESHOLD_PCT
if self.trim_check.isChecked():
trim_pct = number(self.trim_edit, "Trim threshold")
if not 0 < trim_pct < 100:
raise ValueError("Trim threshold must be between 0 and "
"100 (% of the mean |J|).")
cap_max_drill = config.CAP_MAX_DRILL_MM
if self.capped_check.isChecked():
cap_max_drill = number(self.cap_drill_edit, "Capped up to drill")
@@ -251,7 +272,9 @@ class _Dialog(QDialog):
vias_capped=self.capped_check.isChecked(),
cap_max_drill_mm=cap_max_drill,
adaptive=self.adaptive_check.isChecked(),
push_overlays=self.overlay_check.isChecked())
push_overlays=self.overlay_check.isChecked(),
trim_enabled=self.trim_check.isChecked(),
trim_pct=trim_pct)
def _try_accept(self) -> None:
try: