* 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>
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>
Two bugs in WireDragger.pin_world_xy (and corresponding bugs in
PinLocator.get_pin_angle) caused pin coordinates and angles to land on
the wrong pin in 4 of 8 polarized cases (rot=90, rot=270, mirror x on a
vertical part, mirror y on a vertical part). Verified end-to-end against
`kicad-cli sch export netlist`.
(1) Rotation direction. After PR #145's `-ly` Y-flip, calling the
standard math (Y-up CCW) `_rotate` is effectively CW in screen Y-down.
eeschema's TRANSFORM(0,1,-1,0) for rot=90 is screen-CCW. They agreed at
0° and 180° (where the rotation matrices coincide) but disagreed at 90°
and 270°.
(2) Mirror axis semantics swapped. Per eeschema symbol.h:43-44,
SYM_MIRROR_X = TRANSFORM(1,0,0,-1) negates Y, and SYM_MIRROR_Y =
TRANSFORM(-1,0,0,1) negates X. Our code did the inverse: `mirror_x`
negated the X component and `mirror_y` negated the Y component.
Fix shape for `_rotate`: chose option (b) — leave `_rotate` as standard
math and negate the angle at the call site (`_rotate(lx, ly, -rotation)`).
This converts math-CCW to screen-CCW without disturbing
`TestRotatePoint`'s direct expectations of `_rotate`.
Final composition order in `pin_world_xy` matches eeschema's parser
(rotation set first into m_transform, then mirror composed via
`new = old * temp` so the mirror is applied first to the coordinate):
1. Y-flip: ly = -ly (lib Y-up → screen Y-down)
2. Mirror: if mirror_x: ly = -ly (negate screen-Y)
if mirror_y: lx = -lx (negate screen-X)
3. Rotate: _rotate(lx, ly, -rotation) (screen-CCW)
4. Translate: add (sym_x, sym_y)
Verified by hand for {rot=90, rot=270} × {none, mirror_x, mirror_y}
against the TRANSFORM matrices in transform.cpp:44 and symbol.h:43-44.
`PinLocator.get_pin_angle` mirrors the same composition in angle space.
For an angle, Y-flip and mirror_x both negate the angle; mirror_y maps
to (180 - angle). The screen-CCW rotation in `pin_world_xy` corresponds
to subtracting (not adding) the symbol rotation in standard atan2
convention — fixed accordingly. Geometry test
(`test_get_pin_angle.py::test_get_pin_angle_matches_geometric_expectation`)
derives expected angles from `pin_world_xy` itself, so it pins the two
together.
`tests/test_rotate_schematic_mirror.py::test_pin_positions_mirror_x_flips_x`
encoded the OLD inverted semantics and is updated/renamed to
`test_pin_positions_mirror_x_flips_y` with a pin that has non-zero Y so
the assertion is meaningful under the corrected semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PR #145 restored the Y-axis flip in WireDragger.pin_world_xy so pin
coordinates now match the schematic (Y-down) frame instead of the
library (Y-up) frame. PinLocator.get_pin_angle was the companion to
that transform but never received the matching fix: it was returning
the library-frame angle (with mirror handling but no Y-flip), so
angles came out 180° off along the Y axis.
This was masked before PR #145 because pin_world_xy was wrong in the
same direction — both functions skipped the Y-flip, so callers that
compared pin endpoints to angles saw a self-consistent picture. Once
pin_world_xy was corrected the inconsistency surfaced.
Apply the same lib→screen Y-flip (negate angle) after the mirror
handling and before the symbol-rotation add, matching pin_world_xy's
order: mirror in lib space → Y-flip → rotate → translate (no
translate for angles since angles are translation-invariant).
Fixes the 24 parametrized cases in
tests/test_get_pin_angle.py::test_get_pin_angle_matches_geometric_expectation
(pin × mirror × rotation matrix). The test derives its expected value
from pin_world_xy itself, making it the canonical geometric oracle.
test_pin_locator_y_flip and test_move_with_wire_preservation continue
to pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. add_schematic_net_label failed on schematics with no existing labels.
The clone-based path required a pre-existing label to copy from;
the documented "fallback to sexpdata" was a misleading log line —
the RuntimeError was caught and the call silently returned False.
Restore hand-built sexpdata construction (without the buggy
fields_autoplaced token, with orientation-aware justify).
2. get_pin_angle returned the wrong angle for every mirrored symbol
(off by exactly 180°, all rotations, both mirror axes). The
mirror_x and mirror_y formulas were swapped relative to the
pin_world_xy convention — pin_world_xy mirrors a position by
flipping its local axis component, so the matching angle
transform is (180 - θ) for mirror_x and -θ for mirror_y.
Add regression tests:
- test_add_label_empty_schematic.py — first label on empty schematic,
orientation-aware justify.
- test_get_pin_angle.py — full 24-case matrix
(4 rotations × 3 mirror states × 2 pins).
Previously get_pin_location and get_pin_angle read symbol state from a
kicad-skip cache that does not reflect (mirror x/y) tokens written by
rotate_schematic_component. Pin coordinates were always computed as if
the symbol was unmirrored.
Fix:
- Added _get_symbol_transform() which reads position, rotation, mirror_x,
mirror_y, and lib_id directly from the .kicad_sch file via sexpdata +
WireDragger.find_symbol (the authoritative source after a rotate/mirror)
- get_pin_location now delegates the full transform (mirror → rotate →
translate) to WireDragger.pin_world_xy, matching the logic used by
move_schematic_component and rotate_schematic_component
- get_pin_angle now applies mirror-induced angle reflection before adding
symbol rotation: mirror_x negates the angle, mirror_y reflects across 180°
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The symbol-to-schematic y-flip was applied twice in sequence (two identical
negation blocks with matching comments), cancelling out and leaving pin
Y-coordinates mirrored about the placement Y. For symmetric passives the
bug is invisible (pin 1 and pin 2 are electrically interchangeable); for
ICs with non-equivalent pins (power pins, opamp inputs, etc.) this causes
tools that go through PinLocator — connect_to_net, add_schematic_connection,
add_schematic_net_label with componentRef+pinNumber — to place connections
at the mirror-flipped pin. Verified against kicad-cli generate_netlist
ground truth: on a Device:R placed at (111.76, 83.82), pin 1 resolves to
y=80.01 (actual) vs y=87.63 (pre-fix).
This is a regression of PR #103, which originally fixed the y-negation;
the redundant second block was added subsequently.
Includes a regression test with both a straight Device:R and a rotated
Device:C to exercise the y-flip + rotation pipeline.
Adds robust multi-sheet (hierarchical) net connectivity for KiCad
schematics and switches the wire/label parsing to a direct sexpdata
pipeline that bypasses kicad-skip's collection iteration, which was
silently dropping wires, labels, and symbol instances on real-world
schematics.
python/commands/wire_connectivity.py
- New sexpdata helpers: _load_sexp, _parse_wires_sexp,
_parse_labels_sexp, _parse_symbol_instances_sexp,
_parse_hierarchical_labels_sexp, _discover_sub_sheets.
- _build_adjacency now detects T-junctions (endpoint landing on
another wire's interior segment) so adjacency captures connections
KiCad doesn't represent as separate wire segments.
- _find_connected_wires gains an interior-segment fallback so labels
placed mid-wire still seed BFS correctly.
- _parse_virtual_connections gathers label / global_label /
hierarchical_label and power-symbol pin positions, with a
kicad-skip fallback for unit tests that mock the schematic.
- _find_pins_on_net rebuilds pin positions from sexpdata symbol
instances (with mirror_x/mirror_y/rotation handling) and uses a
plus/minus 1 IU tolerance for floating-point edge cases.
- get_connections_for_net walks the top sheet plus every recursively
discovered sub-sheet, deduping pins across sheets.
python/commands/pin_locator.py
- lib_id matching now falls back to a bare-name + unit-suffix match
so instances like "stat-tis-custom:BAT_18650" resolve to
lib_symbols entries like "BAT_18650_3".
- Pin position math now y-negates lib_symbols coords, applies
mirror_x/mirror_y in local coords before rotation, and propagates
the same transform into get_pin_orientation so downstream callers
get a correct outward angle for mirrored symbols.
python/commands/connection_schematic.py
- generate_netlist now collects nets from both label and
global_label and routes them through get_connections_for_net so
netlists reflect cross-sheet connectivity instead of single-sheet
label-only nets.
python/kicad_interface.py
- list_schematic_nets aggregates net names across the top sheet and
all sub-sheets via the sexp helpers, then resolves connections
using get_connections_for_net.
- get_net_connections delegates to get_connections_for_net for
consistent multi-sheet results.
Three bugs fixed in the schematic component and pin locator pipeline:
1. component_schematic: remove redundant symbol.append() after clone()
kicad-skip's clone() already inserts the raw element into the schematic
tree. The subsequent NamedCollection.append() detects the reference as
already registered (from the elementRename triggered by setting
property.Reference.value) and renames it "R1_" with a trailing
underscore, causing all subsequent pin lookups to fail.
2. pin_locator: negate lib y coordinate before rotation
lib_symbols in .kicad_sch use library y-up convention; schematic
coordinates use y-down. get_pin_location now negates pin_rel_y before
applying rotation, matching KiCad's own transform order (same approach
as _transform_local_point in schematic_analysis.py).
3. pin_locator: add .rstrip("_") guard in all symbol reference lookups
Defensive guard against any residual cases where kicad-skip writes a
trailing underscore to the Reference property value.
Also fixes the self-test script to use template_with_symbols.kicad_sch
(which contains placed _TEMPLATE_* symbols) rather than the expanded
template (which only contains lib_symbols definitions and has no cloneable
instances).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three bugs fixed in the schematic component and pin locator pipeline:
1. component_schematic: remove redundant symbol.append() after clone()
kicad-skip's clone() already inserts the raw element into the schematic
tree. The subsequent NamedCollection.append() detects the reference as
already registered (from the elementRename triggered by setting
property.Reference.value) and renames it "R1_" with a trailing
underscore, causing all subsequent pin lookups to fail.
2. pin_locator: negate lib y coordinate before rotation
lib_symbols in .kicad_sch use library y-up convention; schematic
coordinates use y-down. get_pin_location now negates pin_rel_y before
applying rotation, matching KiCad's own transform order (same approach
as _transform_local_point in schematic_analysis.py).
3. pin_locator: add .rstrip("_") guard in all symbol reference lookups
Defensive guard against any residual cases where kicad-skip writes a
trailing underscore to the Reference property value.
Also fixes the self-test script to use template_with_symbols.kicad_sch
(which contains placed _TEMPLATE_* symbols) rather than the expanded
template (which only contains lib_symbols definitions and has no cloneable
instances).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mechanical application of the `.gitattributes` rules from the prior commit.
All 50 files differ only in line endings — verified by
`git diff --cached --ignore-all-space` being empty.
Before: main had 42 CRLF + 27 LF Python files plus mixed-ending in YAML,
templates, and shell scripts. After: every text file is LF (except the
Windows-native *.ps1, *.bat scripts which remain CRLF per gitattributes).
This eliminates the noisy-diff failure mode seen in PR #102, where a
small logic change produced a 918-line diff due to whole-file CRLF→LF
conversion.
Add type annotations to all previously untyped functions and remove 9
suppressed error codes (call-arg, assignment, return-value, operator,
has-type, dict-item, misc, list-item, annotation-unchecked) by fixing
the underlying type issues.
Add [[tool.mypy.overrides]] with ignore_missing_imports for KiCAD-specific
modules (pcbnew, sexpdata, skip, cairosvg, kipy, PIL) so the pre-commit
mypy hook passes in its isolated venv. Add types-requests and pytest to
additional_dependencies in .pre-commit-config.yaml.
Also fixes several real bugs uncovered by stricter checks: incorrect static
calls to instance methods in swig_backend, wrong return type on get_size,
missing value param in BoardAPI.place_component, variable shadowing in
kicad_process.py, unqualified LibraryManager reference in kicad_interface,
and missing top-level Path import.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add isort configuration (profile=black, line_length=100) to pyproject.toml,
add isort pre-commit hook, and auto-sort imports across all Python source files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add [tool.black] config to pyproject.toml and Black hook to
.pre-commit-config.yaml (rev 26.3.1), then auto-format all Python
source and test files with line-length=100, target-version=py310.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace /home/chris/... absolute paths with Path(__file__)-relative
equivalents in wire_manager.py and pin_locator.py so the test
scripts work on any machine.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- add_schematic_net_label: warn in description that coords must be exact pin endpoints; recommend connect_to_net instead
- connect_to_net: stub wire direction now follows pin angle (was hardcoded +X)
- pin_locator.py: add get_pin_angle() and _get_lib_id() helpers
- new tool: get_schematic_pin_locations(schematicPath, reference) → returns exact x/y of every pin endpoint, so Claude can place labels correctly
- connection_schematic.py: generate_netlist() now accepts schematic_path param,
threaded through to get_net_connections() so PinLocator is actually invoked
(previously only 1 connection per component was returned due to fallback break)
- kicad_interface.py: pass schematic_path to generate_netlist()
- pin_locator.py: add _schematic_cache to avoid loading Schematic() once per pin
(was causing timeout: O(nets x components x pins) Schematic() calls)
- server.ts: remove fragile PYTHONPATH?.includes('KiCad') condition,
always prefer KiCAD bundled Python on Windows when executable exists
- CHANGELOG.md: document fixes under v2.2.0-alpha
Phase 1 critical fixes addressing three high-priority issues:
Issue #36 - Windows IPC backend crash (os.getuid not available):
- Add platform detection to skip Unix-specific socket paths on Windows
- Use hasattr check before calling os.getuid()
- Windows now uses auto-detect fallback (named pipes)
- Maintains full Unix socket support on Linux/macOS
Issue #37 - Windows schematic file creation broken:
- Generate unique UUIDs instead of invalid all-zeros UUID
- Add explicit UTF-8 encoding for cross-platform compatibility
- Force Unix line endings (LF) to prevent Windows CRLF issues
- Update template file with valid placeholder UUID
- Fix hardcoded /tmp/ paths in wire_manager.py and pin_locator.py
Issue #35 - JLCPCB download limited to 100 parts:
- Change batch_size from 1000 to 100 to match tscircuit API limit
- Remove premature loop termination when batch < batch_size
- Add documentation explaining API limitation
- Expected result: Full ~2.5M part catalog download (40-60 minutes)
All changes maintain backward compatibility and include detailed comments.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Updates MCP handlers to use the new wiring infrastructure:
Handler Updates:
- _handle_add_schematic_wire: Uses WireManager.add_wire() with S-expression manipulation
- _handle_add_schematic_connection: Uses ConnectionManager with automatic pin discovery and routing options (direct, orthogonal_h, orthogonal_v)
- _handle_add_schematic_net_label: Uses WireManager.add_label() with support for label types and orientation
Features:
- Automatic pin location discovery with rotation support
- Professional wire routing (direct, orthogonal horizontal-first, orthogonal vertical-first)
- Net label placement with customizable types (label, global_label, hierarchical_label)
- Comprehensive error handling and logging
Testing:
- All MCP handlers tested and verified working
- Integration test: 100% passing (2 wires, 1 label created successfully)
- Verified with kicad-skip that wires and labels are correctly formed
Part of Issue #26 schematic wiring implementation (Phase 1)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>