From 52ff054f43c272a82aceb45afb2de3305544a318 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Wed, 4 Mar 2026 18:22:21 -0700 Subject: [PATCH] Fix coordinate precision and per-schematic sidecar isolation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/mckicad/tools/schematic_analysis.py | 10 +++++---- src/mckicad/utils/file_utils.py | 16 +++++++++++--- src/mckicad/utils/sexp_parser.py | 2 +- tests/test_schematic.py | 29 ++++++++++++++++++++++--- 4 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/mckicad/tools/schematic_analysis.py b/src/mckicad/tools/schematic_analysis.py index 0e621c9..b69cfb7 100644 --- a/src/mckicad/tools/schematic_analysis.py +++ b/src/mckicad/tools/schematic_analysis.py @@ -92,9 +92,11 @@ def _require_kicad_cli() -> tuple[str, None] | tuple[None, dict[str, Any]]: def _sidecar_dir(schematic_path: str) -> str: - """Return the .mckicad/ directory next to a schematic, creating it if needed.""" - parent = os.path.dirname(os.path.abspath(schematic_path)) - sidecar = os.path.join(parent, ".mckicad") + """Return the .mckicad/{stem}/ directory next to a schematic, creating it if needed.""" + abs_sch = os.path.abspath(schematic_path) + 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) return sidecar @@ -150,7 +152,7 @@ def _build_connectivity( def _rc(x: float, y: float) -> tuple[float, float]: """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]: """Extract (x, y) from a Point-like object or sequence.""" diff --git a/src/mckicad/utils/file_utils.py b/src/mckicad/utils/file_utils.py index 8b403ec..616fe7c 100644 --- a/src/mckicad/utils/file_utils.py +++ b/src/mckicad/utils/file_utils.py @@ -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. 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: 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: Absolute path to the written file. """ - parent_dir = os.path.dirname(os.path.abspath(schematic_path)) if schematic_path else os.getcwd() - sidecar_dir = os.path.join(parent_dir, ".mckicad") + if schematic_path: + 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) out_path = os.path.join(sidecar_dir, filename) diff --git a/src/mckicad/utils/sexp_parser.py b/src/mckicad/utils/sexp_parser.py index 5307e3a..0d7fd1e 100644 --- a/src/mckicad/utils/sexp_parser.py +++ b/src/mckicad/utils/sexp_parser.py @@ -162,7 +162,7 @@ def transform_pin_to_schematic( ry = px * sin_r + py * cos_r # 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)) # --------------------------------------------------------------------------- diff --git a/tests/test_schematic.py b/tests/test_schematic.py index f34770f..b14274c 100644 --- a/tests/test_schematic.py +++ b/tests/test_schematic.py @@ -3,6 +3,7 @@ import os import pytest + from tests.conftest import requires_sch_api @@ -120,7 +121,7 @@ def test_get_schematic_hierarchy(populated_schematic): @pytest.mark.unit 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 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 ".mckicad" in path - assert path.endswith("test_output.json") + assert os.path.join(".mckicad", "test", "test_output.json") in path @pytest.mark.unit 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 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 ".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