feat(schematic): expose justify directive on field labels via MCP
Add optional `justify` property to `fieldPositions` entries in `edit_schematic_component` and to `set_schematic_component_property`. Changes: - `python/kicad_interface.py`: new `_set_justify_on_property()` helper that adds/replaces/removes the `(justify ...)` token inside a property's `(effects ...)` block. Passing "center" (the KiCad default) removes the directive entirely. Integrated into `_set_property_in_block()` (for the `properties` dict path) and into the `field_positions` loop in `_handle_edit_schematic_component()`. `_handle_set_schematic_component_property()` now forwards `justify` from params through to the spec dict. Also fixes pre-existing mypy type-ignore on `circle.radius` (kipy stub). - `src/tools/schematic.ts`: extend the `fieldPositions` Zod schema to accept `justify?: string | string[]` (array form normalised to a space-separated string before the Python call). Add `justify?: string` to `set_schematic_component_property`. - `python/commands/routing.py`: fix pre-existing mypy error — annotate `ex` and `ey` as `float` in `_point_to_segment_distance_nm`. - `python/commands/pin_locator.py`: fix pre-existing mypy error — use explicit `str()` cast on `pin_data["number"]` before `dict.get()` call. - `tests/test_schematic_field_justify.py`: 14 unit + integration tests covering add/replace/remove of the justify directive and backward compatibility (calls without justify leave existing directives untouched). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1539,7 +1539,10 @@ class KiCADInterface:
|
||||
Args:
|
||||
block: The full text of the (symbol ...) block.
|
||||
name: Property name (e.g. "MPN", "Manufacturer").
|
||||
spec: Dict that may contain keys: value, x, y, angle, hide, fontSize.
|
||||
spec: Dict that may contain keys: value, x, y, angle, hide, fontSize,
|
||||
justify. ``justify`` is a space-separated string of KiCad
|
||||
alignment keywords (e.g. "left", "right top"). "center" removes
|
||||
the directive (KiCad default).
|
||||
default_position: (x, y) of the parent symbol — used as the default
|
||||
location for newly-created properties so the field is anchored
|
||||
near the component, not at (0, 0).
|
||||
@@ -1554,6 +1557,7 @@ class KiCADInterface:
|
||||
new_y = spec.get("y")
|
||||
new_angle = spec.get("angle")
|
||||
new_hide = spec.get("hide")
|
||||
new_justify = spec.get("justify")
|
||||
font_size = spec.get("fontSize", 1.27)
|
||||
|
||||
existing_match = re.search(
|
||||
@@ -1594,6 +1598,9 @@ class KiCADInterface:
|
||||
if new_hide is not None:
|
||||
block = self._set_hide_on_property(block, name, bool(new_hide))
|
||||
|
||||
if new_justify is not None:
|
||||
block = self._set_justify_on_property(block, name, str(new_justify))
|
||||
|
||||
return block, "updated"
|
||||
|
||||
# Property does not exist — append a new one after the last existing property
|
||||
@@ -1612,9 +1619,16 @@ class KiCADInterface:
|
||||
escaped = self._escape_sexpr_string(str(new_value))
|
||||
escaped_name = self._escape_sexpr_string(str(name))
|
||||
|
||||
# Build optional (justify ...) token for the effects block.
|
||||
justify_str = ""
|
||||
if new_justify is not None:
|
||||
tokens = str(new_justify).strip().split()
|
||||
if not all(t == "center" for t in tokens):
|
||||
justify_str = f" (justify {str(new_justify).strip()})"
|
||||
|
||||
new_prop = (
|
||||
f' (property "{escaped_name}" "{escaped}" (at {cx} {cy} {ca})\n'
|
||||
f" (effects (font (size {font_size} {font_size})) {hide_str})\n"
|
||||
f" (effects (font (size {font_size} {font_size})){justify_str} {hide_str})\n"
|
||||
f" )"
|
||||
)
|
||||
|
||||
@@ -1675,6 +1689,53 @@ class KiCADInterface:
|
||||
new_effects = "(" + eff_inner + ")"
|
||||
return block[:eff_start] + new_effects + block[eff_end + 1 :]
|
||||
|
||||
def _set_justify_on_property(self, block: str, name: str, justify: str) -> str:
|
||||
"""Set or clear the (justify ...) directive on a named property's effects clause.
|
||||
|
||||
``justify`` is a space-separated string of KiCad alignment keywords:
|
||||
horizontal: "left" | "right" | "center"
|
||||
vertical: "top" | "bottom" | "center"
|
||||
Any combination of one or two tokens is accepted, e.g. "left", "right top".
|
||||
Passing "center" (the KiCad default) removes the (justify ...) directive
|
||||
entirely, which is how KiCad represents centered alignment.
|
||||
|
||||
Handles effects clauses that already contain a (justify ...) token, and
|
||||
those that do not.
|
||||
"""
|
||||
import re
|
||||
|
||||
prop_match = re.search(
|
||||
r'\(property\s+"' + re.escape(name) + r'"',
|
||||
block,
|
||||
)
|
||||
if not prop_match:
|
||||
return block
|
||||
prop_start = prop_match.start()
|
||||
prop_end = self._find_matching_paren(block, prop_start)
|
||||
if prop_end < 0:
|
||||
return block
|
||||
|
||||
prop_segment = block[prop_start : prop_end + 1]
|
||||
eff_match = re.search(r"\(effects\b", prop_segment)
|
||||
if not eff_match:
|
||||
return block
|
||||
eff_start = prop_start + eff_match.start()
|
||||
eff_end = self._find_matching_paren(block, eff_start)
|
||||
if eff_end < 0:
|
||||
return block
|
||||
|
||||
eff_inner = block[eff_start + 1 : eff_end] # 'effects (font ...) ...'
|
||||
# Remove any pre-existing (justify ...) token
|
||||
eff_inner = re.sub(r"\s*\(justify\b[^)]*\)", "", eff_inner)
|
||||
# "center" is the KiCad default — omitting the directive means centered
|
||||
tokens = justify.strip().split()
|
||||
is_center_only = all(t == "center" for t in tokens)
|
||||
if not is_center_only:
|
||||
eff_inner = eff_inner.rstrip() + f" (justify {justify.strip()})"
|
||||
|
||||
new_effects = "(" + eff_inner + ")"
|
||||
return block[:eff_start] + new_effects + block[eff_end + 1 :]
|
||||
|
||||
def _remove_property_from_block(self, block: str, name: str) -> Tuple[str, bool]:
|
||||
"""Remove a property from the symbol block. Returns (new_block, removed_bool)."""
|
||||
import re
|
||||
@@ -1885,6 +1946,11 @@ class KiCADInterface:
|
||||
rf"\1(at {x} {y} {angle})",
|
||||
block_text,
|
||||
)
|
||||
justify = pos.get("justify")
|
||||
if justify is not None:
|
||||
block_text = self._set_justify_on_property(
|
||||
block_text, field_name, str(justify)
|
||||
)
|
||||
|
||||
properties_added: Dict[str, Any] = {}
|
||||
properties_updated: Dict[str, Any] = {}
|
||||
@@ -1973,7 +2039,7 @@ class KiCADInterface:
|
||||
return {"success": False, "message": "value is required"}
|
||||
|
||||
spec: Dict[str, Any] = {"value": params["value"]}
|
||||
for key in ("x", "y", "angle", "hide", "fontSize"):
|
||||
for key in ("x", "y", "angle", "hide", "fontSize", "justify"):
|
||||
if params.get(key) is not None:
|
||||
spec[key] = params[key]
|
||||
|
||||
@@ -6002,7 +6068,7 @@ print("ok")
|
||||
# Create circle on Edge.Cuts layer for the hole
|
||||
circle = BoardCircle()
|
||||
circle.center = Vector2.from_xy(from_mm(x), from_mm(y))
|
||||
circle.radius = from_mm(diameter / 2)
|
||||
circle.radius = from_mm(diameter / 2) # type: ignore[assignment,method-assign]
|
||||
circle.layer = BoardLayer.BL_Edge_Cuts
|
||||
circle.attributes.stroke.width = from_mm(0.1)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user