app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Reference implementation of the KiCad bridge: multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.
← Commit history
0.9.350 notes and the model check tests
2 files changed
+61
RELEASE_NOTES.md+14@@ -10,6 +10,20 @@ unnamed. These entries were reconstructed after the fact from the work they contain, which is exactly the situation the mandatory-notes gate now prevents. Treat them as accurate but secondhand. +## 0.9.350 (2026-09-09 13:49 UTC)++kicad_model_check and model diagnostics on the 3D show verbs (#88)++## 0.9.350 (2026-09-09): 3D model diagnostics (#88)++- New read-only `kicad_model_check {library, footprintName}` or `{boardPath}`: per (model ...) reference it+ reports where KiCad would look (project dir, then the 3D search paths; never next to the .kicad_mod),+ whether the file exists, whether it is a STEP/VRML with geometry, hidden flags and implausible scale,+ with coded warnings and hints. Footprints that need no body (test points, fiducials, mounting holes,+ board-only parts) are exempt. `renderVerified` is always false: the viewer frame stays the render proof.+- `kicad_show_3d_chip` and `kicad_show_3d_board {filePath}` attach a `models` summary and a `_modelHint`+ when a body cannot be there, so a pads-only 3D view arrives with its reason.+ ## 0.9.349 (2026-09-09 13:17 UTC) kicad_status carries operation state for timeout recovery (#87)
tests/test_model_check.pyadded+47@@ -0,0 +1,47 @@+import sys, tempfile, unittest+from pathlib import Path+from unittest.mock import patch+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))+from handlers import model_check, install_footprint++STEP = b"ISO-10303-21;\nHEADER;ENDSEC;\nDATA;\n#1=MANIFOLD_SOLID_BREP('x',#2);\n#2=CLOSED_SHELL('',());\nENDSEC;\nEND-ISO-10303-21;\n"++def fp_text(model_clause):+ return '(footprint "X" (version 20240108) (generator "t") (layer "F.Cu") (attr smd) (pad "1" smd rect (at 0 0) (size 1 1) (layers "F.Cu"))' + model_clause + ')'++class ModelCheck(unittest.TestCase):+ def setUp(self):+ self.tmp = tempfile.TemporaryDirectory(); d = Path(self.tmp.name)+ self.pretty = d / "Adom.pretty"; self.pretty.mkdir()+ self.base = d / "kicad"; (self.base / "share/kicad/3dmodels/Adom.3dshapes").mkdir(parents=True)+ (self.base / "share/kicad/3dmodels/Adom.3dshapes/GOOD.step").write_bytes(STEP)+ (self.pretty / "BARE.step").write_bytes(STEP)+ (self.pretty / "EMPTY.step").write_bytes(b"ISO-10303-21;\nHEADER;ENDSEC;DATA;ENDSEC;END-ISO-10303-21;")+ self.info = {"version": "10.0", "base_dir": str(self.base), "user_dir": str(d)}+ self.p1 = patch.object(model_check, "_library_dir", return_value=str(self.pretty)); self.p1.start()+ self.p2 = patch.object(model_check, "_kicad_common", return_value={}); self.p2.start()+ def tearDown(self): self.p1.stop(); self.p2.stop(); self.tmp.cleanup()+ def write(self, name, clause): (self.pretty / f"{name}.kicad_mod").write_text(fp_text(clause))+ def test_good_stock_reference(self):+ self.write("A", '(model "${KICAD10_3DMODEL_DIR}/Adom.3dshapes/GOOD.step" (offset (xyz 0 0 0)) (scale (xyz 1 1 1)) (rotate (xyz 0 0 0)))')+ r = model_check.check_footprint(self.info, "Adom", "A"); self.assertTrue(r["ok"], r); self.assertEqual(r["models"][0]["format"], "step"); self.assertGreater(r["models"][0]["geometryHints"], 0)+ def test_bare_relative_next_to_footprint_is_not_found(self):+ self.write("B", '(model "BARE.step" (offset (xyz 0 1.39 0.18)) (scale (xyz 1 1 1)) (rotate (xyz 0 0 -90)))')+ r = model_check.check_footprint(self.info, "Adom", "B"); self.assertFalse(r["ok"]); self.assertEqual(r["warnings"][0]["code"], "not_found_relative"); self.assertIn("3DMODEL_DIR", r["warnings"][0]["hint"])+ def test_absolute_empty_geometry_and_hidden_and_scale(self):+ p = str(self.pretty / "EMPTY.step").replace("\\", "/")+ self.write("C", f'(model "{p}" (hide yes) (offset (xyz 0 0 0)) (scale (xyz 0 1 1)) (rotate (xyz 0 0 0)))')+ r = model_check.check_footprint(self.info, "Adom", "C"); codes = sorted(w["code"] for w in r["warnings"]); self.assertEqual(codes, ["empty_geometry", "hidden", "implausible_scale"])+ def test_no_model_and_exempt(self):+ self.write("D", ""); r = model_check.check_footprint(self.info, "Adom", "D"); self.assertEqual(r["warnings"][0]["code"], "no_model_reference")+ self.write("TestPoint_Pad", ""); r = model_check.check_footprint(self.info, "Adom", "TestPoint_Pad"); self.assertTrue(r["ok"]); self.assertIn("none expected", r["note"])+ def test_unresolved_variable(self):+ self.write("E", '(model "${NOPE}/x.step")'); r = model_check.check_footprint(self.info, "Adom", "E"); self.assertEqual(r["warnings"][0]["code"], "unresolved_variable")+ def test_board(self):+ d = Path(self.tmp.name); board = d / "b.kicad_pcb"+ board.write_text('(kicad_pcb (version 20240108) (generator "t") (footprint "Adom:A" (layer "F.Cu") (property "Reference" "U1") (model "${KICAD10_3DMODEL_DIR}/Adom.3dshapes/GOOD.step")) (footprint "Adom:B" (layer "F.Cu") (property "Reference" "U2") (model "BARE.step")) (footprint "TestPoint:TP" (layer "F.Cu") (property "Reference" "TP1")) (footprint "Adom:C" (layer "F.Cu") (property "Reference" "U3")))')+ r = model_check.check_board(self.info, str(board)); c = r["counts"]+ self.assertEqual((c["footprints"], c["withModel"], c["noModelExpected"], c["missingModel"], c["unresolvedModel"]), (4, 2, 1, 1, 1)); self.assertEqual([x["reference"] for x in r["footprintsWithWarnings"]], ["U2", "U3"])++if __name__ == "__main__":+ unittest.main()