Merge pull request #252 from gcolonese/feat-set-footprint-type

Note: get_component_properties 'attributes' response changed from raw bitmask ints to {type: smd|through_hole|unspecified, exclude_from_pos_files, exclude_from_bom, not_in_schematic}. Cleaner shape for AI consumers; callers parsing the old integer keys must update.
This commit is contained in:
mixelpixx
2026-06-12 12:18:25 -04:00
committed by GitHub
5 changed files with 700 additions and 5 deletions

View File

@@ -390,6 +390,104 @@ class ComponentCommands:
logger.error(f"Error editing component: {str(e)}")
return {"success": False, "message": "Failed to edit component", "errorDetails": str(e)}
def set_footprint_type(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Set the placement type and exclusion flags of an existing PCB footprint.
The placement type controls whether a footprint appears in pick-and-place
(.pos) output files. KiCAD recognises three types:
- ``through_hole`` — PTH component (through-hole)
- ``smd`` — SMD component (surface-mount)
- ``unspecified`` — neither bit set (default for manually-placed items)
Optional exclusion flags (omit to leave unchanged):
- ``exclude_from_pos_files`` — suppress this ref from .pos exports
- ``exclude_from_bom`` — suppress this ref from BoM exports
- ``not_in_schematic`` — marks footprint as board-only (no schematic
symbol); KiCAD stores this as FP_BOARD_ONLY
"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first",
}
reference = params.get("reference")
fp_type = params.get("type")
if not reference:
return {
"success": False,
"message": "Missing reference",
"errorDetails": "reference parameter is required",
}
if fp_type not in ("smd", "through_hole", "unspecified"):
return {
"success": False,
"message": "Invalid type",
"errorDetails": "type must be one of: smd, through_hole, unspecified",
}
# Find the footprint
module = self.board.FindFootprintByReference(reference)
if not module:
return {
"success": False,
"message": "Component not found",
"errorDetails": f"Could not find component: {reference}",
}
# --- Placement type (mutually exclusive bits) ---
# Read current attributes, clear both type bits, then set the new one.
attrs = module.GetAttributes()
attrs &= ~(pcbnew.FP_THROUGH_HOLE | pcbnew.FP_SMD)
if fp_type == "through_hole":
attrs |= pcbnew.FP_THROUGH_HOLE
elif fp_type == "smd":
attrs |= pcbnew.FP_SMD
# "unspecified" leaves both bits clear
module.SetAttributes(attrs)
# --- Optional exclusion flags ---
# Use the dedicated setters so we don't have to manage bit masks manually.
if "exclude_from_pos_files" in params:
module.SetExcludedFromPosFiles(bool(params["exclude_from_pos_files"]))
if "exclude_from_bom" in params:
module.SetExcludedFromBOM(bool(params["exclude_from_bom"]))
if "not_in_schematic" in params:
# FP_BOARD_ONLY is the underlying bit for the "not in schematic" flag.
module.SetBoardOnly(bool(params["not_in_schematic"]))
# Read back the final state so the caller can verify.
final_attrs = module.GetAttributes()
if final_attrs & pcbnew.FP_THROUGH_HOLE:
resolved_type = "through_hole"
elif final_attrs & pcbnew.FP_SMD:
resolved_type = "smd"
else:
resolved_type = "unspecified"
return {
"success": True,
"message": f"Updated footprint type for {reference}",
"component": {
"reference": reference,
"type": resolved_type,
"exclude_from_pos_files": module.IsExcludedFromPosFiles(),
"exclude_from_bom": module.IsExcludedFromBOM(),
"not_in_schematic": module.IsBoardOnly(),
},
}
except Exception as e:
logger.error(f"Error setting footprint type: {str(e)}")
return {
"success": False,
"message": "Failed to set footprint type",
"errorDetails": str(e),
}
def get_component_properties(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get detailed properties of a component"""
try:
@@ -454,6 +552,15 @@ class ComponentCommands:
except Exception:
pass # Courtyard may not exist or API may differ
# Resolve placement type as a human-readable string
raw_attrs = module.GetAttributes()
if raw_attrs & pcbnew.FP_THROUGH_HOLE:
placement_type = "through_hole"
elif raw_attrs & pcbnew.FP_SMD:
placement_type = "smd"
else:
placement_type = "unspecified"
return {
"success": True,
"component": {
@@ -464,9 +571,10 @@ class ComponentCommands:
"rotation": module.GetOrientation().AsDegrees(),
"layer": self.board.GetLayerName(module.GetLayer()),
"attributes": {
"smd": module.GetAttributes() & pcbnew.FP_SMD,
"through_hole": module.GetAttributes() & pcbnew.FP_THROUGH_HOLE,
"board_only": module.GetAttributes() & pcbnew.FP_BOARD_ONLY,
"type": placement_type,
"exclude_from_pos_files": module.IsExcludedFromPosFiles(),
"exclude_from_bom": module.IsExcludedFromBOM(),
"not_in_schematic": module.IsBoardOnly(),
},
"boundingBox": bbox_data,
"courtyard": courtyard_data,

View File

@@ -325,9 +325,9 @@ try:
from commands.project import ProjectCommands
from commands.routing import RoutingCommands
from commands.schematic import SchematicManager
from commands.schematic_hierarchy import SchematicHierarchyCommands
from commands.schematic_field_layout import SchematicFieldLayoutCommands
from commands.schematic_batch import SchematicBatchCommands
from commands.schematic_field_layout import SchematicFieldLayoutCommands
from commands.schematic_hierarchy import SchematicHierarchyCommands
from commands.symbol_creator import SymbolCreator
from commands.symbol_pins import SymbolPinCommands
@@ -504,6 +504,7 @@ class KiCADInterface:
"align_components": self.component_commands.align_components,
"check_courtyard_overlaps": self.component_commands.check_courtyard_overlaps,
"duplicate_component": self.component_commands.duplicate_component,
"set_footprint_type": self.component_commands.set_footprint_type,
# Routing commands
"add_net": self.routing_commands.add_net,
"route_trace": self.routing_commands.route_trace,
@@ -678,6 +679,7 @@ class KiCADInterface:
"delete_component": "_ipc_delete_component",
"get_component_list": "_ipc_get_component_list",
"get_component_properties": "_ipc_get_component_properties",
"set_footprint_type": "_ipc_set_footprint_type",
# Save command
"save_project": "_ipc_save_project",
}
@@ -930,6 +932,7 @@ class KiCADInterface:
"sync_schematic_to_board",
"connect_passthrough",
"connect_to_net",
"set_footprint_type",
}
@staticmethod
@@ -6267,6 +6270,97 @@ print("ok")
logger.error(f"IPC get_component_properties error: {e}")
return {"success": False, "message": str(e)}
def _ipc_set_footprint_type(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""IPC handler for set_footprint_type.
Sets the placement type (through_hole / smd / unspecified) and optional
exclusion flags on a footprint via the kipy proto API, so the change is
visible in the KiCAD UI without a manual reload.
Falls back to the SWIG path if the IPC footprint lookup fails, because
kipy's Footprint wrapper does not always expose a ``not_in_schematic``
setter depending on the installed kipy version.
"""
try:
reference = params.get("reference", params.get("componentId", ""))
fp_type = params.get("type")
if fp_type not in ("smd", "through_hole", "unspecified"):
return {
"success": False,
"message": "Invalid type",
"errorDetails": "type must be one of: smd, through_hole, unspecified",
}
board = self.ipc_board_api._get_board()
footprints = board.get_footprints()
target_fp = None
for fp in footprints:
if fp.reference_field and fp.reference_field.text.value == reference:
target_fp = fp
break
if not target_fp:
return {"success": False, "message": f"Component {reference} not found"}
try:
from kipy.proto.board.board_types_pb2 import FootprintMountingStyle
style_map = {
"through_hole": FootprintMountingStyle.FMS_THROUGH_HOLE,
"smd": FootprintMountingStyle.FMS_SMD,
"unspecified": FootprintMountingStyle.FMS_UNSPECIFIED,
}
target_fp.proto.attributes.mounting_style = style_map[fp_type]
if "exclude_from_pos_files" in params:
target_fp.proto.attributes.exclude_from_position_files = bool(
params["exclude_from_pos_files"]
)
if "exclude_from_bom" in params:
target_fp.proto.attributes.exclude_from_bill_of_materials = bool(
params["exclude_from_bom"]
)
if "not_in_schematic" in params:
target_fp.proto.attributes.not_in_schematic = bool(params["not_in_schematic"])
commit = board.begin_commit()
board.update_items([target_fp])
board.push_commit(commit, f"Set footprint type for {reference}")
return {
"success": True,
"message": f"Updated footprint type for {reference} (visible in KiCAD UI)",
"component": {
"reference": reference,
"type": fp_type,
"exclude_from_pos_files": target_fp.proto.attributes.exclude_from_position_files, # noqa: E501
"exclude_from_bom": target_fp.proto.attributes.exclude_from_bill_of_materials,
"not_in_schematic": target_fp.proto.attributes.not_in_schematic,
},
"_backend": "ipc",
"_realtime": True,
}
except Exception as ipc_err:
# IPC proto manipulation failed; fall back to SWIG for the write.
logger.warning(
f"_ipc_set_footprint_type: IPC proto update failed ({ipc_err}), "
"falling back to SWIG"
)
if self.board:
result = self.component_commands.set_footprint_type(params)
if isinstance(result, dict):
result["_backend"] = "swig"
result["_realtime"] = False
return result
raise
except Exception as e:
logger.error(f"IPC set_footprint_type error: {e}")
return {"success": False, "message": str(e)}
# =========================================================================
# Legacy IPC command handlers (explicit ipc_* commands)