Merge pull request #229 from ravishivt/feat/hierarchical-sheets
feat(schematic): hierarchical sheet insertion and subsheet scaffolding
This commit is contained in:
272
python/commands/schematic_hierarchy.py
Normal file
272
python/commands/schematic_hierarchy.py
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
"""
|
||||||
|
Schematic hierarchical-sheet commands.
|
||||||
|
|
||||||
|
Tools:
|
||||||
|
- add_hierarchical_sheet: insert a hierarchical-sheet reference into a parent schematic
|
||||||
|
- create_hierarchical_subsheet: create a sub-sheet file and link it in one call
|
||||||
|
|
||||||
|
The command class holds a back-reference to KiCADInterface so it can reuse the existing
|
||||||
|
create_schematic handler, and exposes fix_subsheet_instances for the batch module to call.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
import sexpdata
|
||||||
|
from sexpdata import Symbol
|
||||||
|
|
||||||
|
logger = logging.getLogger("kicad_interface")
|
||||||
|
|
||||||
|
|
||||||
|
class SchematicHierarchyCommands:
|
||||||
|
"""Handlers for hierarchical sheet insertion and subsheet scaffolding."""
|
||||||
|
|
||||||
|
def __init__(self, iface):
|
||||||
|
self.iface = iface
|
||||||
|
|
||||||
|
def add_hierarchical_sheet(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Insert a hierarchical sheet reference block into a parent schematic."""
|
||||||
|
logger.info("Adding hierarchical sheet")
|
||||||
|
try:
|
||||||
|
schematic_path = params.get("schematicPath")
|
||||||
|
subsheet_path = params.get("subsheetPath")
|
||||||
|
sheet_name = params.get("sheetName", "Sheet")
|
||||||
|
position = params.get("position", {})
|
||||||
|
size = params.get("size", {})
|
||||||
|
|
||||||
|
if not schematic_path or not subsheet_path:
|
||||||
|
return {"success": False, "message": "schematicPath and subsheetPath are required"}
|
||||||
|
|
||||||
|
x = float(position.get("x", 50))
|
||||||
|
y = float(position.get("y", 50))
|
||||||
|
w = float(size.get("width", 80))
|
||||||
|
h = float(size.get("height", 50))
|
||||||
|
|
||||||
|
parent_file = Path(schematic_path)
|
||||||
|
try:
|
||||||
|
rel_str = str(
|
||||||
|
Path(subsheet_path).resolve().relative_to(parent_file.parent.resolve())
|
||||||
|
).replace("\\", "/")
|
||||||
|
except ValueError:
|
||||||
|
rel_str = str(subsheet_path).replace("\\", "/")
|
||||||
|
|
||||||
|
sheet_block_uuid = str(uuid.uuid4())
|
||||||
|
name_x, name_y = round(x + 2.54, 4), round(y - 1.27, 4)
|
||||||
|
file_x, file_y = round(x + 2.54, 4), round(y + h + 1.27, 4)
|
||||||
|
|
||||||
|
sheet_block = (
|
||||||
|
f" (sheet (at {x} {y}) (size {w} {h}) (fields_autoplaced yes)\n"
|
||||||
|
f" (stroke (width 0.0006) (type default))\n"
|
||||||
|
f" (fill (color 0 0 0 0.0000))\n"
|
||||||
|
f' (uuid "{sheet_block_uuid}")\n'
|
||||||
|
f' (property "Sheet name" "{sheet_name}" (at {name_x} {name_y} 0)\n'
|
||||||
|
f" (effects (font (size 1.27 1.27)) (justify left bottom))\n"
|
||||||
|
f" )\n"
|
||||||
|
f' (property "Sheet file" "{rel_str}" (at {file_x} {file_y} 0)\n'
|
||||||
|
f" (effects (font (size 1.27 1.27)) (justify left bottom))\n"
|
||||||
|
f" )\n"
|
||||||
|
f" )\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
content = parent_file.read_text(encoding="utf-8")
|
||||||
|
parent_uuid_match = re.search(r"\(uuid\s+([0-9a-fA-F-]+)\)", content)
|
||||||
|
parent_uuid = parent_uuid_match.group(1) if parent_uuid_match else ""
|
||||||
|
existing_pages = re.findall(r'\(page\s+"(\d+)"\)', content)
|
||||||
|
next_page = max((int(p) for p in existing_pages), default=0) + 1
|
||||||
|
|
||||||
|
instance_path = (
|
||||||
|
f"/{parent_uuid}/{sheet_block_uuid}" if parent_uuid else f"/{sheet_block_uuid}"
|
||||||
|
)
|
||||||
|
path_entry = f' (path "{instance_path}" (page "{next_page}"))\n'
|
||||||
|
|
||||||
|
insert_at = content.rfind("(sheet_instances")
|
||||||
|
if insert_at == -1:
|
||||||
|
return {"success": False, "message": "Could not find (sheet_instances in schematic"}
|
||||||
|
content = content[:insert_at] + sheet_block + " " + content[insert_at:]
|
||||||
|
|
||||||
|
si_start = content.rfind("(sheet_instances")
|
||||||
|
depth = 0
|
||||||
|
si_close = len(content) - 1
|
||||||
|
for i in range(si_start, len(content)):
|
||||||
|
if content[i] == "(":
|
||||||
|
depth += 1
|
||||||
|
elif content[i] == ")":
|
||||||
|
depth -= 1
|
||||||
|
if depth == 0:
|
||||||
|
si_close = i
|
||||||
|
break
|
||||||
|
content = content[:si_close] + path_entry + " " + content[si_close:]
|
||||||
|
|
||||||
|
parent_file.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
|
# Ensure each sub-sheet component has the hierarchical instance entry.
|
||||||
|
self.fix_subsheet_instances(str(parent_file), content)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"sheet_uuid": sheet_block_uuid,
|
||||||
|
"sheet_name": sheet_name,
|
||||||
|
"subsheet_path": rel_str,
|
||||||
|
"page": next_page,
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error adding hierarchical sheet: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
|
def create_hierarchical_subsheet(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Create a new sub-sheet file and link it into a parent schematic in one call."""
|
||||||
|
logger.info("Creating hierarchical subsheet")
|
||||||
|
try:
|
||||||
|
parent_path = params.get("parentSchematicPath")
|
||||||
|
subsheet_path = params.get("subsheetPath")
|
||||||
|
sheet_name = params.get("sheetName", "Sheet")
|
||||||
|
position = params.get("position", {})
|
||||||
|
size = params.get("size", {})
|
||||||
|
metadata = params.get("metadata", {})
|
||||||
|
|
||||||
|
if not parent_path or not subsheet_path:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "parentSchematicPath and subsheetPath are required",
|
||||||
|
}
|
||||||
|
|
||||||
|
create_result = self.iface._handle_create_schematic(
|
||||||
|
{"filename": subsheet_path, "metadata": metadata}
|
||||||
|
)
|
||||||
|
if not create_result.get("success"):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"Failed to create sub-sheet: {create_result.get('message')}",
|
||||||
|
}
|
||||||
|
|
||||||
|
link_result = self.add_hierarchical_sheet(
|
||||||
|
{
|
||||||
|
"schematicPath": parent_path,
|
||||||
|
"subsheetPath": subsheet_path,
|
||||||
|
"sheetName": sheet_name,
|
||||||
|
"position": position,
|
||||||
|
"size": size,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if not link_result.get("success"):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"Created sub-sheet but failed to link: {link_result.get('message')}",
|
||||||
|
"subsheet_created": create_result.get("file_path", subsheet_path),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"subsheet_path": create_result.get("file_path", subsheet_path),
|
||||||
|
"subsheet_uuid": create_result.get("schematic_uuid"),
|
||||||
|
"sheet_block_uuid": link_result.get("sheet_uuid"),
|
||||||
|
"sheet_name": sheet_name,
|
||||||
|
"page": link_result.get("page"),
|
||||||
|
"message": (
|
||||||
|
f"Created sub-sheet '{sheet_name}' at {subsheet_path} "
|
||||||
|
f"and linked it into {parent_path} (page {link_result.get('page')})"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in create_hierarchical_subsheet: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
|
def fix_subsheet_instances(self, parent_path: str, parent_content: str) -> List[str]:
|
||||||
|
"""Ensure every component in each referenced sub-sheet has an instances entry for the
|
||||||
|
sheet-block UUID, so ERC resolves references correctly. Returns modified sub-sheet paths.
|
||||||
|
"""
|
||||||
|
modified_sheets: List[str] = []
|
||||||
|
try:
|
||||||
|
parent_file = Path(parent_path)
|
||||||
|
parent_data = sexpdata.loads(parent_content)
|
||||||
|
|
||||||
|
for item in parent_data:
|
||||||
|
if not (isinstance(item, list) and len(item) > 0 and item[0] == Symbol("sheet")):
|
||||||
|
continue
|
||||||
|
|
||||||
|
sheet_block_uuid = None
|
||||||
|
sheet_file_rel = None
|
||||||
|
for sub in item:
|
||||||
|
if isinstance(sub, list) and len(sub) >= 2 and sub[0] == Symbol("uuid"):
|
||||||
|
sheet_block_uuid = str(sub[1])
|
||||||
|
elif (
|
||||||
|
isinstance(sub, list)
|
||||||
|
and len(sub) >= 3
|
||||||
|
and sub[0] == Symbol("property")
|
||||||
|
and sub[1] == "Sheet file"
|
||||||
|
):
|
||||||
|
sheet_file_rel = str(sub[2])
|
||||||
|
if not sheet_block_uuid or not sheet_file_rel:
|
||||||
|
continue
|
||||||
|
|
||||||
|
sub_sheet_path = parent_file.parent / sheet_file_rel
|
||||||
|
if not sub_sheet_path.exists():
|
||||||
|
logger.warning(f"Sub-sheet not found: {sub_sheet_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
parent_uuid_match = re.search(r"\(uuid\s+([0-9a-fA-F-]+)\)", parent_content)
|
||||||
|
parent_uuid = parent_uuid_match.group(1) if parent_uuid_match else ""
|
||||||
|
target_path = (
|
||||||
|
f"/{parent_uuid}/{sheet_block_uuid}" if parent_uuid else f"/{sheet_block_uuid}"
|
||||||
|
)
|
||||||
|
|
||||||
|
sub_content = sub_sheet_path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
def _balanced_end(s: str, start: int) -> int:
|
||||||
|
depth = 0
|
||||||
|
for j in range(start, len(s)):
|
||||||
|
if s[j] == "(":
|
||||||
|
depth += 1
|
||||||
|
elif s[j] == ")":
|
||||||
|
depth -= 1
|
||||||
|
if depth == 0:
|
||||||
|
return j
|
||||||
|
return len(s) - 1
|
||||||
|
|
||||||
|
result_parts: List[str] = []
|
||||||
|
pos = 0
|
||||||
|
changed = False
|
||||||
|
while True:
|
||||||
|
idx = sub_content.find("(instances", pos)
|
||||||
|
if idx == -1:
|
||||||
|
result_parts.append(sub_content[pos:])
|
||||||
|
break
|
||||||
|
result_parts.append(sub_content[pos:idx])
|
||||||
|
end = _balanced_end(sub_content, idx)
|
||||||
|
block = sub_content[idx : end + 1]
|
||||||
|
|
||||||
|
if target_path not in block:
|
||||||
|
existing = re.search(r'\(reference\s+"([^"]+)"\)\s*\(unit\s+(\d+)\)', block)
|
||||||
|
if existing:
|
||||||
|
new_entry = (
|
||||||
|
f'(path "{target_path}" (reference "{existing.group(1)}") '
|
||||||
|
f"(unit {existing.group(2)}))"
|
||||||
|
)
|
||||||
|
proj_start = block.find("(project ")
|
||||||
|
if proj_start != -1:
|
||||||
|
proj_end = _balanced_end(block, proj_start)
|
||||||
|
block = block[:proj_end] + " " + new_entry + block[proj_end:]
|
||||||
|
changed = True
|
||||||
|
result_parts.append(block)
|
||||||
|
pos = end + 1
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
sub_sheet_path.write_text("".join(result_parts), encoding="utf-8")
|
||||||
|
modified_sheets.append(str(sub_sheet_path))
|
||||||
|
logger.info(
|
||||||
|
f"Fixed instances in {sub_sheet_path} for sheet-block {sheet_block_uuid}"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fixing sub-sheet instances: {e}")
|
||||||
|
return modified_sheets
|
||||||
@@ -325,6 +325,7 @@ try:
|
|||||||
from commands.project import ProjectCommands
|
from commands.project import ProjectCommands
|
||||||
from commands.routing import RoutingCommands
|
from commands.routing import RoutingCommands
|
||||||
from commands.schematic import SchematicManager
|
from commands.schematic import SchematicManager
|
||||||
|
from commands.schematic_hierarchy import SchematicHierarchyCommands
|
||||||
from commands.symbol_creator import SymbolCreator
|
from commands.symbol_creator import SymbolCreator
|
||||||
from commands.symbol_pins import SymbolPinCommands
|
from commands.symbol_pins import SymbolPinCommands
|
||||||
|
|
||||||
@@ -447,6 +448,8 @@ class KiCADInterface:
|
|||||||
|
|
||||||
# Symbol pin discovery commands (read-only pin lookup from symbol libraries)
|
# Symbol pin discovery commands (read-only pin lookup from symbol libraries)
|
||||||
self.symbol_pin_commands = SymbolPinCommands()
|
self.symbol_pin_commands = SymbolPinCommands()
|
||||||
|
# Schematic hierarchy commands (insert sheets, scaffold sub-sheets)
|
||||||
|
self.hierarchy_commands = SchematicHierarchyCommands(self)
|
||||||
|
|
||||||
# Initialize JLCPCB API integration
|
# Initialize JLCPCB API integration
|
||||||
self.jlcpcb_client = JLCPCBClient() # Official API (requires auth)
|
self.jlcpcb_client = JLCPCBClient() # Official API (requires auth)
|
||||||
@@ -534,6 +537,9 @@ class KiCADInterface:
|
|||||||
# Symbol pin discovery commands (read pins straight from symbol libraries)
|
# Symbol pin discovery commands (read pins straight from symbol libraries)
|
||||||
"list_symbol_pins": self.symbol_pin_commands.list_symbol_pins,
|
"list_symbol_pins": self.symbol_pin_commands.list_symbol_pins,
|
||||||
"batch_list_symbol_pins": self.symbol_pin_commands.batch_list_symbol_pins,
|
"batch_list_symbol_pins": self.symbol_pin_commands.batch_list_symbol_pins,
|
||||||
|
# Schematic hierarchy commands (sheet insertion + subsheet scaffolding)
|
||||||
|
"add_hierarchical_sheet": self.hierarchy_commands.add_hierarchical_sheet,
|
||||||
|
"create_hierarchical_subsheet": self.hierarchy_commands.create_hierarchical_subsheet,
|
||||||
# JLCPCB API commands (complete parts catalog via API)
|
# JLCPCB API commands (complete parts catalog via API)
|
||||||
"download_jlcpcb_database": self._handle_download_jlcpcb_database,
|
"download_jlcpcb_database": self._handle_download_jlcpcb_database,
|
||||||
"search_jlcpcb_parts": self._handle_search_jlcpcb_parts,
|
"search_jlcpcb_parts": self._handle_search_jlcpcb_parts,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { registerExportTools } from "./tools/export.js";
|
|||||||
import { registerSchematicTools } from "./tools/schematic.js";
|
import { registerSchematicTools } from "./tools/schematic.js";
|
||||||
import { registerLibraryTools } from "./tools/library.js";
|
import { registerLibraryTools } from "./tools/library.js";
|
||||||
import { registerSymbolLibraryTools } from "./tools/library-symbol.js";
|
import { registerSymbolLibraryTools } from "./tools/library-symbol.js";
|
||||||
|
import { registerSchematicHierarchyTools } from "./tools/schematic-hierarchy.js";
|
||||||
import { registerJLCPCBApiTools } from "./tools/jlcpcb-api.js";
|
import { registerJLCPCBApiTools } from "./tools/jlcpcb-api.js";
|
||||||
import { registerDatasheetTools } from "./tools/datasheet.js";
|
import { registerDatasheetTools } from "./tools/datasheet.js";
|
||||||
import { registerFootprintTools } from "./tools/footprint.js";
|
import { registerFootprintTools } from "./tools/footprint.js";
|
||||||
@@ -296,6 +297,7 @@ export class KiCADMcpServer {
|
|||||||
registerSchematicTools(this.server, this.callKicadScript.bind(this));
|
registerSchematicTools(this.server, this.callKicadScript.bind(this));
|
||||||
registerLibraryTools(this.server, this.callKicadScript.bind(this));
|
registerLibraryTools(this.server, this.callKicadScript.bind(this));
|
||||||
registerSymbolLibraryTools(this.server, this.callKicadScript.bind(this));
|
registerSymbolLibraryTools(this.server, this.callKicadScript.bind(this));
|
||||||
|
registerSchematicHierarchyTools(this.server, this.callKicadScript.bind(this));
|
||||||
registerJLCPCBApiTools(this.server, this.callKicadScript.bind(this));
|
registerJLCPCBApiTools(this.server, this.callKicadScript.bind(this));
|
||||||
registerDatasheetTools(this.server, this.callKicadScript.bind(this));
|
registerDatasheetTools(this.server, this.callKicadScript.bind(this));
|
||||||
registerFootprintTools(this.server, this.callKicadScript.bind(this));
|
registerFootprintTools(this.server, this.callKicadScript.bind(this));
|
||||||
|
|||||||
@@ -123,6 +123,11 @@ export const toolCategories: ToolCategory[] = [
|
|||||||
description: "Read a symbol's pins straight from the library (no schematic needed)",
|
description: "Read a symbol's pins straight from the library (no schematic needed)",
|
||||||
tools: ["list_symbol_pins", "batch_list_symbol_pins"],
|
tools: ["list_symbol_pins", "batch_list_symbol_pins"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "schematic_hierarchy",
|
||||||
|
description: "Hierarchical schematic sheets: insert a sheet, scaffold a sub-sheet",
|
||||||
|
tools: ["add_hierarchical_sheet", "create_hierarchical_subsheet"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "routing",
|
name: "routing",
|
||||||
description: "Advanced routing operations: vias, copper pours",
|
description: "Advanced routing operations: vias, copper pours",
|
||||||
|
|||||||
64
src/tools/schematic-hierarchy.ts
Normal file
64
src/tools/schematic-hierarchy.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Schematic hierarchy tools: insert a hierarchical sheet, scaffold a sub-sheet.
|
||||||
|
*/
|
||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export function registerSchematicHierarchyTools(server: McpServer, callKicadScript: Function) {
|
||||||
|
// Link an existing sub-sheet into a parent
|
||||||
|
server.tool(
|
||||||
|
"add_hierarchical_sheet",
|
||||||
|
"Insert a hierarchical-sheet reference block into a parent schematic, pointing at an existing sub-sheet file. Adds the sheet box, name/file fields, a sheet_instances path entry on the next page number, and fixes sub-sheet component instance paths so ERC resolves references.",
|
||||||
|
{
|
||||||
|
schematicPath: z.string().describe("Path to the parent .kicad_sch"),
|
||||||
|
subsheetPath: z.string().describe("Path to the existing sub-sheet .kicad_sch to reference"),
|
||||||
|
sheetName: z.string().optional().default("Sheet").describe("Display name for the sheet"),
|
||||||
|
position: z
|
||||||
|
.object({ x: z.number(), y: z.number() })
|
||||||
|
.optional()
|
||||||
|
.describe("Top-left of the sheet box in mm (default 50,50)"),
|
||||||
|
size: z
|
||||||
|
.object({ width: z.number(), height: z.number() })
|
||||||
|
.optional()
|
||||||
|
.describe("Sheet box size in mm (default 80x50)"),
|
||||||
|
},
|
||||||
|
async (args: any) => {
|
||||||
|
const r = await callKicadScript("add_hierarchical_sheet", args);
|
||||||
|
if (!r.success)
|
||||||
|
return { content: [{ type: "text", text: `Failed: ${r.message || "Unknown error"}` }] };
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Added sheet '${r.sheet_name}' -> ${r.subsheet_path} (page ${r.page})`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a sub-sheet file AND link it in one call
|
||||||
|
server.tool(
|
||||||
|
"create_hierarchical_subsheet",
|
||||||
|
"Create a new sub-sheet .kicad_sch file and link it into a parent schematic in a single call (create_schematic + add_hierarchical_sheet). The fastest way to grow a hierarchical design.",
|
||||||
|
{
|
||||||
|
parentSchematicPath: z.string().describe("Path to the parent .kicad_sch"),
|
||||||
|
subsheetPath: z.string().describe("Path for the new sub-sheet .kicad_sch to create"),
|
||||||
|
sheetName: z.string().optional().default("Sheet").describe("Display name for the sheet"),
|
||||||
|
position: z.object({ x: z.number(), y: z.number() }).optional(),
|
||||||
|
size: z.object({ width: z.number(), height: z.number() }).optional(),
|
||||||
|
metadata: z
|
||||||
|
.record(z.string(), z.any())
|
||||||
|
.optional()
|
||||||
|
.describe("Optional metadata for the new sub-sheet (title, etc.)"),
|
||||||
|
},
|
||||||
|
async (args: any) => {
|
||||||
|
const r = await callKicadScript("create_hierarchical_subsheet", args);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: r.success ? r.message : `Failed: ${r.message || "Unknown error"}` },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
96
tests/test_schematic_hierarchy.py
Normal file
96
tests/test_schematic_hierarchy.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
"""
|
||||||
|
Unit/integration tests for the hierarchy commands (commands/schematic_hierarchy.py).
|
||||||
|
|
||||||
|
The hierarchical-sheet text manipulation is exercised against real files in tmp,
|
||||||
|
and create_hierarchical_subsheet's orchestration is checked with a stub interface.
|
||||||
|
No KiCad needed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent / "python"))
|
||||||
|
|
||||||
|
from commands.schematic_hierarchy import SchematicHierarchyCommands # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def _cmds(iface=None):
|
||||||
|
return SchematicHierarchyCommands(iface or types.SimpleNamespace())
|
||||||
|
|
||||||
|
|
||||||
|
class TestAddHierarchicalSheet:
|
||||||
|
def test_requires_params(self):
|
||||||
|
assert _cmds().add_hierarchical_sheet({"schematicPath": "/x"})["success"] is False
|
||||||
|
|
||||||
|
def test_inserts_sheet_block(self, tmp_path):
|
||||||
|
parent = tmp_path / "top.kicad_sch"
|
||||||
|
parent.write_text(
|
||||||
|
'(kicad_sch (uuid abcd-1234)\n (sheet_instances (path "/" (page "1")))\n)'
|
||||||
|
)
|
||||||
|
r = _cmds().add_hierarchical_sheet(
|
||||||
|
{
|
||||||
|
"schematicPath": str(parent),
|
||||||
|
"subsheetPath": str(tmp_path / "sub.kicad_sch"),
|
||||||
|
"sheetName": "Power",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert r["success"] is True
|
||||||
|
assert r["page"] == 2 # next page after existing "1"
|
||||||
|
content = parent.read_text()
|
||||||
|
assert "(sheet " in content
|
||||||
|
assert '"Sheet name" "Power"' in content
|
||||||
|
assert '"Sheet file" "sub.kicad_sch"' in content
|
||||||
|
# a sheet_instances path entry for the new sheet block was added
|
||||||
|
assert f'/{ "abcd-1234" }/{ r["sheet_uuid"] }' in content
|
||||||
|
|
||||||
|
|
||||||
|
class TestCreateHierarchicalSubsheet:
|
||||||
|
def test_orchestrates(self):
|
||||||
|
iface = types.SimpleNamespace(
|
||||||
|
_handle_create_schematic=lambda p: {
|
||||||
|
"success": True,
|
||||||
|
"file_path": p["filename"],
|
||||||
|
"schematic_uuid": "sub-uuid",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
c = SchematicHierarchyCommands(iface)
|
||||||
|
c.add_hierarchical_sheet = lambda p: {"success": True, "sheet_uuid": "blk", "page": 2}
|
||||||
|
r = c.create_hierarchical_subsheet(
|
||||||
|
{
|
||||||
|
"parentSchematicPath": "/top.kicad_sch",
|
||||||
|
"subsheetPath": "/sub.kicad_sch",
|
||||||
|
"sheetName": "IO",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert r["success"] is True
|
||||||
|
assert r["sheet_block_uuid"] == "blk" and r["page"] == 2
|
||||||
|
|
||||||
|
def test_requires_params(self):
|
||||||
|
assert (
|
||||||
|
_cmds().create_hierarchical_subsheet({"parentSchematicPath": "/x"})["success"] is False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFixSubsheetInstances:
|
||||||
|
def test_adds_path_entry(self, tmp_path):
|
||||||
|
sub = tmp_path / "sub.kicad_sch"
|
||||||
|
sub.write_text(
|
||||||
|
'(kicad_sch (symbol (lib_id "Device:R")'
|
||||||
|
' (instances (project "proj" (path "/old" (reference "R1") (unit 1))))))'
|
||||||
|
)
|
||||||
|
parent = tmp_path / "top.kicad_sch"
|
||||||
|
parent_content = (
|
||||||
|
"(kicad_sch (uuid abcd-1234)"
|
||||||
|
' (sheet (at 50 50) (uuid "sheet-blk-1")'
|
||||||
|
' (property "Sheet name" "Sub" (at 1 1 0))'
|
||||||
|
' (property "Sheet file" "sub.kicad_sch" (at 1 2 0)))'
|
||||||
|
' (sheet_instances (path "/" (page "1"))))'
|
||||||
|
)
|
||||||
|
parent.write_text(parent_content)
|
||||||
|
|
||||||
|
modified = _cmds().fix_subsheet_instances(str(parent), parent_content)
|
||||||
|
assert str(sub) in modified
|
||||||
|
sub_after = sub.read_text()
|
||||||
|
assert "/abcd-1234/sheet-blk-1" in sub_after
|
||||||
|
assert '(reference "R1")' in sub_after
|
||||||
Reference in New Issue
Block a user