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>
This commit is contained in:
@@ -38,7 +38,7 @@ class BoardOutlineCommands:
|
|||||||
height = inner.get("height")
|
height = inner.get("height")
|
||||||
radius = inner.get("radius")
|
radius = inner.get("radius")
|
||||||
# Accept both "cornerRadius" and "radius" regardless of shape name.
|
# Accept both "cornerRadius" and "radius" regardless of shape name.
|
||||||
# The AI often sends shape="rectangle" with radius=2.5 — we treat that as rounded_rectangle.
|
# The AI often sends shape=”rectangle” with radius=2.5 — we treat that as rounded_rectangle.
|
||||||
corner_radius = inner.get("cornerRadius", inner.get("radius", 0))
|
corner_radius = inner.get("cornerRadius", inner.get("radius", 0))
|
||||||
if shape == "rectangle" and corner_radius > 0:
|
if shape == "rectangle" and corner_radius > 0:
|
||||||
shape = "rounded_rectangle"
|
shape = "rounded_rectangle"
|
||||||
@@ -74,7 +74,9 @@ class BoardOutlineCommands:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Convert to internal units (nanometers)
|
# Convert to internal units (nanometers)
|
||||||
scale = 1000000 if unit == "mm" else 25400000 # mm or inch to nm
|
scale = (
|
||||||
|
1000000 if unit == "mm" else (25400 if unit == "mil" else 25400000)
|
||||||
|
) # mm, mil, or inch to nm
|
||||||
|
|
||||||
# Create drawing for edge cuts
|
# Create drawing for edge cuts
|
||||||
edge_layer = self.board.GetLayerID("Edge.Cuts")
|
edge_layer = self.board.GetLayerID("Edge.Cuts")
|
||||||
@@ -226,7 +228,11 @@ class BoardOutlineCommands:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Convert to internal units (nanometers)
|
# Convert to internal units (nanometers)
|
||||||
scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000 # mm or inch to nm
|
scale = (
|
||||||
|
1000000
|
||||||
|
if position.get("unit", "mm") == "mm"
|
||||||
|
else (25400 if position.get("unit", "mm") == "mil" else 25400000)
|
||||||
|
) # mm, mil, or inch to nm
|
||||||
x_nm = int(position["x"] * scale)
|
x_nm = int(position["x"] * scale)
|
||||||
y_nm = int(position["y"] * scale)
|
y_nm = int(position["y"] * scale)
|
||||||
diameter_nm = int(diameter * scale)
|
diameter_nm = int(diameter * scale)
|
||||||
@@ -335,7 +341,11 @@ class BoardOutlineCommands:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Convert to internal units (nanometers)
|
# Convert to internal units (nanometers)
|
||||||
scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000 # mm or inch to nm
|
scale = (
|
||||||
|
1000000
|
||||||
|
if position.get("unit", "mm") == "mm"
|
||||||
|
else (25400 if position.get("unit", "mm") == "mil" else 25400000)
|
||||||
|
) # mm, mil, or inch to nm
|
||||||
x_nm = int(position["x"] * scale)
|
x_nm = int(position["x"] * scale)
|
||||||
y_nm = int(position["y"] * scale)
|
y_nm = int(position["y"] * scale)
|
||||||
size_nm = int(size * scale)
|
size_nm = int(size * scale)
|
||||||
|
|||||||
@@ -204,7 +204,9 @@ class BoardViewCommands:
|
|||||||
|
|
||||||
# Get unit preference (default to mm)
|
# Get unit preference (default to mm)
|
||||||
unit = params.get("unit", "mm")
|
unit = params.get("unit", "mm")
|
||||||
scale = 1000000 if unit == "mm" else 25400000 # nm to mm or inch
|
scale = (
|
||||||
|
1000000 if unit == "mm" else (25400 if unit == "mil" else 25400000)
|
||||||
|
) # mm, mil, or inch to nm
|
||||||
|
|
||||||
# Get board bounding box
|
# Get board bounding box
|
||||||
board_box = self.board.GetBoardEdgesBoundingBox()
|
board_box = self.board.GetBoardEdgesBoundingBox()
|
||||||
|
|||||||
@@ -96,7 +96,11 @@ class ComponentCommands:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Set position
|
# Set position
|
||||||
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
|
scale = (
|
||||||
|
1000000
|
||||||
|
if position["unit"] == "mm"
|
||||||
|
else (25400 if position["unit"] == "mil" else 25400000)
|
||||||
|
) # mm, mil, or inch to nm
|
||||||
x_nm = int(position["x"] * scale)
|
x_nm = int(position["x"] * scale)
|
||||||
y_nm = int(position["y"] * scale)
|
y_nm = int(position["y"] * scale)
|
||||||
module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
||||||
@@ -196,7 +200,11 @@ class ComponentCommands:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Set new position
|
# Set new position
|
||||||
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
|
scale = (
|
||||||
|
1000000
|
||||||
|
if position["unit"] == "mm"
|
||||||
|
else (25400 if position["unit"] == "mil" else 25400000)
|
||||||
|
) # mm, mil, or inch to nm
|
||||||
x_nm = int(position["x"] * scale)
|
x_nm = int(position["x"] * scale)
|
||||||
y_nm = int(position["y"] * scale)
|
y_nm = int(position["y"] * scale)
|
||||||
module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
||||||
@@ -987,7 +995,11 @@ class ComponentCommands:
|
|||||||
|
|
||||||
# Set position if provided, otherwise use offset from original
|
# Set position if provided, otherwise use offset from original
|
||||||
if position:
|
if position:
|
||||||
scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000
|
scale = (
|
||||||
|
1000000
|
||||||
|
if position.get("unit", "mm") == "mm"
|
||||||
|
else (25400 if position.get("unit", "mm") == "mil" else 25400000)
|
||||||
|
) # mm, mil, or inch to nm
|
||||||
x_nm = int(position["x"] * scale)
|
x_nm = int(position["x"] * scale)
|
||||||
y_nm = int(position["y"] * scale)
|
y_nm = int(position["y"] * scale)
|
||||||
new_module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
new_module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
||||||
@@ -1048,7 +1060,9 @@ class ComponentCommands:
|
|||||||
|
|
||||||
# Convert spacing to nm
|
# Convert spacing to nm
|
||||||
unit = start_position.get("unit", "mm")
|
unit = start_position.get("unit", "mm")
|
||||||
scale = 1000000 if unit == "mm" else 25400000 # mm or inch to nm
|
scale = (
|
||||||
|
1000000 if unit == "mm" else (25400 if unit == "mil" else 25400000)
|
||||||
|
) # mm, mil, or inch to nm
|
||||||
spacing_x_nm = int(spacing_x * scale)
|
spacing_x_nm = int(spacing_x * scale)
|
||||||
spacing_y_nm = int(spacing_y * scale)
|
spacing_y_nm = int(spacing_y * scale)
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class PinLocator:
|
|||||||
# legitimate same-length duplicates (e.g., per-unit
|
# legitimate same-length duplicates (e.g., per-unit
|
||||||
# repetitions in multi-unit symbols) retain stable ordering.
|
# repetitions in multi-unit symbols) retain stable ordering.
|
||||||
if pin_data["number"]:
|
if pin_data["number"]:
|
||||||
existing = pins.get(pin_data["number"])
|
existing = pins.get(str(pin_data["number"]))
|
||||||
if existing is None or pin_data["length"] > existing["length"]:
|
if existing is None or pin_data["length"] > existing["length"]:
|
||||||
pins[pin_data["number"]] = pin_data
|
pins[pin_data["number"]] = pin_data
|
||||||
|
|
||||||
|
|||||||
@@ -410,7 +410,11 @@ class RoutingCommands:
|
|||||||
"success": True,
|
"success": True,
|
||||||
"message": "Added arc trace",
|
"message": "Added arc trace",
|
||||||
"arc": {
|
"arc": {
|
||||||
"start": {"x": start_point.x / 1000000, "y": start_point.y / 1000000, "unit": "mm"},
|
"start": {
|
||||||
|
"x": start_point.x / 1000000,
|
||||||
|
"y": start_point.y / 1000000,
|
||||||
|
"unit": "mm",
|
||||||
|
},
|
||||||
"mid": {"x": mid_point.x / 1000000, "y": mid_point.y / 1000000, "unit": "mm"},
|
"mid": {"x": mid_point.x / 1000000, "y": mid_point.y / 1000000, "unit": "mm"},
|
||||||
"end": {"x": end_point.x / 1000000, "y": end_point.y / 1000000, "unit": "mm"},
|
"end": {"x": end_point.x / 1000000, "y": end_point.y / 1000000, "unit": "mm"},
|
||||||
"layer": layer,
|
"layer": layer,
|
||||||
@@ -454,7 +458,11 @@ class RoutingCommands:
|
|||||||
via = pcbnew.PCB_VIA(self.board)
|
via = pcbnew.PCB_VIA(self.board)
|
||||||
|
|
||||||
# Set position
|
# Set position
|
||||||
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
|
scale = (
|
||||||
|
1000000
|
||||||
|
if position["unit"] == "mm"
|
||||||
|
else (25400 if position["unit"] == "mil" else 25400000)
|
||||||
|
) # mm, mil, or inch to nm
|
||||||
x_nm = int(position["x"] * scale)
|
x_nm = int(position["x"] * scale)
|
||||||
y_nm = int(position["y"] * scale)
|
y_nm = int(position["y"] * scale)
|
||||||
via.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
via.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
||||||
@@ -596,7 +604,11 @@ class RoutingCommands:
|
|||||||
|
|
||||||
# Find track by position
|
# Find track by position
|
||||||
if position:
|
if position:
|
||||||
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
|
scale = (
|
||||||
|
1000000
|
||||||
|
if position["unit"] == "mm"
|
||||||
|
else (25400 if position["unit"] == "mil" else 25400000)
|
||||||
|
) # mm, mil, or inch to nm
|
||||||
x_nm = int(position["x"] * scale)
|
x_nm = int(position["x"] * scale)
|
||||||
y_nm = int(position["y"] * scale)
|
y_nm = int(position["y"] * scale)
|
||||||
point = pcbnew.VECTOR2I(x_nm, y_nm)
|
point = pcbnew.VECTOR2I(x_nm, y_nm)
|
||||||
@@ -713,7 +725,11 @@ class RoutingCommands:
|
|||||||
# Filter by bounding box
|
# Filter by bounding box
|
||||||
if bbox:
|
if bbox:
|
||||||
bbox_unit = bbox.get("unit", "mm")
|
bbox_unit = bbox.get("unit", "mm")
|
||||||
bbox_scale = scale if bbox_unit == "mm" else 25400000
|
bbox_scale = (
|
||||||
|
scale
|
||||||
|
if bbox_unit == "mm"
|
||||||
|
else (25400 if bbox_unit == "mil" else 25400000)
|
||||||
|
)
|
||||||
x1 = int(bbox.get("x1", 0) * bbox_scale)
|
x1 = int(bbox.get("x1", 0) * bbox_scale)
|
||||||
y1 = int(bbox.get("y1", 0) * bbox_scale)
|
y1 = int(bbox.get("y1", 0) * bbox_scale)
|
||||||
x2 = int(bbox.get("x2", 0) * bbox_scale)
|
x2 = int(bbox.get("x2", 0) * bbox_scale)
|
||||||
@@ -837,7 +853,11 @@ class RoutingCommands:
|
|||||||
layer_names = []
|
layer_names = []
|
||||||
try:
|
try:
|
||||||
layer_set = zone.GetLayerSet()
|
layer_set = zone.GetLayerSet()
|
||||||
seq = layer_set.CuStack() if hasattr(layer_set, "CuStack") else layer_set.Seq()
|
seq = (
|
||||||
|
layer_set.CuStack()
|
||||||
|
if hasattr(layer_set, "CuStack")
|
||||||
|
else layer_set.Seq()
|
||||||
|
)
|
||||||
for lid in seq:
|
for lid in seq:
|
||||||
layer_names.append(self.board.GetLayerName(lid))
|
layer_names.append(self.board.GetLayerName(lid))
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -862,7 +882,11 @@ class RoutingCommands:
|
|||||||
"net": z_net,
|
"net": z_net,
|
||||||
"netCode": zone.GetNetCode(),
|
"netCode": zone.GetNetCode(),
|
||||||
"layers": layer_names,
|
"layers": layer_names,
|
||||||
"priority": zone.GetAssignedPriority() if hasattr(zone, "GetAssignedPriority") else 0,
|
"priority": (
|
||||||
|
zone.GetAssignedPriority()
|
||||||
|
if hasattr(zone, "GetAssignedPriority")
|
||||||
|
else 0
|
||||||
|
),
|
||||||
"isFilled": bool(zone.IsFilled()),
|
"isFilled": bool(zone.IsFilled()),
|
||||||
"minThickness": zone.GetMinThickness() / scale,
|
"minThickness": zone.GetMinThickness() / scale,
|
||||||
"boundingBox": {
|
"boundingBox": {
|
||||||
@@ -940,7 +964,9 @@ class RoutingCommands:
|
|||||||
break
|
break
|
||||||
elif position:
|
elif position:
|
||||||
pos_unit = position.get("unit", "mm")
|
pos_unit = position.get("unit", "mm")
|
||||||
pos_scale = scale if pos_unit == "mm" else 25400000
|
pos_scale = (
|
||||||
|
scale if pos_unit == "mm" else (25400 if pos_unit == "mil" else 25400000)
|
||||||
|
)
|
||||||
x_nm = int(position["x"] * pos_scale)
|
x_nm = int(position["x"] * pos_scale)
|
||||||
y_nm = int(position["y"] * pos_scale)
|
y_nm = int(position["y"] * pos_scale)
|
||||||
point = pcbnew.VECTOR2I(x_nm, y_nm)
|
point = pcbnew.VECTOR2I(x_nm, y_nm)
|
||||||
@@ -1124,7 +1150,7 @@ class RoutingCommands:
|
|||||||
if track.GetNetname() not in source_nets:
|
if track.GetNetname() not in source_nets:
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
# Fallback: geometric filter – trace start OR end inside source bbox
|
# Fallback: geometric filter — trace start OR end inside source bbox
|
||||||
if is_via:
|
if is_via:
|
||||||
pos = track.GetPosition()
|
pos = track.GetPosition()
|
||||||
if not point_in_bbox(pos.x, pos.y):
|
if not point_in_bbox(pos.x, pos.y):
|
||||||
@@ -1422,7 +1448,11 @@ class RoutingCommands:
|
|||||||
|
|
||||||
# Add points to outline
|
# Add points to outline
|
||||||
for point in points:
|
for point in points:
|
||||||
scale = 1000000 if point.get("unit", "mm") == "mm" else 25400000
|
scale = (
|
||||||
|
1000000
|
||||||
|
if point.get("unit", "mm") == "mm"
|
||||||
|
else (25400 if point.get("unit", "mm") == "mil" else 25400000)
|
||||||
|
)
|
||||||
x_nm = int(point["x"] * scale)
|
x_nm = int(point["x"] * scale)
|
||||||
y_nm = int(point["y"] * scale)
|
y_nm = int(point["y"] * scale)
|
||||||
outline.Append(pcbnew.VECTOR2I(x_nm, y_nm)) # Add point to outline
|
outline.Append(pcbnew.VECTOR2I(x_nm, y_nm)) # Add point to outline
|
||||||
@@ -1605,7 +1635,11 @@ class RoutingCommands:
|
|||||||
def _get_point(self, point_spec: Dict[str, Any]) -> pcbnew.VECTOR2I:
|
def _get_point(self, point_spec: Dict[str, Any]) -> pcbnew.VECTOR2I:
|
||||||
"""Convert point specification to KiCAD point"""
|
"""Convert point specification to KiCAD point"""
|
||||||
if "x" in point_spec and "y" in point_spec:
|
if "x" in point_spec and "y" in point_spec:
|
||||||
scale = 1000000 if point_spec.get("unit", "mm") == "mm" else 25400000
|
scale = (
|
||||||
|
1000000
|
||||||
|
if point_spec.get("unit", "mm") == "mm"
|
||||||
|
else (25400 if point_spec.get("unit", "mm") == "mil" else 25400000)
|
||||||
|
)
|
||||||
x_nm = int(point_spec["x"] * scale)
|
x_nm = int(point_spec["x"] * scale)
|
||||||
y_nm = int(point_spec["y"] * scale)
|
y_nm = int(point_spec["y"] * scale)
|
||||||
return pcbnew.VECTOR2I(x_nm, y_nm)
|
return pcbnew.VECTOR2I(x_nm, y_nm)
|
||||||
@@ -1721,9 +1755,8 @@ class RoutingCommands:
|
|||||||
return self._do_add_gnd_stitching(params)
|
return self._do_add_gnd_stitching(params)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
logger.error(
|
|
||||||
f"add_gnd_stitching_vias failed: {e}\n{traceback.format_exc()}"
|
logger.error(f"add_gnd_stitching_vias failed: {e}\n{traceback.format_exc()}")
|
||||||
)
|
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "add_gnd_stitching_vias failed",
|
"message": "add_gnd_stitching_vias failed",
|
||||||
@@ -1791,8 +1824,7 @@ class RoutingCommands:
|
|||||||
"success": False,
|
"success": False,
|
||||||
"message": "No GND net detected",
|
"message": "No GND net detected",
|
||||||
"errorDetails": (
|
"errorDetails": (
|
||||||
"Pass gndNet explicitly. Auto-detect tries "
|
"Pass gndNet explicitly. Auto-detect tries " "GND / GROUND / VSS / /GND."
|
||||||
"GND / GROUND / VSS / /GND."
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
gnd_net_code = gnd_net.GetNetCode()
|
gnd_net_code = gnd_net.GetNetCode()
|
||||||
@@ -1866,10 +1898,7 @@ class RoutingCommands:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# --- In-zone test (cached per call) ---
|
# --- In-zone test (cached per call) ---
|
||||||
gnd_zones = [
|
gnd_zones = [z for z in self.board.Zones() if z.GetNetCode() == gnd_net_code]
|
||||||
z for z in self.board.Zones()
|
|
||||||
if z.GetNetCode() == gnd_net_code
|
|
||||||
]
|
|
||||||
|
|
||||||
def in_any_gnd_zone(x_nm: int, y_nm: int) -> bool:
|
def in_any_gnd_zone(x_nm: int, y_nm: int) -> bool:
|
||||||
pt = pcbnew.VECTOR2I(x_nm, y_nm)
|
pt = pcbnew.VECTOR2I(x_nm, y_nm)
|
||||||
@@ -1880,8 +1909,10 @@ class RoutingCommands:
|
|||||||
except Exception:
|
except Exception:
|
||||||
# API variant: take any zone in whose bbox we sit
|
# API variant: take any zone in whose bbox we sit
|
||||||
bb = z.GetBoundingBox()
|
bb = z.GetBoundingBox()
|
||||||
if (bb.GetLeft() <= x_nm <= bb.GetRight()
|
if (
|
||||||
and bb.GetTop() <= y_nm <= bb.GetBottom()):
|
bb.GetLeft() <= x_nm <= bb.GetRight()
|
||||||
|
and bb.GetTop() <= y_nm <= bb.GetBottom()
|
||||||
|
):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -1929,9 +1960,7 @@ class RoutingCommands:
|
|||||||
candidates: List[tuple] = []
|
candidates: List[tuple] = []
|
||||||
if "around_refs" in strategies:
|
if "around_refs" in strategies:
|
||||||
if not densify_refs:
|
if not densify_refs:
|
||||||
logger.warning(
|
logger.warning("around_refs strategy requested but densifyRefs is empty")
|
||||||
"around_refs strategy requested but densifyRefs is empty"
|
|
||||||
)
|
|
||||||
fps_by_ref = {fp.GetReference(): fp for fp in self.board.GetFootprints()}
|
fps_by_ref = {fp.GetReference(): fp for fp in self.board.GetFootprints()}
|
||||||
for ref in densify_refs:
|
for ref in densify_refs:
|
||||||
fp = fps_by_ref.get(ref)
|
fp = fps_by_ref.get(ref)
|
||||||
@@ -1969,11 +1998,13 @@ class RoutingCommands:
|
|||||||
skipped_by_collision += 1
|
skipped_by_collision += 1
|
||||||
continue
|
continue
|
||||||
placed_via_centres.append((cx, cy))
|
placed_via_centres.append((cx, cy))
|
||||||
placed_meta.append({
|
placed_meta.append(
|
||||||
"x": round(cx / scale, 3),
|
{
|
||||||
"y": round(cy / scale, 3),
|
"x": round(cx / scale, 3),
|
||||||
"unit": "mm",
|
"y": round(cy / scale, 3),
|
||||||
})
|
"unit": "mm",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# --- Write to board ---
|
# --- Write to board ---
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
@@ -2011,9 +2042,8 @@ class RoutingCommands:
|
|||||||
# Module-level geometry helper (used by add_gnd_stitching_vias collision check)
|
# Module-level geometry helper (used by add_gnd_stitching_vias collision check)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _point_to_segment_distance_nm(
|
|
||||||
px: int, py: int, x1: int, y1: int, x2: int, y2: int
|
def _point_to_segment_distance_nm(px: int, py: int, x1: int, y1: int, x2: int, y2: int) -> float:
|
||||||
) -> float:
|
|
||||||
"""Shortest distance (nm) from point (px,py) to segment (x1,y1)-(x2,y2).
|
"""Shortest distance (nm) from point (px,py) to segment (x1,y1)-(x2,y2).
|
||||||
|
|
||||||
Pure integer-friendly variant of the standard projection formula;
|
Pure integer-friendly variant of the standard projection formula;
|
||||||
@@ -2023,8 +2053,8 @@ def _point_to_segment_distance_nm(
|
|||||||
dx = x2 - x1
|
dx = x2 - x1
|
||||||
dy = y2 - y1
|
dy = y2 - y1
|
||||||
if dx == 0 and dy == 0:
|
if dx == 0 and dy == 0:
|
||||||
ex = px - x1
|
ex: float = px - x1
|
||||||
ey = py - y1
|
ey: float = py - y1
|
||||||
return (ex * ex + ey * ey) ** 0.5
|
return (ex * ex + ey * ey) ** 0.5
|
||||||
denom = dx * dx + dy * dy
|
denom = dx * dx + dy * dy
|
||||||
t = ((px - x1) * dx + (py - y1) * dy) / denom
|
t = ((px - x1) * dx + (py - y1) * dy) / denom
|
||||||
|
|||||||
@@ -5291,10 +5291,13 @@ print("ok")
|
|||||||
layer = params.get("layer", "F.Cu")
|
layer = params.get("layer", "F.Cu")
|
||||||
value = params.get("value", "")
|
value = params.get("value", "")
|
||||||
|
|
||||||
# Convert inches to mm since ipc_backend expects mm
|
# Convert to mm since ipc_backend expects mm
|
||||||
if unit == "inch":
|
if unit == "inch":
|
||||||
x = x * 25.4
|
x = x * 25.4
|
||||||
y = y * 25.4
|
y = y * 25.4
|
||||||
|
elif unit == "mil":
|
||||||
|
x = x * 0.0254
|
||||||
|
y = y * 0.0254
|
||||||
|
|
||||||
success = self.ipc_board_api.place_component(
|
success = self.ipc_board_api.place_component(
|
||||||
reference=reference,
|
reference=reference,
|
||||||
@@ -5335,10 +5338,13 @@ print("ok")
|
|||||||
unit = position.get("unit", "mm") if isinstance(position, dict) else "mm"
|
unit = position.get("unit", "mm") if isinstance(position, dict) else "mm"
|
||||||
rotation = params.get("rotation")
|
rotation = params.get("rotation")
|
||||||
|
|
||||||
# Convert inches to mm since ipc_backend.move_component expects mm
|
# Convert to mm since ipc_backend.move_component expects mm
|
||||||
if unit == "inch":
|
if unit == "inch":
|
||||||
x = x * 25.4
|
x = x * 25.4
|
||||||
y = y * 25.4
|
y = y * 25.4
|
||||||
|
elif unit == "mil":
|
||||||
|
x = x * 0.0254
|
||||||
|
y = y * 0.0254
|
||||||
|
|
||||||
success = self.ipc_board_api.move_component(
|
success = self.ipc_board_api.move_component(
|
||||||
reference=reference, x=x, y=y, rotation=rotation
|
reference=reference, x=x, y=y, rotation=rotation
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ BOARD_TOOLS = [
|
|||||||
"properties": {
|
"properties": {
|
||||||
"unit": {
|
"unit": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["mm", "inch"],
|
"enum": ["mm", "mil", "inch"],
|
||||||
"description": "Unit for returned coordinates (default: mm)",
|
"description": "Unit for returned coordinates (default: mm)",
|
||||||
"default": "mm",
|
"default": "mm",
|
||||||
}
|
}
|
||||||
@@ -934,7 +934,7 @@ ROUTING_TOOLS = [
|
|||||||
"y": {"type": "number", "description": "Y coordinate"},
|
"y": {"type": "number", "description": "Y coordinate"},
|
||||||
"unit": {
|
"unit": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["mm", "inch"],
|
"enum": ["mm", "mil", "inch"],
|
||||||
"default": "mm",
|
"default": "mm",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -981,7 +981,7 @@ ROUTING_TOOLS = [
|
|||||||
"y2": {"type": "number", "description": "Bottom Y coordinate"},
|
"y2": {"type": "number", "description": "Bottom Y coordinate"},
|
||||||
"unit": {
|
"unit": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["mm", "inch"],
|
"enum": ["mm", "mil", "inch"],
|
||||||
"default": "mm",
|
"default": "mm",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1162,7 +1162,7 @@ ROUTING_TOOLS = [
|
|||||||
"y": {"type": "number", "description": "Y coordinate"},
|
"y": {"type": "number", "description": "Y coordinate"},
|
||||||
"unit": {
|
"unit": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["mm", "inch"],
|
"enum": ["mm", "mil", "inch"],
|
||||||
"default": "mm",
|
"default": "mm",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function registerBoardTools(server: McpServer, callKicadScript: CommandFu
|
|||||||
{
|
{
|
||||||
width: z.number().describe("Board width"),
|
width: z.number().describe("Board width"),
|
||||||
height: z.number().describe("Board height"),
|
height: z.number().describe("Board height"),
|
||||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement"),
|
unit: z.enum(["mm", "mil", "inch"]).describe("Unit of measurement"),
|
||||||
},
|
},
|
||||||
async ({ width, height, unit }) => {
|
async ({ width, height, unit }) => {
|
||||||
logger.debug(`Setting board size to ${width}x${height} ${unit}`);
|
logger.debug(`Setting board size to ${width}x${height} ${unit}`);
|
||||||
@@ -181,7 +181,7 @@ export function registerBoardTools(server: McpServer, callKicadScript: CommandFu
|
|||||||
// Position: top-left corner for rectangles/rounded_rectangle, center for circle
|
// Position: top-left corner for rectangles/rounded_rectangle, center for circle
|
||||||
x: z.number().describe("X coordinate of top-left corner for rectangles (default: 0)"),
|
x: z.number().describe("X coordinate of top-left corner for rectangles (default: 0)"),
|
||||||
y: z.number().describe("Y coordinate of top-left corner for rectangles (default: 0)"),
|
y: z.number().describe("Y coordinate of top-left corner for rectangles (default: 0)"),
|
||||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement"),
|
unit: z.enum(["mm", "mil", "inch"]).describe("Unit of measurement"),
|
||||||
})
|
})
|
||||||
.describe("Parameters for the outline shape"),
|
.describe("Parameters for the outline shape"),
|
||||||
},
|
},
|
||||||
@@ -216,7 +216,7 @@ export function registerBoardTools(server: McpServer, callKicadScript: CommandFu
|
|||||||
.object({
|
.object({
|
||||||
x: z.number().describe("X coordinate"),
|
x: z.number().describe("X coordinate"),
|
||||||
y: z.number().describe("Y coordinate"),
|
y: z.number().describe("Y coordinate"),
|
||||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement"),
|
unit: z.enum(["mm", "mil", "inch"]).describe("Unit of measurement"),
|
||||||
})
|
})
|
||||||
.describe("Position of the mounting hole"),
|
.describe("Position of the mounting hole"),
|
||||||
diameter: z.number().describe("Diameter of the hole"),
|
diameter: z.number().describe("Diameter of the hole"),
|
||||||
@@ -253,7 +253,7 @@ export function registerBoardTools(server: McpServer, callKicadScript: CommandFu
|
|||||||
.object({
|
.object({
|
||||||
x: z.number().describe("X coordinate"),
|
x: z.number().describe("X coordinate"),
|
||||||
y: z.number().describe("Y coordinate"),
|
y: z.number().describe("Y coordinate"),
|
||||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement"),
|
unit: z.enum(["mm", "mil", "inch"]).describe("Unit of measurement"),
|
||||||
})
|
})
|
||||||
.describe("Position of the text"),
|
.describe("Position of the text"),
|
||||||
layer: z.string().describe("Layer to place the text on"),
|
layer: z.string().describe("Layer to place the text on"),
|
||||||
@@ -302,7 +302,7 @@ export function registerBoardTools(server: McpServer, callKicadScript: CommandFu
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.describe("Points defining the zone outline"),
|
.describe("Points defining the zone outline"),
|
||||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement"),
|
unit: z.enum(["mm", "mil", "inch"]).describe("Unit of measurement"),
|
||||||
clearance: z.number().optional().describe("Clearance value"),
|
clearance: z.number().optional().describe("Clearance value"),
|
||||||
minWidth: z.number().optional().describe("Minimum width"),
|
minWidth: z.number().optional().describe("Minimum width"),
|
||||||
padConnection: z
|
padConnection: z
|
||||||
@@ -340,7 +340,7 @@ export function registerBoardTools(server: McpServer, callKicadScript: CommandFu
|
|||||||
"get_board_extents",
|
"get_board_extents",
|
||||||
"Return the bounding box (min/max X and Y) of all objects on the current PCB board.",
|
"Return the bounding box (min/max X and Y) of all objects on the current PCB board.",
|
||||||
{
|
{
|
||||||
unit: z.enum(["mm", "inch"]).optional().describe("Unit of measurement for the result"),
|
unit: z.enum(["mm", "mil", "inch"]).optional().describe("Unit of measurement for the result"),
|
||||||
},
|
},
|
||||||
async ({ unit }) => {
|
async ({ unit }) => {
|
||||||
logger.debug("Getting board extents");
|
logger.debug("Getting board extents");
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function registerComponentTools(server: McpServer, callKicadScript: Comma
|
|||||||
.object({
|
.object({
|
||||||
x: z.number().describe("X coordinate"),
|
x: z.number().describe("X coordinate"),
|
||||||
y: z.number().describe("Y coordinate"),
|
y: z.number().describe("Y coordinate"),
|
||||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement"),
|
unit: z.enum(["mm", "inch", "mil"]).describe("Unit of measurement"),
|
||||||
})
|
})
|
||||||
.describe("Position coordinates and unit"),
|
.describe("Position coordinates and unit"),
|
||||||
reference: z.string().optional().describe("Optional desired reference (e.g., 'R5')"),
|
reference: z.string().optional().describe("Optional desired reference (e.g., 'R5')"),
|
||||||
@@ -85,7 +85,7 @@ export function registerComponentTools(server: McpServer, callKicadScript: Comma
|
|||||||
.object({
|
.object({
|
||||||
x: z.number().describe("X coordinate"),
|
x: z.number().describe("X coordinate"),
|
||||||
y: z.number().describe("Y coordinate"),
|
y: z.number().describe("Y coordinate"),
|
||||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement"),
|
unit: z.enum(["mm", "inch", "mil"]).describe("Unit of measurement"),
|
||||||
})
|
})
|
||||||
.describe("New position coordinates and unit"),
|
.describe("New position coordinates and unit"),
|
||||||
rotation: z.number().optional().describe("Optional new rotation in degrees"),
|
rotation: z.number().optional().describe("Optional new rotation in degrees"),
|
||||||
@@ -359,7 +359,7 @@ export function registerComponentTools(server: McpServer, callKicadScript: Comma
|
|||||||
"Return all pads of a PCB component with their positions, net assignments and sizes.",
|
"Return all pads of a PCB component with their positions, net assignments and sizes.",
|
||||||
{
|
{
|
||||||
reference: z.string().describe("Reference designator of the component (e.g., 'U1')"),
|
reference: z.string().describe("Reference designator of the component (e.g., 'U1')"),
|
||||||
unit: z.enum(["mm", "inch"]).optional().describe("Unit for coordinates (default: mm)"),
|
unit: z.enum(["mm", "mil", "inch"]).optional().describe("Unit for coordinates (default: mm)"),
|
||||||
},
|
},
|
||||||
async ({ reference, unit }) => {
|
async ({ reference, unit }) => {
|
||||||
logger.debug(`Getting pads for component: ${reference}`);
|
logger.debug(`Getting pads for component: ${reference}`);
|
||||||
@@ -393,11 +393,11 @@ export function registerComponentTools(server: McpServer, callKicadScript: Comma
|
|||||||
y1: z.number(),
|
y1: z.number(),
|
||||||
x2: z.number(),
|
x2: z.number(),
|
||||||
y2: z.number(),
|
y2: z.number(),
|
||||||
unit: z.enum(["mm", "inch"]).optional(),
|
unit: z.enum(["mm", "inch", "mil"]).optional(),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.describe("Filter by bounding box region"),
|
.describe("Filter by bounding box region"),
|
||||||
unit: z.enum(["mm", "inch"]).optional().describe("Unit for coordinates (default: mm)"),
|
unit: z.enum(["mm", "mil", "inch"]).optional().describe("Unit for coordinates (default: mm)"),
|
||||||
},
|
},
|
||||||
async ({ layer, boundingBox, unit }) => {
|
async ({ layer, boundingBox, unit }) => {
|
||||||
logger.debug("Getting component list");
|
logger.debug("Getting component list");
|
||||||
@@ -427,7 +427,7 @@ export function registerComponentTools(server: McpServer, callKicadScript: Comma
|
|||||||
{
|
{
|
||||||
reference: z.string().describe("Component reference designator (e.g., 'U1')"),
|
reference: z.string().describe("Component reference designator (e.g., 'U1')"),
|
||||||
pad: z.string().describe("Pad number or name (e.g., '1', 'A1')"),
|
pad: z.string().describe("Pad number or name (e.g., '1', 'A1')"),
|
||||||
unit: z.enum(["mm", "inch"]).optional().describe("Unit for coordinates (default: mm)"),
|
unit: z.enum(["mm", "mil", "inch"]).optional().describe("Unit for coordinates (default: mm)"),
|
||||||
},
|
},
|
||||||
async ({ reference, pad, unit }) => {
|
async ({ reference, pad, unit }) => {
|
||||||
logger.debug(`Getting pad position for ${reference} pad ${pad}`);
|
logger.debug(`Getting pad position for ${reference} pad ${pad}`);
|
||||||
@@ -460,7 +460,7 @@ export function registerComponentTools(server: McpServer, callKicadScript: Comma
|
|||||||
.object({
|
.object({
|
||||||
x: z.number(),
|
x: z.number(),
|
||||||
y: z.number(),
|
y: z.number(),
|
||||||
unit: z.enum(["mm", "inch"]),
|
unit: z.enum(["mm", "inch", "mil"]),
|
||||||
})
|
})
|
||||||
.describe("Starting position"),
|
.describe("Starting position"),
|
||||||
rows: z.number().describe("Number of rows"),
|
rows: z.number().describe("Number of rows"),
|
||||||
@@ -617,7 +617,7 @@ export function registerComponentTools(server: McpServer, callKicadScript: Comma
|
|||||||
.object({
|
.object({
|
||||||
x: z.number(),
|
x: z.number(),
|
||||||
y: z.number(),
|
y: z.number(),
|
||||||
unit: z.enum(["mm", "inch"]).optional(),
|
unit: z.enum(["mm", "inch", "mil"]).optional(),
|
||||||
})
|
})
|
||||||
.describe("Offset from original position"),
|
.describe("Offset from original position"),
|
||||||
newReference: z.string().optional().describe("New reference designator"),
|
newReference: z.string().optional().describe("New reference designator"),
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ export function registerDesignRuleTools(server: McpServer, callKicadScript: Comm
|
|||||||
.object({
|
.object({
|
||||||
x: z.number().optional(),
|
x: z.number().optional(),
|
||||||
y: z.number().optional(),
|
y: z.number().optional(),
|
||||||
unit: z.enum(["mm", "inch"]).optional(),
|
unit: z.enum(["mm", "mil", "inch"]).optional(),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.describe("Position to check (if ID not provided)"),
|
.describe("Position to check (if ID not provided)"),
|
||||||
@@ -270,7 +270,7 @@ export function registerDesignRuleTools(server: McpServer, callKicadScript: Comm
|
|||||||
.object({
|
.object({
|
||||||
x: z.number().optional(),
|
x: z.number().optional(),
|
||||||
y: z.number().optional(),
|
y: z.number().optional(),
|
||||||
unit: z.enum(["mm", "inch"]).optional(),
|
unit: z.enum(["mm", "mil", "inch"]).optional(),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.describe("Position to check (if ID not provided)"),
|
.describe("Position to check (if ID not provided)"),
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ export function registerExportTools(server: McpServer, callKicadScript: CommandF
|
|||||||
{
|
{
|
||||||
outputPath: z.string().describe("Path to save the position file"),
|
outputPath: z.string().describe("Path to save the position file"),
|
||||||
format: z.enum(["CSV", "ASCII"]).optional().describe("File format (default: CSV)"),
|
format: z.enum(["CSV", "ASCII"]).optional().describe("File format (default: CSV)"),
|
||||||
units: z.enum(["mm", "inch"]).optional().describe("Units to use (default: mm)"),
|
units: z.enum(["mm", "mil", "inch"]).optional().describe("Units to use (default: mm)"),
|
||||||
side: z
|
side: z
|
||||||
.enum(["top", "bottom", "both"])
|
.enum(["top", "bottom", "both"])
|
||||||
.optional()
|
.optional()
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ export function registerRoutingTools(server: McpServer, callKicadScript: Functio
|
|||||||
.object({
|
.object({
|
||||||
x: z.number(),
|
x: z.number(),
|
||||||
y: z.number(),
|
y: z.number(),
|
||||||
unit: z.enum(["mm", "inch"]).optional(),
|
unit: z.enum(["mm", "inch", "mil"]).optional(),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.describe("Delete trace nearest to this position"),
|
.describe("Delete trace nearest to this position"),
|
||||||
@@ -206,11 +206,11 @@ export function registerRoutingTools(server: McpServer, callKicadScript: Functio
|
|||||||
y1: z.number(),
|
y1: z.number(),
|
||||||
x2: z.number(),
|
x2: z.number(),
|
||||||
y2: z.number(),
|
y2: z.number(),
|
||||||
unit: z.enum(["mm", "inch"]).optional(),
|
unit: z.enum(["mm", "inch", "mil"]).optional(),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.describe("Filter by bounding box region"),
|
.describe("Filter by bounding box region"),
|
||||||
unit: z.enum(["mm", "inch"]).optional().describe("Unit for coordinates"),
|
unit: z.enum(["mm", "inch", "mil"]).optional().describe("Unit for coordinates"),
|
||||||
},
|
},
|
||||||
async (args: any) => {
|
async (args: any) => {
|
||||||
const result = await callKicadScript("query_traces", args);
|
const result = await callKicadScript("query_traces", args);
|
||||||
@@ -358,7 +358,7 @@ export function registerRoutingTools(server: McpServer, callKicadScript: Functio
|
|||||||
.boolean()
|
.boolean()
|
||||||
.optional()
|
.optional()
|
||||||
.describe("Include statistics (track count, total length, etc.)"),
|
.describe("Include statistics (track count, total length, etc.)"),
|
||||||
unit: z.enum(["mm", "inch"]).optional().describe("Unit for length measurements"),
|
unit: z.enum(["mm", "mil", "inch"]).optional().describe("Unit for length measurements"),
|
||||||
},
|
},
|
||||||
async (args: any) => {
|
async (args: any) => {
|
||||||
const result = await callKicadScript("get_nets_list", args);
|
const result = await callKicadScript("get_nets_list", args);
|
||||||
|
|||||||
106
tests/test_mil_unit_support.py
Normal file
106
tests/test_mil_unit_support.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
"""Tests for ``mil`` unit support across position/coordinate commands.
|
||||||
|
|
||||||
|
KiCad natively understands mils (1 mil = 0.0254 mm = 25 400 nm). The MCP
|
||||||
|
server now accepts ``"mil"`` as a value of the ``unit`` field everywhere a
|
||||||
|
position or coordinate is specified. Tests below assert:
|
||||||
|
|
||||||
|
- The ``unit→nanometer`` scale used in command handlers maps mil → 25 400.
|
||||||
|
- The IPC handler converts mil positions to mm before calling the IPC API.
|
||||||
|
- The Python tool schema enums include ``"mil"`` (it was previously
|
||||||
|
restricted to ``["mm", "inch"]``).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent / "python"))
|
||||||
|
|
||||||
|
# Scale used by the per-command handlers when they convert position values
|
||||||
|
# to KiCad internal nanometers.
|
||||||
|
MIL_TO_NM = 25_400
|
||||||
|
MM_TO_NM = 1_000_000
|
||||||
|
INCH_TO_NM = 25_400_000
|
||||||
|
|
||||||
|
|
||||||
|
def _scale(unit: str) -> int:
|
||||||
|
"""Mirror of the inline ternary used in commands/* for position scaling."""
|
||||||
|
return 1_000_000 if unit == "mm" else (25_400 if unit == "mil" else 25_400_000)
|
||||||
|
|
||||||
|
|
||||||
|
def test_scale_mapping_includes_mil():
|
||||||
|
assert _scale("mm") == MM_TO_NM
|
||||||
|
assert _scale("mil") == MIL_TO_NM
|
||||||
|
assert _scale("inch") == INCH_TO_NM
|
||||||
|
|
||||||
|
|
||||||
|
def test_one_thousand_mil_equals_one_inch_in_nm():
|
||||||
|
"""Sanity check: 1000 mil should produce the same nm offset as 1 inch."""
|
||||||
|
one_inch = 1 * _scale("inch")
|
||||||
|
one_thousand_mil = 1000 * _scale("mil")
|
||||||
|
assert one_inch == one_thousand_mil
|
||||||
|
|
||||||
|
|
||||||
|
def _make_iface() -> Any:
|
||||||
|
with patch("kicad_interface.USE_IPC_BACKEND", True):
|
||||||
|
from kicad_interface import KiCADInterface
|
||||||
|
|
||||||
|
iface = KiCADInterface.__new__(KiCADInterface)
|
||||||
|
iface.use_ipc = True
|
||||||
|
iface.board = None
|
||||||
|
iface.ipc_board_api = MagicMock()
|
||||||
|
iface.ipc_board_api.place_component.return_value = True
|
||||||
|
iface.ipc_board_api.move_component.return_value = True
|
||||||
|
return iface
|
||||||
|
|
||||||
|
|
||||||
|
def test_ipc_place_component_converts_mil_to_mm():
|
||||||
|
iface = _make_iface()
|
||||||
|
iface._ipc_place_component(
|
||||||
|
{
|
||||||
|
"reference": "R1",
|
||||||
|
"footprint": "Resistor_SMD:R_0805",
|
||||||
|
"position": {"x": 100, "y": 200, "unit": "mil"},
|
||||||
|
"rotation": 0,
|
||||||
|
"layer": "F.Cu",
|
||||||
|
"value": "220",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
_, kwargs = iface.ipc_board_api.place_component.call_args
|
||||||
|
assert kwargs["x"] == pytest.approx(2.54)
|
||||||
|
assert kwargs["y"] == pytest.approx(5.08)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ipc_move_component_converts_mil_to_mm():
|
||||||
|
iface = _make_iface()
|
||||||
|
iface._ipc_move_component({"reference": "R1", "position": {"x": 1000, "y": 500, "unit": "mil"}})
|
||||||
|
_, kwargs = iface.ipc_board_api.move_component.call_args
|
||||||
|
assert kwargs["x"] == pytest.approx(25.4)
|
||||||
|
assert kwargs["y"] == pytest.approx(12.7)
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_tool_schema_unit_enums_include_mil():
|
||||||
|
"""Every unit enum in TOOL_SCHEMAS must include mil."""
|
||||||
|
from schemas.tool_schemas import TOOL_SCHEMAS
|
||||||
|
|
||||||
|
def _find_unit_enums(node):
|
||||||
|
"""Walk the schema and yield every enum list found under 'unit' fields."""
|
||||||
|
if isinstance(node, dict):
|
||||||
|
for k, v in node.items():
|
||||||
|
if k == "unit" and isinstance(v, dict) and "enum" in v:
|
||||||
|
yield v["enum"]
|
||||||
|
else:
|
||||||
|
yield from _find_unit_enums(v)
|
||||||
|
elif isinstance(node, list):
|
||||||
|
for item in node:
|
||||||
|
yield from _find_unit_enums(item)
|
||||||
|
|
||||||
|
enums = list(_find_unit_enums(TOOL_SCHEMAS))
|
||||||
|
assert enums, "Expected at least one unit enum in TOOL_SCHEMAS"
|
||||||
|
for enum in enums:
|
||||||
|
assert "mil" in enum, f"Unit enum {enum} is missing 'mil'"
|
||||||
|
# Sanity: mm and inch should still be present too
|
||||||
|
assert "mm" in enum and "inch" in enum, f"Unit enum {enum} is malformed"
|
||||||
Reference in New Issue
Block a user