Fix coordinate precision and per-schematic sidecar isolation

Reduce _rc() and transform_pin_to_schematic() rounding from 3 to 2
decimal places to match KiCad's 0.01mm coordinate quantum — prevents
union-find misses when wire endpoints and sexp-parsed pin positions
differ at the sub-quantum level.

Use schematic stem as subdirectory inside .mckicad/ so multi-sheet
analysis outputs (connectivity.json, etc.) don't collide.
This commit is contained in:
Ryan Malloy 2026-03-04 18:22:21 -07:00
parent b347679c67
commit 52ff054f43
4 changed files with 46 additions and 11 deletions

View File

@ -92,9 +92,11 @@ def _require_kicad_cli() -> tuple[str, None] | tuple[None, dict[str, Any]]:
def _sidecar_dir(schematic_path: str) -> str: def _sidecar_dir(schematic_path: str) -> str:
"""Return the .mckicad/ directory next to a schematic, creating it if needed.""" """Return the .mckicad/{stem}/ directory next to a schematic, creating it if needed."""
parent = os.path.dirname(os.path.abspath(schematic_path)) abs_sch = os.path.abspath(schematic_path)
sidecar = os.path.join(parent, ".mckicad") parent = os.path.dirname(abs_sch)
stem = os.path.splitext(os.path.basename(abs_sch))[0]
sidecar = os.path.join(parent, ".mckicad", stem)
os.makedirs(sidecar, exist_ok=True) os.makedirs(sidecar, exist_ok=True)
return sidecar return sidecar
@ -150,7 +152,7 @@ def _build_connectivity(
def _rc(x: float, y: float) -> tuple[float, float]: def _rc(x: float, y: float) -> tuple[float, float]:
"""Round coordinates for stable floating-point comparison.""" """Round coordinates for stable floating-point comparison."""
return (round(x, 3), round(y, 3)) return (round(x, 2), round(y, 2))
def _coord_from_point(pt: Any) -> tuple[float, float]: def _coord_from_point(pt: Any) -> tuple[float, float]:
"""Extract (x, y) from a Point-like object or sequence.""" """Extract (x, y) from a Point-like object or sequence."""

View File

@ -18,7 +18,12 @@ def write_detail_file(schematic_path: str | None, filename: str, data: Any) -> s
"""Write large result data to a .mckicad/ sidecar directory. """Write large result data to a .mckicad/ sidecar directory.
When ``schematic_path`` is provided, the sidecar directory is created When ``schematic_path`` is provided, the sidecar directory is created
next to the schematic file. When ``None``, falls back to CWD. next to the schematic file, inside a subdirectory named after the
schematic's stem (e.g. ``.mckicad/power/connectivity.json``). This
prevents multi-sheet designs from overwriting each other's output.
When ``None``, falls back to a flat ``.mckicad/`` in CWD (used for
search results that have no schematic context).
Args: Args:
schematic_path: Path to a .kicad_sch file (sidecar dir created next to it), schematic_path: Path to a .kicad_sch file (sidecar dir created next to it),
@ -29,8 +34,13 @@ def write_detail_file(schematic_path: str | None, filename: str, data: Any) -> s
Returns: Returns:
Absolute path to the written file. Absolute path to the written file.
""" """
parent_dir = os.path.dirname(os.path.abspath(schematic_path)) if schematic_path else os.getcwd() if schematic_path:
sidecar_dir = os.path.join(parent_dir, ".mckicad") abs_sch = os.path.abspath(schematic_path)
parent_dir = os.path.dirname(abs_sch)
stem = os.path.splitext(os.path.basename(abs_sch))[0]
sidecar_dir = os.path.join(parent_dir, ".mckicad", stem)
else:
sidecar_dir = os.path.join(os.getcwd(), ".mckicad")
os.makedirs(sidecar_dir, exist_ok=True) os.makedirs(sidecar_dir, exist_ok=True)
out_path = os.path.join(sidecar_dir, filename) out_path = os.path.join(sidecar_dir, filename)

View File

@ -162,7 +162,7 @@ def transform_pin_to_schematic(
ry = px * sin_r + py * cos_r ry = px * sin_r + py * cos_r
# Apply position offset # Apply position offset
return (round(comp_x + rx, 3), round(comp_y + ry, 3)) return (round(comp_x + rx, 2), round(comp_y + ry, 2))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@ -3,6 +3,7 @@
import os import os
import pytest import pytest
from tests.conftest import requires_sch_api from tests.conftest import requires_sch_api
@ -120,7 +121,7 @@ def test_get_schematic_hierarchy(populated_schematic):
@pytest.mark.unit @pytest.mark.unit
def test_file_output_infrastructure(tmp_output_dir): def test_file_output_infrastructure(tmp_output_dir):
"""write_detail_file should create .mckicad sidecar directory and file.""" """write_detail_file should create .mckicad/{stem}/ sidecar directory and file."""
from mckicad.utils.file_utils import write_detail_file from mckicad.utils.file_utils import write_detail_file
fake_sch = os.path.join(tmp_output_dir, "test.kicad_sch") fake_sch = os.path.join(tmp_output_dir, "test.kicad_sch")
@ -132,12 +133,12 @@ def test_file_output_infrastructure(tmp_output_dir):
assert os.path.isfile(path) assert os.path.isfile(path)
assert ".mckicad" in path assert ".mckicad" in path
assert path.endswith("test_output.json") assert os.path.join(".mckicad", "test", "test_output.json") in path
@pytest.mark.unit @pytest.mark.unit
def test_file_output_cwd_fallback(tmp_output_dir, monkeypatch): def test_file_output_cwd_fallback(tmp_output_dir, monkeypatch):
"""write_detail_file with None path should use CWD.""" """write_detail_file with None path should use flat CWD/.mckicad/."""
from mckicad.utils.file_utils import write_detail_file from mckicad.utils.file_utils import write_detail_file
monkeypatch.chdir(tmp_output_dir) monkeypatch.chdir(tmp_output_dir)
@ -145,3 +146,25 @@ def test_file_output_cwd_fallback(tmp_output_dir, monkeypatch):
assert os.path.isfile(path) assert os.path.isfile(path)
assert ".mckicad" in path assert ".mckicad" in path
# None path stays flat — no stem subdirectory
assert path.endswith(os.path.join(".mckicad", "test_cwd.json"))
@pytest.mark.unit
def test_sidecar_per_schematic_isolation(tmp_output_dir):
"""Detail files for different schematics land in separate subdirectories."""
from mckicad.utils.file_utils import write_detail_file
sch_a = os.path.join(tmp_output_dir, "power.kicad_sch")
sch_b = os.path.join(tmp_output_dir, "esp32_p4_core.kicad_sch")
open(sch_a, "w").close()
open(sch_b, "w").close()
path_a = write_detail_file(sch_a, "connectivity.json", {"nets": {}})
path_b = write_detail_file(sch_b, "connectivity.json", {"nets": {}})
assert os.path.isfile(path_a)
assert os.path.isfile(path_b)
assert path_a != path_b
assert os.path.join(".mckicad", "power", "connectivity.json") in path_a
assert os.path.join(".mckicad", "esp32_p4_core", "connectivity.json") in path_b