Commit Graph

277 Commits

Author SHA1 Message Date
Ravi
7c385b993f feat(schematic): reposition and auto-place Ref/Value field labels
Adds field-placement tools:
- set_schematic_property_position / batch_set_schematic_property_positions:
  move a symbol's Reference/Value field labels
- autoplace_schematic_fields: place every symbol's fields clear of its body
  and nearby net labels (the #1 readability problem in generated schematics)
- check_schematic_layout: audit out-of-bounds / fields-in-body / duplicate
  labels (note: overlaps upstream find_overlapping_elements etc. — reuse or
  drop on request)

Generic .kicad_sch text helpers are factored into commands/schematic_text_utils.py
so the batch/hierarchy modules don't import from one another.

- python/commands/schematic_text_utils.py: shared text/S-expr helpers
- python/commands/schematic_field_layout.py: SchematicFieldLayoutCommands
- src/tools/schematic-layout.ts + registry 'schematic_layout' category
- python/kicad_interface.py: import + instantiate + dispatch routes
- tests/test_schematic_field_layout.py: 23 unit tests incl. end-to-end on real .kicad_sch

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 17:45:17 -04:00
Ravi
a9d7af5edf feat(schematic): hierarchical sheet insertion and subsheet scaffolding
Adds add_hierarchical_sheet (insert a sheet symbol referencing a child
.kicad_sch, with sheet_instances + fixed component instance paths) and
create_hierarchical_subsheet (create the child file + wire it into the parent
in one call). Upstream has add_sheet_pin / add_schematic_hierarchical_label
but no way to create a sheet or stand up a child sheet, so hierarchical
designs can't be built through the MCP server today.

- python/commands/schematic_hierarchy.py: SchematicHierarchyCommands
- src/tools/schematic-hierarchy.ts + registry 'schematic_hierarchy' category
- python/kicad_interface.py: import + instantiate + dispatch routes
- tests/test_schematic_hierarchy.py: 5 unit/integration tests on real .kicad_sch

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 17:41:12 -04:00
mixelpixx
df82ff5c56 Merge pull request #226 from ravishivt/feat/list-symbol-pins
feat(schematic): add list_symbol_pins to read pins from symbol libraries
2026-06-03 17:38:30 -04:00
mixelpixx
6fdbb65ce6 Merge pull request #212 from mixelpixx/fix/issue-209-kicad10-board-view
fix(board-view): KiCad 10 kicad-cli svg export compatibility (#209)
2026-06-03 17:34:13 -04:00
Ravi
f5d7c0aaf4 feat(schematic): add list_symbol_pins to read pins from symbol libraries
Adds list_symbol_pins and batch_list_symbol_pins: read a symbol's pin
number/name/type/local-position straight from the .kicad_sym libraries,
without placing it on a schematic. Fills a gap in library_symbol.py
(which can search/list symbols and read properties but not pins) and
complements get_schematic_pin_locations (placed-symbol coords only).

- python/commands/symbol_pins.py: SymbolPinCommands (stateless)
- src/tools/library-symbol.ts: tool wrappers; registry 'symbol_pins' category
- python/kicad_interface.py: import + instantiate + dispatch routes
- tests/test_symbol_pins.py: 12 unit tests (MagicMock loader; no system KiCad)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 14:36:22 -07:00
Denis
f7a92c6eb2 fix(library): rebuild SymbolLibraryManager when cache is empty
When a project is opened for the first time (e.g. right after
create_project) the sym-lib-table may not exist on disk yet.
SymbolLibraryManager.__init__ succeeds but leaves self.libraries={}.

The original guard in use_project() was:

    if self.library_manager.project_path == project_path:
        return

Because the path already matched the newly-created manager, the
early-return fired and the manager was never rebuilt once the
sym-lib-table appeared.  All subsequent list_symbols / search calls
returned nothing.

Fix: also require that at least one library was loaded before
treating the cache as valid.

Adds three unit tests that cover:
- rebuild triggered when project_path matches but libraries={}
- no spurious rebuild when libraries are already loaded
- rebuild on project_path change (existing behaviour)
2026-06-01 18:12:36 +05:00
mixelpixx
3b4ffef724 Merge pull request #196 from gcolonese/feat-schematic-justify
feat(schematic): expose justify directive on field labels via MCP
2026-05-30 12:15:28 -04:00
mixelpixx
f95de32cdd fix(logging): bound log size + honor LOG_LEVEL + mute kicad-skip (#181)
The server wrote gigabytes to ~/.kicad-mcp/logs and ignored LOG_LEVEL. Three
root causes, all fixed here (the logging carve-out of #182):

- Python (kicad_interface.py): replace the unbounded FileHandler with a
  RotatingFileHandler (10 MB x 3 backups, env-tunable via KICAD_MCP_LOG_MAX_BYTES
  / KICAD_MCP_LOG_BACKUP_COUNT); read the level from KICAD_MCP_LOG_LEVEL or
  LOG_LEVEL (default INFO) instead of hardcoding DEBUG; mute the noisy
  skip / skip.sexp.* loggers to WARNING unless KICAD_MCP_DEBUG_SKIP is set.
- TypeScript (config.ts): honor KICAD_MCP_LOG_LEVEL / LOG_LEVEL for the TS logger.
- TypeScript (logger.ts): size-cap the per-day log files with the same env knobs.
- Docs + a no-network test for the env helpers, skip muting, and that no
  unbounded handler targets kicad_interface.log.

Verified: LOG_LEVEL is now applied, skip is muted, and the file rotates instead
of growing forever. Full suite unchanged from baseline.

The hierarchical-sheet rewrite from #182 is intentionally left out (stays as a
separate PR pending the #169/#170 design discussion).

Co-Authored-By: angelorodem <angelorodem@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 12:07:03 -04:00
mixelpixx
066e7f63d4 fix(board-view): KiCad 10 kicad-cli svg export compatibility (#209)
KiCad 10 changed `kicad-cli pcb export svg` in three ways that broke
get_board_2d_view (reported with a verified fix in #209):
  - `--output` is now a FILE path, not a directory (a dir fails with
    "Failed to create file '<dir>'").
  - `--mode-single` is required to merge layers into one SVG (the default
    multi-file behavior is deprecated).
  - exit code 2 is returned for the deprecation warning even on success.

Pass a file path to `--output` and add `--mode-single` (both also valid on
KiCad 8/9). Stop gating on the exit code — judge success by whether an SVG was
actually produced — so the exit-2 deprecation warning is no longer a false
failure. Add regression tests for the exit-2-is-success case and the command
shape (file output + --mode-single).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 09:42:20 -04:00
mixelpixx
74a153aab4 Merge pull request #207 from karu2003/feat/schematic-mirrorY
feat(schematic): add angle and mirrorY to add_schematic_component
2026-05-30 09:34:45 -04:00
mixelpixx
40d32d187c Merge pull request #203 from jbjardine/cojbdev/kicad-mcp-lib-table-spaces
fix: parse quoted KiCad library table URIs with spaces
2026-05-30 09:33:09 -04:00
mixelpixx
034ffb79bd Merge pull request #204 from mixelpixx/fix/issue-199-jlcpcb-prebuilt-download
fix(jlcpcb): download prebuilt catalog instead of broken JLCSearch offset loop (#199)
2026-05-30 09:29:48 -04:00
mixelpixx
a18e729076 fix(jlcpcb): skip re-download when cache file already complete (#199, #204)
bhoot found that download_cdfer 416-loops when a complete cdfer.sqlite3 already
exists in the cache (e.g. a prior run downloaded it but died before
conversion/cleanup): resuming a complete file sends Range: bytes=<size>- which
the server answers with HTTP 416, and raise_for_status treated that as a
retryable error, spinning until max_retries then failing.

Fix: HEAD once for Content-Length up front and short-circuit when the local file
already matches the full size; and in the loop, treat a 416 as "complete" when
the size matches (else drop the stale partial and restart fresh). Remove the now
-unused _head_last_modified helper (HEAD is done inline). Add a no-network test.

Verified live: happy path still downloads+converts 616k parts (~50s); a
pre-existing complete file now short-circuits in ~0.4s instead of looping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 09:29:22 -04:00
mixelpixx
00618befb6 Merge pull request #211 from MoChahadeh/ref-based-tools-fix
Fix: reference-based schematic tools fail when lib_symbols contains a paren inside a quoted string
2026-05-30 08:56:27 -04:00
Mo Chahadeh
fdc760a73a Fix ref-based tools fail bug 2026-05-29 20:28:15 +03:00
Mo Chahadeh
36820d6897 Fix MCP tool launch timeout on macos / unable to connect 2026-05-29 14:10:22 +03:00
Gavin Colonese
7313037dc3 feat(schematic): expose justify directive on field labels via MCP
Add optional `justify` property to `fieldPositions` entries in
`edit_schematic_component` and to `set_schematic_component_property`.

Changes:
- `python/kicad_interface.py`: new `_set_justify_on_property()` helper
  that adds/replaces/removes the `(justify ...)` token inside a property's
  `(effects ...)` block. Passing "center" (the KiCad default) removes the
  directive entirely. Integrated into `_set_property_in_block()` (for the
  `properties` dict path) and into the `field_positions` loop in
  `_handle_edit_schematic_component()`. `_handle_set_schematic_component_property()`
  now forwards `justify` from params through to the spec dict.
  Also fixes pre-existing mypy type-ignore on `circle.radius` (kipy stub).
- `src/tools/schematic.ts`: extend the `fieldPositions` Zod schema to accept
  `justify?: string | string[]` (array form normalised to a space-separated
  string before the Python call). Add `justify?: string` to
  `set_schematic_component_property`.
- `python/commands/routing.py`: fix pre-existing mypy error — annotate `ex`
  and `ey` as `float` in `_point_to_segment_distance_nm`.
- `python/commands/pin_locator.py`: fix pre-existing mypy error — use explicit
  `str()` cast on `pin_data["number"]` before `dict.get()` call.
- `tests/test_schematic_field_justify.py`: 14 unit + integration tests
  covering add/replace/remove of the justify directive and backward
  compatibility (calls without justify leave existing directives untouched).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 15:29:00 -04:00
Jeff Laflamme
6c44273d55 fix(board-view): rebase onto main — fold responseMode + restore jpg
Rebased onto current main (d765bfe). Merged changes:

Board view (kicad-cli, cffi-free):
- Replace pcbnew/PLOT_CONTROLLER + cairosvg with kicad-cli SVG export
  and cffi-free PNG conversion: pymupdf → inkscape → imagemagick chain
- pcbPath optional param with fallback to loaded board
- Restore jpg output via PIL post-processing on PNG bytes
- responseMode inline/file from main's #161: inline returns imageData,
  file writes <board>_2d_view.<ext> and returns filePath
- shutil.which for cross-platform kicad-cli lookup
- Explicit TimeoutExpired catch; errorDetails on all error returns
- Validate fmt strictly; TypeScript returns image type for inline png/jpg

Schematic view (kicad_interface.py):
- Same cffi-free _svg_to_png helper for get_schematic_view and
  get_schematic_view_region (pymupdf → inkscape → imagemagick)

Tests:
- Update test_get_board_2d_view_save_to_file.py to mock subprocess/
  kicad-cli and _svg_to_png instead of PLOT_CONTROLLER/cairosvg
- All 5 responseMode tests pass
2026-05-27 10:50:21 +07:00
ka ru
2ced6764b0 fix: restore property-position regex for RefDes/Value placement
cacb27b omitted the closing parenthesis anchor in _extract_lib_property_positions, leaving lib_props empty and falling back to generic resistor positions.
2026-05-26 19:59:29 +02:00
ka ru
5a9790301e feat: add angle rotation and mirrorY horizontal flipping for components
- New angle parameter for component rotation (degrees, KiCad CCW-in-screen)
- New mirrorY parameter for horizontal symbol flipping (transistor orientation)
- Property positions (Reference, Value, Footprint) automatically rotated
- Property text effects preserved from library definitions
- Updates to add_schematic_component tool with new parameters
- Implements proper S-expression formatting for (mirror y) attribute
- Text angles and offsets derived from library properties for correct placement
2026-05-26 19:50:54 +02:00
mixelpixx
030d008843 feat(jlcpcb): make the FULL ~10GB catalog reachable + resumable downloads (#199)
People want to be able to pull the whole catalog, and prior downloads stalled or
repeated the same parts. Two fixes:

- Full catalog correctness: yaqwsx's cache.sqlite3 (the full ~10GB set) stores
  category/manufacturer as IDs with no v_components view, so the convert left
  those fields blank. Build an equivalent v_components join for yaqwsx-style
  sources so the full catalog converts with category/subcategory/manufacturer
  populated. Clarify in the tool schema that source="yaqwsx" = full catalog
  (needs 7z) vs cdfer = in-stock subset.
- Resumable downloads: CDFER stream download now uses a read timeout and resumes
  from the partial file via HTTP Range on interruption (up to 5 retries); the
  yaqwsx curl calls use -C - / --retry. Addresses the stall/partial-download
  complaints.

Adds a yaqwsx-schema conversion test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 13:57:07 -04:00
mixelpixx
cb5f744754 feat(jlcpcb): warn when downloaded catalog is stale (#199)
Per review feedback: CDFER's upstream scraper pipeline has stalled for weeks
(broken cart API + a bug in their scraper), so a "fresh download" can still be
old data. Compute the catalog age from the source Last-Modified header, expose
catalog_age_days, and emit a stale=True + warning when older than 14 days that
points users to source='yaqwsx' (fresh, needs 7z) or source='official'. Surface
the warning in the MCP tool output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 13:49:40 -04:00
mixelpixx
b21b1410eb fix(jlcpcb): download prebuilt catalog instead of broken JLCSearch offset loop (#199)
The download_jlcpcb_database tool paged the community JLCSearch API with an
offset parameter, but that endpoint is a search front-end that ignores offset
and returns the same first 100 parts on every page, so a full catalog download
was impossible.

Add commands/jlcpcb_downloader.py with a layered strategy that reuses prebuilt
catalogs the whole ecosystem already trusts:
  - CDFER single-file SQLite (primary; no 7z/zip, reliable on Windows)
  - yaqwsx/jlcparts split 7z (fallback; only if a 7z CLI is present)
  - official JLCPCB API (optional; cursor pagination, if credentials set)

Conversion reads CDFER's v_components view (or sniffs the largest table for
yaqwsx), C-prefixes integer lcsc, derives library_type from basic/preferred,
maps mfr->mfr_part, and normalizes price JSON to the manager's [{qty,price}]
shape. Rewire _handle_download_jlcpcb_database to use it (closing/reopening the
manager connection so the on-disk db can be rewritten on Windows). Remove the
broken offset loop from jlcsearch.py (client kept for interactive lookups).
Reduce download_jlcpcb.py to a thin CLI wrapper and update the TS tool schema.

Verified end-to-end against live CDFER: 616k parts downloaded + converted in
~40s, FTS search and price-break parsing correct. New unit tests cover the
conversion and source fall-through; no network in tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 11:53:12 -04:00
jbjardine
458631b24c fix: parse quoted KiCad library table URIs with spaces 2026-05-24 12:34:47 +02:00
jbjardine
21fc0702ac Add backend state MCP tool 2026-05-23 23:08:23 +02:00
mixelpixx
df558295aa Merge pull request #198 from bhoot1234567890/fix/issue-195-macos-wxapp-warmup
fix: macOS wxApp warm-up + symbol cache eager loading (#195)
2026-05-23 10:04:20 -04:00
Chaitanya Malhotra
ef6c135cda fix: macOS wxApp warm-up + symbol cache eager loading (#195)
Three-phase MCP server startup: wait for Python READY handshake, send
_warmup command (pcbnew.BOARD() triggers wxApp init on macOS), connect
to MCP transport only after warm-up completes.

Also pre-populate symbol library cache during SymbolLibraryManager
init so the first search_symbols call doesn't parse 241 .kicad_sym
files from disk (30-120s). Both warm-up and cache population happen
before tools are registered with the MCP client.

Fixes #195
2026-05-23 05:39:24 +05:30
scorp508
6113e0c27a fix: reconnect IPC backend after KiCad starts (#140)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-22 17:41:47 -04:00
Matthew Runo
f65eaf2798 fix(swig): detect and recover from BOARD proxy dehydration (#173)
KiCAD nightly builds occasionally return a SwigPyObject from
pcbnew.LoadBoard, and SaveBoard can leave self.board with no method
dispatch as a side-effect of certain sequences (delete_trace + auto-save
is the one users have hit in the wild). Before this fix, open_project
would keep reporting "Opened project: foo.kicad_pcb" while every
subsequent board operation failed with AttributeError on
GetDesignSettings / GetBoardEdgesBoundingBox / GetCurrentViaSize, with
no path to recovery short of restarting the MCP server.

Add two helpers in KiCADInterface:

* _is_board_healthy(board=None) probes for stable BOARD methods
  (GetDesignSettings, GetBoardEdgesBoundingBox, GetFileName) — these
  are missing on a dehydrated SwigPyObject, so hasattr() catches the
  state without segfaulting.
* _safe_load_board(path) wraps pcbnew.LoadBoard, checks health, and
  on dehydration reloads the pcbnew module via importlib.reload and
  retries once. Returns None when recovery is impossible so callers
  surface real failure rather than fake success.

Wire the helpers in:

* handle_command's open_project / create_project path validates the
  loaded board and either recovers (with a warnings[] entry) or
  returns success=False with an explicit "restart the MCP server"
  errorDetails — never claims success when the board is unusable.
* _auto_save_board now detects dehydration introduced by SaveBoard
  itself and reloads from disk so the next command sees a usable
  proxy. This is the post-delete_trace failure mode users hit.
* _handle_place_component, _handle_sync_schematic_to_board,
  _handle_import_svg_logo and _handle_refill_zones all go through
  _safe_load_board instead of bare LoadBoard, surfacing real errors
  consistently.

Also fix two adjacent issues observed in the same incident:

* _handle_check_kicad_ui used to call manager.is_running() and
  manager.get_process_info() separately, with different detection
  methods. They could disagree, producing the confusing
  running=True, processes=[] state users hit after manually
  quitting KiCAD. processes is now the single source of truth and
  running is derived from len().
* run_drc accepts a timeoutSec param (default 600s, clamped to
  [10, 1800]) so callers with smaller MCP transport budgets can
  bound the kicad-cli subprocess. Same timeout is applied to the
  optional report-generation subprocess. Error message names the
  actual timeout that fired.

Tests: tests/test_swig_dehydration.py adds 17 unit tests covering
detection, recovery, the open_project surfacing path, the auto-save
post-recovery path, the check_kicad_ui consistency, and the run_drc
timeout clamping. Full suite: same 12 pre-existing failures both
before and after this change, +17 new tests passing.

Note: the SWIG dehydration is fundamentally a pcbnew memory bug
exposed by repeated LoadBoard calls in a single Python process;
this PR is a defensive recovery layer, not a fix to the underlying
binding. Complementary to PR #151 (auto-save-guard), which expands
the LoadBoard call rate by refusing saves on external file change
and forcing re-open_project cycles.
2026-05-22 17:38:59 -04:00
Gavin Colonese
fa6cdcc0cd feat: add mil unit support across position/coordinate commands (#162)
* feat(units): add mil unit support across all position/coordinate commands

KiCad natively supports mils, so the MCP server should too. Added "mil"
as a valid unit option in tool schemas and updated all unit-to-nanometer
scale conversions across component, routing, outline, view, and IPC
handler code paths. 1 mil = 25400 nm (0.0254 mm).

Also fixes a pre-existing mypy overload error in pin_locator.py (str cast
on dict.get key) that was blocking pre-commit on any Python file change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(units): add mil to TypeScript tool schemas

The Python-side mil support was added but the actual input validation
happens in the TypeScript/Zod schemas. Updated all z.enum(["mm", "inch"])
to include "mil" across board, component, routing, design-rules, and
export tool definitions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tools): replace CP-1252 mojibake with correct Unicode in board.ts

Replace U+00C3 U+00D7 (×) with U+00D7 (×) in add_logo size output string.
Character was mangled when file was saved as CP-1252 instead of UTF-8.

* fix: restore em-dash and fix pre-commit mypy in component/routing

component.py: replace CP-1252 mojibake (â€") with correct Unicode
em-dash (—) in the 'Add to board first' comment. Addresses
maintainer review on PR #162.

routing.py: annotate ex/ey as float at first assignment site in
_point_to_segment_distance_nm so mypy pre-commit hook passes
cleanly on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 17:29:34 -04:00
NiNjA-CodE
76e644e4ef feat(routing): add add_gnd_stitching_vias MCP tool with all-layer collision (#191)
Drop GND stitching vias across the board with collision checking
against every non-GND segment, via, and pad on every copper layer.
PTH vias penetrate the full stackup, so an F.Cu-only check (the most
common shortcut) silently creates shorts on inner / B.Cu copper —
this implementation explicitly walks all layers.

  grid          Regular grid across the board interior. Default
                spacing 5mm.

  around_refs   Densify around specified footprints (e.g. MCUs,
                switching regulators, RF parts). Configurable
                density via densifyRadius.

  in_zones      Restrict placements to candidates inside the filled
                polygons of GND copper zones, so each new via lands
                on copper that's already a GND equipotential.
                Recommended on boards where the GND zone is fragmented:
                these vias actually stitch real polygons rather than
                floating on silkscreen.

All three strategies use the same collision check + intra-call
clump-prevention, so passing `["grid", "around_refs", "in_zones"]`
is a safe kitchen-sink configuration.

  - Auto-detect GND net (tries GND / GROUND / VSS / /GND in order)
    OR explicit `gndNet` parameter.
  - Per-via geometry control: viaSize, viaDrill, clearance.
  - edgeMargin: keep-out distance from board edge.
  - maxVias: cap on total placements (useful for incremental work).
  - dryRun: return placements without modifying the board — for
    previewing before committing.
  - Validates viaDrill < viaSize, rejects unknown strategy names,
    surfaces clear errors when GND net can't be resolved or the
    board outline is missing.

Approach ported from morningfire-pcb-automation
(https://github.com/NiNjA-CodE/morningfire-pcb-automation,
scripts/ground/add_gnd_vias.py). The original parses the PCB text
with regex and writes vias by string concatenation; this port reads
obstacles via the pcbnew API (handles rotated footprints, integrates
with the live in-memory board so two sequential calls see each
other's placements, picks up net codes from the loaded board) and
adds the in_zones strategy, the maxVias cap, and dry-run mode.

Credit is in the docstring, the TypeScript wrapper comment, the MCP
tool description (visible to clients), and the CHANGELOG entry.

tests/test_add_gnd_stitching_vias.py — 18 cases, all passing.
Uses mocked pcbnew objects so the suite runs under both the conftest
stub and a real pcbnew install.

  - grid strategy fills empty board with correct count
  - collision blocks via near a signal track (with extent assertion)
  - GND-net obstacles are correctly ignored
  - around_refs densifies near footprints with bounded extent
  - in_zones rejects candidates outside HitTestFilledArea
  - dryRun does NOT call board.Add
  - actual run calls board.Add per placement
  - maxVias caps total placements
  - intra-call clump prevention (asserts pairwise distance)
  - viaDrill >= viaSize is rejected
  - unknown strategy name is rejected
  - missing GND net returns clear error payload
  - no board loaded returns clear error
  - named GND net (e.g. VSS) is honoured even when GND also exists
  - direct unit tests for _point_to_segment_distance_nm helper

Real-board smoke test on TuneForge_TF001 (4-layer, 44 footprints):
  - GND net auto-detected
  - grid spacing 4mm: 141 placements, 129 blocked by collision
  - grid + in_zones: 140 placed, 15 rejected by zone membership,
    115 blocked by collision

  python/commands/routing.py         (+impl, ~370 LOC)
  python/kicad_interface.py          (+handler registration)
  python/schemas/tool_schemas.py     (+MCP schema)
  src/tools/routing.ts               (+TypeScript surface, builds clean)
  tests/test_add_gnd_stitching_vias.py (+18 tests)
  CHANGELOG.md                       (+Unreleased -> New MCP Tools)
2026-05-19 21:17:25 -04:00
Gavin Colonese
f03a74a93f Feat: add bounding box and courtyard info to component queries (#163)
Components now include boundingBox (min/max X/Y, width, height) in
get_component_list and get_component_properties responses. The SWIG
get_component_properties also includes courtyard dimensions when the
footprint defines a courtyard layer.

SWIG backend uses GetBoundingBox() and GetCourtyard(). IPC backend
tries get_item_bounding_box(), then pad-extent fallback, then falls
back to SWIG backend data when available. This ensures bounding box
data is present regardless of which backend handles the query.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 09:36:33 -04:00
gsdali
95e23c70ff fix: resolve ${KICAD_3RD_PARTY} (no version prefix) in lib URI resolvers (#187)
The Import-LIB-KiCad-Plugin documentation registers third-party libraries
using `${KICAD_3RD_PARTY}` (without a KiCad-version prefix). KiCad accepts
both unprefixed and version-prefixed forms in lib-tables, and the analogous
`KICAD_SYMBOL_DIR` was already handled in `library_symbol.py`'s env-var
dictionary.

`KICAD_3RD_PARTY` was missing from both `_resolve_uri` env-var dictionaries
(in `python/commands/library.py` and `python/commands/library_symbol.py`),
so lib-table rows authored as `${KICAD_3RD_PARTY}/Foo.kicad_sym` or
`${KICAD_3RD_PARTY}/Foo.pretty` failed to substitute, were treated as
non-existent paths, and disappeared from `list_symbol_libraries` /
`list_libraries` results — even though KiCad's GUI showed them correctly.

Changes:
  - Add `KICAD_3RD_PARTY` to both `_resolve_uri` env-var dictionaries.
  - Add `KICAD_3RD_PARTY` fallback in
    `SymbolLibraryManager._find_3rd_party_dir`.
  - Refactor `LibraryManager._find_kicad_3rdparty_dir` to check all four
    env-var forms (KICAD10/9/8_3RD_PARTY + KICAD_3RD_PARTY) consistently.
  - Add regression tests in `tests/test_kicad_3rd_party_env_resolution.py`.

Reproduces with:
  - Set `KICAD_3RD_PARTY` env var in the MCP server's environment.
  - Register `(lib (name "Foo") (type "KiCad") (uri "\${KICAD_3RD_PARTY}/Foo.kicad_sym") ...)` in the global sym-lib-table.
  - Place a real `Foo.kicad_sym` at the resolved path.
  - Before: `list_symbol_libraries` does not return `Foo`.
  - After: `Foo` is listed.
2026-05-18 23:19:50 -04:00
NiNjA-CodE
e2941631c1 feat(autoroute): add best-of-N support (attempts, targetNets, passSchedule) (#190)
The existing single-shot autoroute leaves 1-7 nets unrouted on dense
boards in my testing. Best-of-N drives that to 0 most of the time by
running Freerouting a few times with varied --max-passes and keeping
the SES with the best routing score.

New optional parameters (all backward-compatible):

  attempts:     int, default 1 (unchanged behaviour). When > 1, run
                Freerouting N times and pick the highest-scoring SES.
  targetNets:   list of critical net names. An attempt that routes all
                of them earns a 50,000-point scoring bonus.
  passSchedule: list of --max-passes values to cycle through across
                attempts. Default: [50, 60, 65, 70, 75, 80, 85, 90, 55,
                95] (wraps if attempts > len). Ignored when attempts=1
                (legacy maxPasses still used).

Scoring contract (pinned by tests):
  score = nets_routed * 1000 + segments
  if targetNets and all routed: score += 50_000

  - +1 net always beats any segment-count delta (1000 pt step).
  - Segments break ties at equal net count.
  - Target bonus dominates net-count gains from unrelated nets.

## Implementation notes

  - When attempts > 1, each attempt runs with `-mt 1` (single-thread
    optimisation). Freerouting 2.x's multi-threaded optimiser is
    documented to introduce clearance violations, so forcing
    single-thread during scoring keeps the comparison apples-to-apples.
  - One failed attempt does not abort the whole best-of-N run. The
    failure is recorded in the response under attempts[] with ok=False,
    and the remaining attempts compete for best. If every attempt fails
    the response surfaces a clear error.
  - The winning SES is preserved as <stem>_best.ses next to the
    canonical <stem>.ses so the caller can inspect it after the run.
  - Response shape:
      attempts == 1:  unchanged (no attempts/best_attempt fields)
      attempts > 1:   adds attempts[], best_attempt, best_score,
                      best_ses_path

## Attribution

Scoring approach and default pass schedule ported from
morningfire-pcb-automation
(https://github.com/NiNjA-CodE/morningfire-pcb-automation,
scripts/routing/freeroute_runner.py). Credited in the function
docstring, the TypeScript wrapper comment, the tool description (visible
to MCP clients), and the CHANGELOG entry.

The MCP version adds: cleaner per-attempt result reporting, automatic
single-thread optimisation, graceful degradation on partial failure,
and explicit validation that surfaces clean error payloads for invalid
attempts values.

## Tests

  tests/test_autoroute_score.py             8 cases, scoring contract
  tests/test_autoroute_best_of_n.py         6 cases, orchestration logic

All 14 passing. Tests are pure-Python: subprocess is mocked so the
suite runs in any environment (no Java / Freerouting / KiCad required).

  - Single-attempt response shape unchanged
  - Best-of-three picks the highest-scoring SES
  - One nonzero exit attempt doesn't abort the run
  - passSchedule wraps when attempts exceeds len
  - targetNets bonus wins over higher raw net count
  - attempts=0 rejected with clean error before DSN export
  - +1 net (1000 pts) dominates any segment delta
  - Segments tiebreak at equal net count
  - Quoted net names in SES are normalised vs unquoted targets

TypeScript builds clean.
2026-05-18 23:03:38 -04:00
NiNjA-CodE
983ffc3793 feat(component): add check_courtyard_overlaps MCP tool (#189)
Detects courtyard overlaps between footprints and flags courtyards that
extend past the board outline. Returns overlap pairs with intersection
extents (mm), per-component boundary violations, and a placement summary.

The killer feature for AI-driven workflows is the `positions` parameter,
which accepts hypothetical placements `{ref: [x, y]}` or
`{ref: [x, y, rotation_degrees]}`. The tool evaluates the proposed
placement WITHOUT writing to the board file — so an AI agent can validate
a move_component / place_component before committing it, instead of the
current loop of write -> run DRC -> parse violations -> revert.

## Implementation

- Uses the real courtyard polygons from pcbnew (`fp.GetCourtyard(F_CrtYd)`
  or B_CrtYd) for accurate AABBs even on custom and rotated footprints.
- Falls back to `fp.GetBoundingBox()` when no F/B.Courtyard polygon is
  present.
- For virtual rotation, rotates the four AABB corners and re-axis-aligns.
  Conservative: the rotated-AABB is always >= the rotated-polygon, so
  overlap reports are never false-negatives (may be marginally
  over-cautious on diagonal rectangles, which is the right error bias
  for a placement validator).
- Optional `margin` parameter expands every courtyard by N mm — useful
  for enforcing a manufacturing keepout wider than the symbol's
  declared courtyard.

## Attribution

The approach is ported from morningfire-pcb-automation
(https://github.com/NiNjA-CodE/morningfire-pcb-automation), specifically
`scripts/placement/check_overlaps.py`. The upstream uses a static
per-footprint-type courtyard lookup table; this implementation reads
the real polygons from pcbnew so it works on any footprint without
maintaining a table. Attribution is in the function docstring, the
TypeScript wrapper, the tool's description (visible to MCP clients),
and the CHANGELOG entry.

## Tests

12 pytest cases in tests/test_check_courtyard_overlaps.py, all passing:

  - No overlaps when spaced; overlap detected on intersect
  - Margin pushes borderline pairs into overlap
  - `refs` filter restricts the check
  - Boundary violations are flagged; `include_boundary=false` suppresses
  - Virtual position does not mutate the footprint (asserts
    `SetPosition` is never called)
  - Virtual rotation swaps a tall-narrow courtyard's x/y extents
  - No-board-loaded returns clean error payload
  - Bad position spec (wrong arity) returns clean error payload
  - GetCourtyard() OutlineCount=0 -> fallback to GetBoundingBox()
  - `board_outline` override replaces the Edge.Cuts bbox

Tests use mocked pcbnew objects so they run under both the conftest stub
and a real pcbnew install. Real-board smoke test on a 44-footprint
production board succeeds: 1 known overlap detected (SW1<->SW2), 0
boundary violations, virtual placement test reports 6 expected overlaps.

## Files touched

- python/commands/component.py            (impl + helpers)
- python/kicad_interface.py               (tool registration)
- python/schemas/tool_schemas.py          (MCP schema entry)
- src/tools/component.ts                  (TypeScript surface, builds clean)
- tests/test_check_courtyard_overlaps.py  (12 cases)
- CHANGELOG.md                            (Unreleased -> New MCP Tools)
2026-05-18 23:03:34 -04:00
Gavin Colonese
457e4e30ad fix(ipc): rotate_component uses absolute angle (matches schema) (#159)
* Fix: IPC rotate_component now uses absolute angle as documented

The IPC rotate handler was adding the angle to the current rotation
(relative), but the schema documents it as absolute. This caused
unexpected behavior where setting angle=0 had no effect on a component
already at 180°. Now correctly sets the rotation to the exact angle
specified, matching the SWIG backend behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(changelog): add unreleased entry for rotate_component absolute-angle fix

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-18 23:00:22 -04:00
mixelpixx
c538714743 fix(edit_schematic_component): update (reference "...") inside (instances) on rename (#186)
Closes #126.

A placed schematic symbol carries its reference designator in two places:

  (symbol
    (property "Reference" "R5" …)        ← what eeschema renders
    (instances
      (project "MyProject"
        (path "/sheet-uuid/symbol-uuid"
          (reference "R5")               ← what netlist + PCB sync read
          (unit 1) )))
    …)

Before this change, `edit_schematic_component` with `newReference` updated
only the (property "Reference" …) field. The (reference "…") leaves inside
(instances) → (project) → (path) kept the old value. eeschema rendered the
new reference correctly and ERC passed, but `kicad-cli sch export netlist`
and "Update PCB from Schematic" both read from the (instances) block and
silently used the OLD reference — producing destructive PCB-sync diffs on
what users thought was a clean rename. Severity was high for anyone running
batch renames because the symptom only surfaces at PCB-sync time, by which
point many renames may be queued.

Walk the (instances) subtree within the matched symbol block after the
property update and replace every `(reference "OLD")` leaf with the new
value. The regex matches `(reference "X")` specifically (not
`(property "Reference" "X"`), and the walk is constrained to the
(instances …) range via the existing _find_matching_paren helper so other
(reference …) tokens elsewhere in the file can't be affected.

Adds tests/test_edit_schematic_component_instances.py covering:
  - Single-instance rename updates both property and instances leaf
  - Hierarchical case with multiple (path …) entries all updated atomically
  - No-instances-block schematics don't crash (older KiCad / partial files)
  - The regex doesn't clobber (property "Reference" …) on the instances pass
  - Other field values (Value, Footprint) are left intact
  - The response payload's updated.reference reflects the new ref

All 6 tests fail on main without the fix (3 fully, 3 on the instances
assertions only) and pass on this branch.

The pre-existing TestAddComponentMirrorParam failures in
test_add_schematic_component.py are unrelated and present on main —
documented in inktomi's PR #169.

Co-authored-by: mixelpixx <11727006+mixelpixx@users.noreply.github.com>
2026-05-18 14:55:54 -04:00
kevargaso
40d6d6bba1 Add query_zones tool for auditing copper pours (#174)
query_traces silently omits PCB_ZONE_T objects, so layer-usage audits
miss power planes and GND pours entirely. query_zones complements it by
iterating board.Zones() and returning each zone's net, layers, priority,
fill state, min thickness, bounding box, and filled area, with the same
net/layer/boundingBox filter surface as query_traces.
2026-05-18 14:40:18 -04:00
Sean Link
4e845f24ce fix(jlcpcb): use platform user data dir for parts database (#167)
JLCPCBPartsManager defaulted db_path to a "data/" directory computed
relative to __file__, which fails with read-only filesystems when the
package is installed to a system-managed prefix (e.g. /nix/store, an
immutable container image, or /usr/lib). The same pattern in
download_jlcpcb.py would silently scatter the ~1.5 GB JLCPCB cache
inside the install tree even when it is writable.

The original integration plan (docs/archive/JLCPCB_INTEGRATION_PLAN.md)
called for a per-user database under ~/.kicad-mcp/. This change moves
the default to the platform-appropriate user data directory by adding
a new PlatformHelper.get_data_dir() helper that mirrors the existing
get_config_dir() / get_cache_dir() conventions:

  - Linux:   XDG_DATA_HOME/kicad-mcp or ~/.local/share/kicad-mcp
  - macOS:   ~/Library/Application Support/kicad-mcp
  - Windows: %USERPROFILE%\.kicad-mcp\data

Both JLCPCBPartsManager and download_jlcpcb.py now resolve their
database paths through this helper. ensure_directories() and
detect_platform() include the new directory. Unit tests parallel to
the existing config_dir/cache_dir cases cover platform-appropriate
paths and the relative-XDG_DATA_HOME edge case.
2026-05-18 14:40:13 -04:00
karu2003
5c6b55453e feat(mcp): add route_arc_trace for true PCB arc primitives (SWIG + IPC) (#165) 2026-05-18 14:40:09 -04:00
Matthew Runo
53d9b4b567 fix: net-class API compatibility for KiCad 10 and trackWidth alias (#144)
`create_netclass` previously called legacy NETCLASSES.Find/.Add APIs that
were removed in KiCad 10. NETCLASS getters like GetMicroViaDiameter that
also no longer exist crashed any subsequent edit. The schema accepted
`traceWidth` but the handler only read `trackWidth`, so requests using
the documented field silently produced no-op netclasses.

- Add a KiCad-version-defensive shim around netclass creation that
  prefers the new netclasses_map dict-style API and falls back to legacy.
- Introduce _safe_get/_safe_set helpers so missing getters/setters on
  KiCad 10 NETCLASS objects fail gracefully instead of raising.
- Accept both traceWidth and trackWidth in the request payload.

Net classes still need to be written into .kicad_pro directly because
KiCad 10 stores them in net_settings.classes and the MCP only writes
to .kicad_pcb; that's a separate fix.
2026-05-18 14:34:30 -04:00
Gavin Colonese
b69a4eb88b feat(view): save 2D board view to file instead of base64 (#161)
* Feat: save 2D board view to file instead of returning base64

The 2D view was returning base64-encoded image data in JSON, which
often exceeded token/message size limits. Now saves the rendered
image (PNG/JPG/SVG) next to the PCB file and returns the file path.
This makes the output usable by tools that can read image files
directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(board): add opt-in responseMode param to get_board_2d_view

Add a responseMode string parameter (enum: inline | file, default inline)
so callers can choose how the rendered image is delivered.

- inline (default, pre-PR behavior): image bytes are base64-encoded
  and returned in the imageData response field -- backward-compatible.
- file: image is written next to the .kicad_pcb as
  <board>_2d_view.<ext> and filePath is returned -- resolves the
  MCP message-size limit problem on large boards.

Rendering logic is shared between both modes; only response packaging
differs. Updated tool schema (Python + TypeScript) and replaced the
existing test file with 5 focused unit tests covering inline/file modes
for PNG and SVG formats plus the default-is-inline contract.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-18 14:29:00 -04:00
Matthew Runo
c2c77f5995 fix(auto-save-guard): refuse only on content divergence, not mtime (#172)
* feat: guard SWIG auto-save against external file changes

After every board-mutating SWIG command, kicad_interface._auto_save_board()
unconditionally calls pcbnew.SaveBoard() with the in-memory board. When the
on-disk .kicad_pcb has been modified externally between our LoadBoard and
SaveBoard (KiCad GUI's own save, git checkout, another process), the
in-memory state silently overwrites those external changes - losing data
the user can't see was at risk.

This change records the file's mtime_ns + sha256 at LoadBoard and verifies
the signature matches before each auto-save:

  * If the signature has diverged, refuse the save and attach a structured
    warning to the command result so callers know their mutation is
    in-memory only and they need to reload before retrying.
  * If it matches, copy the existing file to .mcp-backups/<name>.<ts>
    (rotating, keeps last 20) before overwriting.
  * Update the recorded signature after our own writes so subsequent
    saves are not falsely flagged.

Backwards compatible:
  * No tool schemas changed.
  * Successful saves return as before, with an extra `autoSave` field
    when the wrapper observed something noteworthy.
  * Refused saves return success: true (the in-memory mutation did
    succeed) plus warnings: [...] and autoSave.diskChangedExternally,
    so callers can detect the situation programmatically.

Adds tests/test_auto_save_guard.py (10 tests, all passing) covering:
signature math, refusal on external change, backup creation + content,
backup rotation, first-save semantics (no recorded signature proceeds
normally), and skip cases (no board / no path).

Motivation: the aircam-pdb fork-user lost ~480 traces and the full
footprint layout to a silent overwrite incident on 2026-05-03; recovery
was only possible because VS Code's local-history extension happened to
have a snapshot from a few minutes earlier. This guard makes that class
of incident loud and locally recoverable.

* fix(auto-save-guard): refuse only on content divergence, not mtime

The guard added in 9ba0010 records `(mtime_ns, sha256)` as the file's
disk signature and refuses auto-save when the recorded tuple no longer
matches the current one. Comparing the full tuple meant any mtime delta
fired the refusal — including a bare `touch` of the file, an atime-style
backup tool, or any MCP read path that opened the .kicad_pcb between
load and save. Users were trapped: every write needed an explicit
save_project call to bypass the false positive (documented as a
workaround in fork users' notes).

Compare on sha256 only; mtime is incidental. The actual data-loss
scenario the guard is meant to catch — an external write that genuinely
changed the file — produces a different content hash, which is what the
guard now keys off. After a touch-only mtime advance with content
unchanged, refresh the recorded signature so we don't re-hash on every
subsequent call.

Drops the mtime-equality fast path on _disk_signature: a filesystem with
coarse mtime resolution (FAT32, some network mounts) could accept two
writes inside one mtime tick; trusting mtime as a hash cache key would
re-introduce a class of silent overwrites the guard exists to prevent.
The hash itself is cheap (sha256 over a typical .kicad_pcb completes in
tens of milliseconds).

Adds 4 regression tests in test_auto_save_guard.py:
  - touch-only mtime advance proceeds and refreshes the signature
  - content change at the same mtime is still refused (hash divergence
    must drive the decision, not tuple equality)
  - the user-facing warning calls out "contents", not "mtime"
  - _disk_signature returns the same hash when content is unchanged
    even after the file's mtime advances
2026-05-18 14:05:26 -04:00
Matthew Runo
679ccbf744 fix(sync_schematic_to_board): add missing footprints, not just nets (#171)
The handler iterated `board.GetFootprints()` and assigned nets to existing
pads, but had no path to *add* footprints for schematic symbols whose
Reference was not yet on the board. New parts placed on the schematic
landed in the net list with no PCB representation — the rats nest had
nowhere to terminate, and place_component / route_pad_to_pad would fail
because the target footprint did not exist.

KiCad's "Update PCB from Schematic" (F8) implicitly adds the missing
footprints; bring the MCP's behaviour in line.

Implementation:
* `_extract_components_from_schematic` runs `kicad-cli sch export netlist
  --format kicadxml` (the same path `_handle_generate_netlist` already
  uses) and returns a flat `[{reference, value, footprint}]` list. Walks
  hierarchical sub-sheets transparently because kicad-cli does.
* `_add_missing_footprints_from_schematic` resolves each missing component
  against the project's fp-lib-table via `LibraryManager`, calls
  `pcbnew.FootprintLoad`, sets reference / value / FPID, and places the
  footprint at the board origin (the user / autoplacer can position it).
  Power and flag references (`#PWR…`, `#FLG…`) are excluded — they have
  no PCB footprint.
* The pad-net assignment loop now runs *after* the add path, so newly
  placed footprints get their nets assigned in the same call.
* Response payload gains `footprints_added` and `footprints_skipped`
  diagnostic lists. The textual `message` field reports both the new
  footprint count and the existing net / pad counts.

Adds tests/test_sync_schematic_to_board_footprints.py — 9 unit tests
covering the add path (missing ref, already-present ref, power refs,
empty footprint, unknown library) and the kicad-cli helper (XML parse,
missing kicad-cli, non-zero exit).
2026-05-18 14:05:22 -04:00
Matthew Runo
e66c13361b fix(add_schematic_wire): support hierarchical sub-sheets (#170)
WireManager.add_wire and add_polyline_wire bailed out with
"No sheet_instances section found in schematic" when called on a
hierarchical sub-sheet — that block only exists in the root .kicad_sch.
The handler in kicad_interface.py converted the False return into a
flat "Failed to add wire" message, leaving callers with no diagnostic.

Apply the same fallback that WireManager.add_label has used since the
hierarchical-design support: when (sheet_instances ...) is absent,
append the new (wire ...) item at the end of the outer (kicad_sch ...)
form.

Adds tests/test_add_wire_sub_sheet.py with 6 regression tests covering
both add_wire and add_polyline_wire on a sub-sheet, including paren
balance, sexpdata round-trip, and segment count for an N-point polyline.
2026-05-18 14:05:18 -04:00
Matthew Runo
9843d75c91 fix(add_schematic_component): support hierarchical sub-sheets (#169)
DynamicSymbolLoader.create_component_instance only handled root
schematics: it located its insertion point via `(sheet_instances`,
which exists only in the root .kicad_sch. Adding a component to any
hierarchical sub-sheet raised "Could not find insertion point in
schematic" and aborted the call.

Fall back to inserting just before the closing paren of the outer
(kicad_sch ...) form when the marker is absent. WireManager.add_label
already uses the same fallback for hierarchical sub-sheets.

Adds three regression tests in TestCreateComponentInstanceSubSheet
covering successful insertion, paren balance, and sexpdata round-trip
on a sub-sheet without (sheet_instances).
2026-05-18 14:05:14 -04:00
Matthew Runo
4740013d24 fix(add_mounting_hole): set FPID and restrict NPTH pad to mask layers (#154)
`BoardOutlineCommands.add_mounting_hole` produced footprints with an empty
library:name id (`(footprint "" ...)` in the .kicad_pcb), which KiCad's GUI
Move tool refuses to select — users couldn't drag the resulting MHs in the
editor. It also emitted the pad with the default `*.Cu` + `*.Mask` LSET on
NPTHs; with `padDiameter > diameter` that creates phantom copper annular
rings on every Cu layer and trips clearance DRC against neighbouring nets.

Repro: call `add_mounting_hole` with `position={x:117,y:84.5,unit:"mm"},
diameter:3.2, padDiameter:3.5`. The resulting MH is unselectable and DRC
reports phantom F.Cu pad shorts to neighbouring component pads.

Changes:

- Set a real FPID via `module.SetFPID(pcbnew.LIB_ID(lib, name))`. Default
  is `MountingHole:MountingHole_<diameter:g>mm` (e.g. 3.2 → 3.2mm); a new
  optional `footprintLibId` parameter overrides.
- For NPTH (the default `plated:false`), restrict the pad's LSET to
  `F.Mask` + `B.Mask` only. PTH path is unchanged — the default Cu+Mask
  LSET is correct there.
- Update the schema in `tool_schemas.py`: previously advertised
  `x`/`y`/`diameter` at the top level, but the impl reads
  `position={x,y,unit}`, `padDiameter`, `plated`. Schema now matches the
  implementation and exposes the new `footprintLibId` param.
- New `tests/test_add_mounting_hole.py` regression suite (7 tests) asserting
  SetFPID is invoked with non-empty lib:name (default + override forms),
  NPTH SetLayerSet excludes any Cu layer, and PTH does not call
  SetLayerSet (preserves default Cu+Mask).
2026-05-18 14:05:10 -04:00
blinksoft
ff6ae63b8c fix(pin_locator): keep outer pin when a symbol defines a number twice (#179)
Some community-generated KiCad symbol libraries (e.g.
PCM_Diode_Schottky_AKL:MBRS130) define each pin number twice — once as a
visible "real" pin with non-zero length whose ``at`` coordinate is the
wire-connection endpoint, and once as an inner zero-length "ghost" pin
used as an internal graphic-anchoring join. Both definitions live inside
the same ``lib_symbols`` block.

``parse_symbol_definition`` stored pins via ``pins[number] = pin_data``
— a plain assignment. Each duplicate-numbered pin encountered during the
recursive walk overwrote the previous one. The recursion order put the
ghost pins last for MBRS130, so the ghost won and ``get_pin_location``
returned a coordinate that did not match any wire/label.

Downstream this caused ``get_connections_for_net`` to silently miss diode
pins on the rails they were wired to — on a real schematic, querying
``+BATT`` returned 8 of 9 expected nodes (D1/1 absent) and ``+3V3``
returned 44 of 46 (D1/2 and D2/2 absent), because the BFS could not find
the diode's pin endpoint at the labelled position.

Fix: when the same pin number is defined more than once, keep the entry
with the greater ``length``. The outer real pin has length > 0; the
inner ghost has length == 0. Strict-greater comparison resolves ties to
first-encountered, so legitimate same-length duplicates (e.g., per-unit
repetitions in multi-unit symbols) keep stable existing behaviour.

Tests: four unit tests in ``tests/test_pin_locator_duplicate_pin_defs.py``
cover (a) outer-then-ghost order (the real bug), (b) ghost-then-outer
order (length-not-order heuristic), (c) no-duplicate baseline regression,
and (d) equal-length tie keeps first-encountered. Two of the four fail
on main, all four pass on this branch.

Full suite: 671 passed, 11 skipped, 0 regressions (modulo the pre-existing
tests/test_get_pin_angle.py collection error which is unrelated to
pin_locator).

End-to-end on the real schematic that triggered the report: after the
fix ``get_connections_for_net('+BATT')`` returns all 9 expected nodes
matching ``kicad-cli sch export netlist`` exactly. The companion fix in
PR #177 (wire_connectivity pwr-flag bridge) closes the orthogonal
over-merge bug; together they bring net membership to full parity with
kicad-cli on schematics that use both ``PWR_FLAG`` markers and
diodes from community libraries.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:53:37 -04:00
blinksoft
68b8bbf2fb fix(wire_connectivity): pwr-flags must not bridge unrelated power rails (#177)
Every power:PWR_FLAG symbol carries Value="PWR_FLAG" — the ERC marker
inherits its rail's name from the wire/label it sits on. Commit 7f3a379
added #FLG symbols to the same handling loop as #PWR power ports inside
_parse_virtual_connections, so every pwr-flag was getting appended to
label_to_points["PWR_FLAG"]. The BFS in _find_connected_wires uses
label_to_points for virtual jumps; reaching any pwr-flag pin caused it
to teleport to every other pwr-flag pin, walking across each one's stub
wire into a different power rail. The result: get_net_connections(rail)
returned the union of pins on every rail that had a pwr-flag, for any
rail.

Fix: pwr-flag pin positions still register as anchors in point_to_label
(preserving the original intent of 7f3a379 so find_orphaned_wires keeps
accepting them), but they no longer enter label_to_points. The pwr-flag
remains electrically connected to its rail via the wire-graph BFS through
the wire it sits on; the label-jump mechanism is unnecessary for that
path and actively harmful when the "label" is the same for unrelated
rails.

Tests: three unit tests on _parse_virtual_connections cover the bug
(over-merge gone), regression check (power ports still work in both maps),
and edge case (pwr-flag and port at same point — port name wins). All
three fail on main, pass on this branch.

Full suite: 667 passed, 11 skipped, 0 regressions (modulo the pre-existing
tests/test_get_pin_angle.py collection error which is unrelated to
wire_connectivity).

End-to-end verification on a single-sheet schematic with 7 distinct
power rails each carrying a pwr-flag: every queried net now matches
the official kicad-cli netlist output (modulo a separate library-symbol
bug on PCM_Diode_Schottky_AKL:MBRS130 with duplicate pin definitions,
out of scope here).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:53:34 -04:00
Gavin Colonese
1f095cff59 fix(loader): read global sym-lib-table; quoted URIs with spaces (#164)
dynamic_symbol_loader only consulted the project-local sym-lib-table
and a hardcoded list of bundled symbol directories, so libraries
registered via the user-global sym-lib-table (Preferences > Manage
Symbol Libraries > Global) were invisible to add_schematic_component.
Common case: company libraries that live under OneDrive / a network
share / any other custom path the user added through the GUI.

Also widened the sym-lib-table parser regex to accept quoted URIs (and
quoted names) that contain spaces — required for paths like
"C:/Users/.../OneDrive - Company/Documents/KiCad/...". The old bare-
word capture stopped at the first space.

Search order is now:
1. Project sym-lib-table
2. User-global sym-lib-table (~/AppData/Roaming/kicad/<ver>/sym-lib-table
   on Windows, ~/.config/kicad/<ver>/sym-lib-table on Linux,
   ~/Library/Preferences/kicad/<ver>/sym-lib-table on macOS)
3. Bundled / well-known symbol directories

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:39:33 -04:00