Merge pull request #196 from gcolonese/feat-schematic-justify

feat(schematic): expose justify directive on field labels via MCP
This commit is contained in:
mixelpixx
2026-05-30 12:15:28 -04:00
committed by GitHub
4 changed files with 409 additions and 7 deletions

View File

@@ -1616,7 +1616,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).
@@ -1631,6 +1634,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(
@@ -1671,6 +1675,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
@@ -1689,9 +1696,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" )"
)
@@ -1752,6 +1766,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
@@ -1962,6 +2023,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] = {}
@@ -2050,7 +2116,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]
@@ -6072,7 +6138,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)