Merge pull request #121 from mixelpixx/feat/schematic-text-tools
feat: add add_schematic_text and list_schematic_texts tools
This commit is contained in:
@@ -268,6 +268,45 @@ Checks net label / power symbol positions first (exact IU match), then wire endp
|
||||
| position | `{"x": float, "y": float}` — echoes the query coordinates |
|
||||
| source | `"net_label"` \| `"wire_endpoint"` \| `null` — how the net was resolved |
|
||||
|
||||
## Text Annotations (2 tools)
|
||||
|
||||
### add_schematic_text
|
||||
|
||||
Add a free-form text annotation (note, heading, documentation string) directly on the schematic canvas. Text annotations have no electrical significance — they are purely visual. For electrically meaningful labels, use `add_schematic_net_label` instead.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| ------------- | ------------ | -------- | ------------------------------------------------ |
|
||||
| schematicPath | string | Yes | Path to the .kicad_sch file |
|
||||
| text | string | Yes | Text content to display |
|
||||
| position | array [x, y] | Yes | Position in schematic mm coordinates |
|
||||
| angle | number | No | Rotation angle in degrees (default: 0) |
|
||||
| fontSize | number | No | Font size in mm (default: 1.27 — KiCad standard) |
|
||||
| bold | boolean | No | Bold text (default: false) |
|
||||
| italic | boolean | No | Italic text (default: false) |
|
||||
| justify | string | No | `left` \| `center` \| `right` (default: `left`) |
|
||||
|
||||
### list_schematic_texts
|
||||
|
||||
List all free-form text annotations in the schematic. Optionally filter by a substring of the text content.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| ------------- | ------ | -------- | -------------------------------------------------------------- |
|
||||
| schematicPath | string | Yes | Path to the .kicad_sch file |
|
||||
| text | string | No | Case-insensitive substring filter — only return matching texts |
|
||||
|
||||
**Response fields (per text entry):**
|
||||
|
||||
| Field | Description |
|
||||
| --------- | ----------------------------------- |
|
||||
| text | Text string content |
|
||||
| position | `{"x": float, "y": float}` in mm |
|
||||
| angle | Rotation angle in degrees |
|
||||
| font_size | Font size in mm |
|
||||
| bold | `true` / `false` |
|
||||
| italic | `true` / `false` |
|
||||
| justify | `"left"` \| `"center"` \| `"right"` |
|
||||
| uuid | KiCad UUID of the element |
|
||||
|
||||
## Schematic Creation and Export (6 tools)
|
||||
|
||||
### create_schematic
|
||||
|
||||
@@ -169,6 +169,13 @@ _Source: `src/tools/schematic.ts`_
|
||||
| `list_schematic_wires` | List all wires in schematic | Routed (schematic) |
|
||||
| `list_schematic_labels` | List all net labels | Routed (schematic) |
|
||||
|
||||
### Text Annotations
|
||||
|
||||
| Tool | Description | Access |
|
||||
| ---------------------- | ------------------------------------------------ | ------------------ |
|
||||
| `add_schematic_text` | Add free-form text annotation to schematic | Routed (schematic) |
|
||||
| `list_schematic_texts` | List all text annotations (with optional filter) | Routed (schematic) |
|
||||
|
||||
### Schematic Creation and Export
|
||||
|
||||
| Tool | Description | Access |
|
||||
|
||||
@@ -754,6 +754,128 @@ class WireManager:
|
||||
|
||||
return [start, corner, end]
|
||||
|
||||
@staticmethod
|
||||
def list_texts(schematic_path: Path) -> Optional[List[Any]]:
|
||||
"""Return all free-form text annotations (SCH_TEXT) in a schematic.
|
||||
|
||||
Each entry is a dict with keys: text, position (x/y), angle,
|
||||
font_size, bold, italic, justify, uuid.
|
||||
Returns None on parse error.
|
||||
"""
|
||||
try:
|
||||
with open(schematic_path, "r", encoding="utf-8") as f:
|
||||
sch_data = sexpdata.loads(f.read())
|
||||
|
||||
_SYM_TEXT = Symbol("text")
|
||||
_SYM_EFFECTS = Symbol("effects")
|
||||
_SYM_FONT = Symbol("font")
|
||||
_SYM_SIZE = Symbol("size")
|
||||
_SYM_JUSTIFY = Symbol("justify")
|
||||
_SYM_BOLD = Symbol("bold")
|
||||
_SYM_ITALIC = Symbol("italic")
|
||||
|
||||
results = []
|
||||
for item in sch_data:
|
||||
if not (isinstance(item, list) and len(item) >= 2 and item[0] == _SYM_TEXT):
|
||||
continue
|
||||
# item[1] is the text string
|
||||
text_val = item[1] if len(item) > 1 else ""
|
||||
|
||||
pos_x = pos_y = angle = 0.0
|
||||
font_size = 1.27
|
||||
bold = italic = False
|
||||
justify = "left"
|
||||
uid = ""
|
||||
|
||||
for part in item[2:]:
|
||||
if not isinstance(part, list) or not part:
|
||||
continue
|
||||
tag = part[0]
|
||||
if tag == _SYM_AT and len(part) >= 3:
|
||||
pos_x = float(part[1])
|
||||
pos_y = float(part[2])
|
||||
angle = float(part[3]) if len(part) >= 4 else 0.0
|
||||
elif tag == _SYM_UUID and len(part) >= 2:
|
||||
uid = str(part[1])
|
||||
elif tag == _SYM_EFFECTS:
|
||||
for eff in part[1:]:
|
||||
if not isinstance(eff, list) or not eff:
|
||||
continue
|
||||
if eff[0] == _SYM_FONT:
|
||||
for fp in eff[1:]:
|
||||
if not isinstance(fp, list) or not fp:
|
||||
continue
|
||||
if fp[0] == _SYM_SIZE and len(fp) >= 2:
|
||||
font_size = float(fp[1])
|
||||
elif fp[0] == _SYM_BOLD and len(fp) >= 2:
|
||||
bold = str(fp[1]).lower() == "yes"
|
||||
elif fp[0] == _SYM_ITALIC and len(fp) >= 2:
|
||||
italic = str(fp[1]).lower() == "yes"
|
||||
elif eff[0] == _SYM_JUSTIFY and len(eff) >= 2:
|
||||
justify = str(eff[1])
|
||||
|
||||
results.append(
|
||||
{
|
||||
"text": text_val,
|
||||
"position": {"x": pos_x, "y": pos_y},
|
||||
"angle": angle,
|
||||
"font_size": font_size,
|
||||
"bold": bold,
|
||||
"italic": italic,
|
||||
"justify": justify,
|
||||
"uuid": uid,
|
||||
}
|
||||
)
|
||||
return results
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing texts: {e}")
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def add_text(
|
||||
schematic_path: Path,
|
||||
text: str,
|
||||
position: List[float],
|
||||
angle: float = 0,
|
||||
font_size: float = 1.27,
|
||||
bold: bool = False,
|
||||
italic: bool = False,
|
||||
justify: str = "left",
|
||||
) -> bool:
|
||||
"""Add a free-form text annotation (SCH_TEXT) to a KiCad schematic."""
|
||||
try:
|
||||
text_escaped = text.replace("\\", "\\\\").replace('"', '\\"')
|
||||
uid = str(uuid.uuid4())
|
||||
font_attrs = f"\n\t\t\t\t(size {font_size} {font_size})"
|
||||
if bold:
|
||||
font_attrs += "\n\t\t\t\t(bold yes)"
|
||||
if italic:
|
||||
font_attrs += "\n\t\t\t\t(italic yes)"
|
||||
text_sexp = (
|
||||
f'\t(text "{text_escaped}"\n'
|
||||
f"\t\t(exclude_from_sim no)\n"
|
||||
f"\t\t(at {position[0]} {position[1]} {angle})\n"
|
||||
f"\t\t(effects\n"
|
||||
f"\t\t\t(font{font_attrs}\n"
|
||||
f"\t\t\t)\n"
|
||||
f"\t\t\t(justify {justify} bottom)\n"
|
||||
f"\t\t)\n"
|
||||
f'\t\t(uuid "{uid}")\n'
|
||||
f"\t)\n"
|
||||
)
|
||||
_text_insert(schematic_path, text_sexp)
|
||||
logger.info(f"Added text '{text}' at {position}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding text: {e}")
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def add_hierarchical_label(
|
||||
schematic_path: Path,
|
||||
|
||||
@@ -410,6 +410,8 @@ class KiCADInterface:
|
||||
"list_floating_labels": self._handle_list_floating_labels,
|
||||
"snap_to_grid": self._handle_snap_to_grid,
|
||||
"add_schematic_hierarchical_label": self._handle_add_schematic_hierarchical_label,
|
||||
"add_schematic_text": self._handle_add_schematic_text,
|
||||
"list_schematic_texts": self._handle_list_schematic_texts,
|
||||
"add_sheet_pin": self._handle_add_sheet_pin,
|
||||
"import_svg_logo": self._handle_import_svg_logo,
|
||||
# UI/Process management commands
|
||||
@@ -2731,6 +2733,98 @@ class KiCADInterface:
|
||||
logger.error(traceback.format_exc())
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _handle_list_schematic_texts(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""List all free-form text annotations (SCH_TEXT) in a schematic."""
|
||||
logger.info("Listing schematic text annotations")
|
||||
try:
|
||||
from commands.wire_manager import WireManager
|
||||
|
||||
schematic_path = params.get("schematicPath")
|
||||
if not schematic_path:
|
||||
return {"success": False, "message": "schematicPath is required"}
|
||||
|
||||
sch_file = Path(schematic_path)
|
||||
if not sch_file.exists():
|
||||
return {"success": False, "message": f"Schematic not found: {schematic_path}"}
|
||||
|
||||
texts = WireManager.list_texts(sch_file)
|
||||
if texts is None:
|
||||
return {"success": False, "message": "Failed to parse schematic"}
|
||||
|
||||
# Optional text filter
|
||||
filter_text = params.get("text")
|
||||
if filter_text is not None:
|
||||
texts = [t for t in texts if filter_text.lower() in t["text"].lower()]
|
||||
|
||||
return {"success": True, "texts": texts, "count": len(texts)}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing schematic texts: {e}")
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _handle_add_schematic_text(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add a free-form text annotation (SCH_TEXT) to a schematic."""
|
||||
logger.info("Adding text annotation to schematic")
|
||||
try:
|
||||
from commands.wire_manager import WireManager
|
||||
|
||||
schematic_path = params.get("schematicPath")
|
||||
text = params.get("text")
|
||||
position = params.get("position")
|
||||
angle = params.get("angle", 0)
|
||||
font_size = params.get("fontSize", 1.27)
|
||||
bold = params.get("bold", False)
|
||||
italic = params.get("italic", False)
|
||||
justify = params.get("justify", "left")
|
||||
|
||||
if not schematic_path:
|
||||
return {"success": False, "message": "schematicPath is required"}
|
||||
if not text:
|
||||
return {"success": False, "message": "text is required"}
|
||||
if not position or len(position) != 2:
|
||||
return {"success": False, "message": "position [x, y] is required"}
|
||||
if justify not in ("left", "center", "right"):
|
||||
return {"success": False, "message": "justify must be left, center, or right"}
|
||||
if font_size <= 0:
|
||||
return {"success": False, "message": "fontSize must be positive"}
|
||||
|
||||
sch_file = Path(schematic_path)
|
||||
if not sch_file.exists():
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Schematic not found: {schematic_path}",
|
||||
}
|
||||
|
||||
success = WireManager.add_text(
|
||||
sch_file,
|
||||
text,
|
||||
position,
|
||||
angle=angle,
|
||||
font_size=font_size,
|
||||
bold=bold,
|
||||
italic=italic,
|
||||
justify=justify,
|
||||
)
|
||||
|
||||
if success:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Added text '{text}' at ({position[0]}, {position[1]})",
|
||||
"position": {"x": position[0], "y": position[1]},
|
||||
"angle": angle,
|
||||
}
|
||||
return {"success": False, "message": "Failed to add text annotation"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding schematic text: {e}")
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _handle_add_schematic_hierarchical_label(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add a hierarchical label to a sub-sheet schematic."""
|
||||
logger.info("Adding hierarchical label to schematic")
|
||||
|
||||
@@ -109,6 +109,8 @@ export const toolCategories: ToolCategory[] = [
|
||||
"get_schematic_view",
|
||||
"export_schematic_svg",
|
||||
"export_schematic_pdf",
|
||||
"add_schematic_text",
|
||||
"list_schematic_texts",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1633,6 +1633,114 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
},
|
||||
);
|
||||
|
||||
// List free-form text annotations in schematic
|
||||
server.tool(
|
||||
"list_schematic_texts",
|
||||
"List all free-form text annotations (notes, headings, documentation strings) in the schematic. " +
|
||||
"Returns position, angle, font size, bold/italic flags, and justification for each text element. " +
|
||||
"Optionally filter by a substring match on the text content.",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||
text: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Case-insensitive substring filter — only return texts containing this string"),
|
||||
},
|
||||
async (args: { schematicPath: string; text?: string }) => {
|
||||
const result = await callKicadScript("list_schematic_texts", args);
|
||||
if (result.success) {
|
||||
const texts = result.texts || [];
|
||||
if (texts.length === 0) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: "No text annotations found in schematic." }],
|
||||
};
|
||||
}
|
||||
const lines = texts.map(
|
||||
(t: any) =>
|
||||
` "${t.text}" at (${t.position.x}, ${t.position.y})` +
|
||||
(t.angle ? ` angle=${t.angle}` : "") +
|
||||
` size=${t.font_size}` +
|
||||
(t.bold ? " bold" : "") +
|
||||
(t.italic ? " italic" : "") +
|
||||
` justify=${t.justify}`,
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: `Text annotations (${texts.length}):\n${lines.join("\n")}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: `Failed to list text annotations: ${result.message || "Unknown error"}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Add free-form text annotation to schematic
|
||||
server.tool(
|
||||
"add_schematic_text",
|
||||
"Add a free-form text annotation to the schematic. " +
|
||||
"Use this to add notes, labels, section headings, or documentation strings " +
|
||||
"directly on the schematic canvas. Unlike net labels, text annotations have " +
|
||||
"no electrical significance.",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||
text: z.string().describe("Text content to display"),
|
||||
position: z
|
||||
.array(z.number())
|
||||
.length(2)
|
||||
.describe("Position [x, y] in schematic mm coordinates"),
|
||||
angle: z.number().optional().describe("Rotation angle in degrees (default: 0)"),
|
||||
fontSize: z.number().optional().describe("Font size in mm (default: 1.27)"),
|
||||
bold: z.boolean().optional().describe("Bold text (default: false)"),
|
||||
italic: z.boolean().optional().describe("Italic text (default: false)"),
|
||||
justify: z
|
||||
.enum(["left", "center", "right"])
|
||||
.optional()
|
||||
.describe("Horizontal text justification (default: left)"),
|
||||
},
|
||||
async (args: {
|
||||
schematicPath: string;
|
||||
text: string;
|
||||
position: number[];
|
||||
angle?: number;
|
||||
fontSize?: number;
|
||||
bold?: boolean;
|
||||
italic?: boolean;
|
||||
justify?: "left" | "center" | "right";
|
||||
}) => {
|
||||
const result = await callKicadScript("add_schematic_text", args);
|
||||
if (result.success) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: result.message || "Text annotation added successfully",
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: `Failed to add text annotation: ${result.message || "Unknown error"}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Add sheet pin to a sheet block on the parent schematic
|
||||
server.tool(
|
||||
"add_sheet_pin",
|
||||
|
||||
406
tests/test_add_schematic_text.py
Normal file
406
tests/test_add_schematic_text.py
Normal file
@@ -0,0 +1,406 @@
|
||||
"""
|
||||
Tests for the add_schematic_text tool.
|
||||
|
||||
Covers:
|
||||
- WireManager.add_text S-expression insertion
|
||||
- Correct position, angle, font options, justification
|
||||
- String escaping (double quotes in text)
|
||||
- Parameter validation in _handle_add_schematic_text
|
||||
"""
|
||||
|
||||
import sys
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import sexpdata
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "python"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_iface() -> Any:
|
||||
with patch("kicad_interface.USE_IPC_BACKEND", False):
|
||||
from kicad_interface import KiCADInterface
|
||||
|
||||
iface = KiCADInterface.__new__(KiCADInterface)
|
||||
return iface
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def iface():
|
||||
return _make_iface()
|
||||
|
||||
|
||||
_MINIMAL_SCH = textwrap.dedent("""\
|
||||
(kicad_sch (version 20250114) (generator "test")
|
||||
\t(uuid aaaaaaaa-0000-0000-0000-000000000001)
|
||||
\t(paper "A4")
|
||||
\t(sheet_instances (path "/" (page "1")))
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WireManager.add_text unit tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestWireManagerAddText:
|
||||
def test_inserts_text_element(self, tmp_path):
|
||||
from commands.wire_manager import WireManager
|
||||
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
|
||||
result = WireManager.add_text(sch, "Test Note", [50.0, 40.0])
|
||||
|
||||
assert result is True
|
||||
content = sch.read_text(encoding="utf-8")
|
||||
assert '(text "Test Note"' in content
|
||||
assert "(at 50.0 40.0 0)" in content
|
||||
|
||||
def test_inserts_before_sheet_instances(self, tmp_path):
|
||||
from commands.wire_manager import WireManager
|
||||
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
|
||||
WireManager.add_text(sch, "Section A", [10.0, 20.0])
|
||||
|
||||
content = sch.read_text(encoding="utf-8")
|
||||
text_pos = content.find('(text "Section A"')
|
||||
instances_pos = content.find("(sheet_instances")
|
||||
assert text_pos < instances_pos
|
||||
|
||||
def test_rotation_angle(self, tmp_path):
|
||||
from commands.wire_manager import WireManager
|
||||
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
|
||||
WireManager.add_text(sch, "Rotated", [20.0, 20.0], angle=90)
|
||||
|
||||
content = sch.read_text(encoding="utf-8")
|
||||
assert "(at 20.0 20.0 90)" in content
|
||||
|
||||
def test_bold_and_italic_flags(self, tmp_path):
|
||||
from commands.wire_manager import WireManager
|
||||
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
|
||||
WireManager.add_text(sch, "Bold Italic", [30.0, 30.0], bold=True, italic=True)
|
||||
|
||||
content = sch.read_text(encoding="utf-8")
|
||||
assert "(bold yes)" in content
|
||||
assert "(italic yes)" in content
|
||||
|
||||
def test_center_justification(self, tmp_path):
|
||||
from commands.wire_manager import WireManager
|
||||
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
|
||||
WireManager.add_text(sch, "Centered", [50.0, 50.0], justify="center")
|
||||
|
||||
content = sch.read_text(encoding="utf-8")
|
||||
assert "(justify center bottom)" in content
|
||||
|
||||
def test_custom_font_size(self, tmp_path):
|
||||
from commands.wire_manager import WireManager
|
||||
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
|
||||
WireManager.add_text(sch, "Big Text", [10.0, 10.0], font_size=2.54)
|
||||
|
||||
content = sch.read_text(encoding="utf-8")
|
||||
assert "(size 2.54 2.54)" in content
|
||||
|
||||
def test_escapes_double_quotes_in_text(self, tmp_path):
|
||||
from commands.wire_manager import WireManager
|
||||
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
|
||||
WireManager.add_text(sch, 'He said "hello"', [10.0, 10.0])
|
||||
|
||||
content = sch.read_text(encoding="utf-8")
|
||||
assert r"He said \"hello\"" in content
|
||||
|
||||
def test_result_is_valid_sexp(self, tmp_path):
|
||||
from commands.wire_manager import WireManager
|
||||
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
|
||||
WireManager.add_text(sch, "Note", [10.0, 10.0])
|
||||
|
||||
# Must parse without error
|
||||
sexpdata.loads(sch.read_text(encoding="utf-8"))
|
||||
|
||||
def test_no_bold_italic_by_default(self, tmp_path):
|
||||
from commands.wire_manager import WireManager
|
||||
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
|
||||
WireManager.add_text(sch, "Plain", [10.0, 10.0])
|
||||
|
||||
content = sch.read_text(encoding="utf-8")
|
||||
assert "(bold yes)" not in content
|
||||
assert "(italic yes)" not in content
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _handle_add_schematic_text parameter-validation tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestHandleAddSchematicText:
|
||||
def test_missing_schematic_path(self, iface):
|
||||
result = iface._handle_add_schematic_text({"text": "Note", "position": [10.0, 20.0]})
|
||||
assert result["success"] is False
|
||||
assert "schematicPath" in result["message"]
|
||||
|
||||
def test_missing_text(self, iface, tmp_path):
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
result = iface._handle_add_schematic_text(
|
||||
{"schematicPath": str(sch), "position": [10.0, 20.0]}
|
||||
)
|
||||
assert result["success"] is False
|
||||
assert "text" in result["message"]
|
||||
|
||||
def test_missing_position(self, iface, tmp_path):
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
result = iface._handle_add_schematic_text({"schematicPath": str(sch), "text": "Note"})
|
||||
assert result["success"] is False
|
||||
assert "position" in result["message"]
|
||||
|
||||
def test_invalid_justify(self, iface, tmp_path):
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
result = iface._handle_add_schematic_text(
|
||||
{
|
||||
"schematicPath": str(sch),
|
||||
"text": "Note",
|
||||
"position": [10.0, 20.0],
|
||||
"justify": "top",
|
||||
}
|
||||
)
|
||||
assert result["success"] is False
|
||||
assert "justify" in result["message"]
|
||||
|
||||
def test_invalid_font_size(self, iface, tmp_path):
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
result = iface._handle_add_schematic_text(
|
||||
{
|
||||
"schematicPath": str(sch),
|
||||
"text": "Note",
|
||||
"position": [10.0, 20.0],
|
||||
"fontSize": 0,
|
||||
}
|
||||
)
|
||||
assert result["success"] is False
|
||||
assert "fontSize" in result["message"]
|
||||
|
||||
def test_nonexistent_file(self, iface, tmp_path):
|
||||
result = iface._handle_add_schematic_text(
|
||||
{
|
||||
"schematicPath": str(tmp_path / "nope.kicad_sch"),
|
||||
"text": "Note",
|
||||
"position": [10, 20],
|
||||
}
|
||||
)
|
||||
assert result["success"] is False
|
||||
assert "not found" in result["message"].lower()
|
||||
|
||||
def test_success(self, iface, tmp_path):
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
result = iface._handle_add_schematic_text(
|
||||
{
|
||||
"schematicPath": str(sch),
|
||||
"text": "Power Section",
|
||||
"position": [25.4, 50.8],
|
||||
"angle": 0,
|
||||
"fontSize": 1.5,
|
||||
}
|
||||
)
|
||||
assert result["success"] is True
|
||||
assert "position" in result
|
||||
content = sch.read_text(encoding="utf-8")
|
||||
assert '(text "Power Section"' in content
|
||||
|
||||
def test_success_returns_position(self, iface, tmp_path):
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
result = iface._handle_add_schematic_text(
|
||||
{"schematicPath": str(sch), "text": "Note", "position": [12.7, 25.4]}
|
||||
)
|
||||
assert result["success"] is True
|
||||
assert result["position"] == {"x": 12.7, "y": 25.4}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WireManager.list_texts unit tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestWireManagerListTexts:
|
||||
def test_empty_schematic_returns_empty_list(self, tmp_path):
|
||||
from commands.wire_manager import WireManager
|
||||
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
|
||||
result = WireManager.list_texts(sch)
|
||||
|
||||
assert result == []
|
||||
|
||||
def test_lists_added_text(self, tmp_path):
|
||||
from commands.wire_manager import WireManager
|
||||
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
WireManager.add_text(sch, "Power Section", [10.0, 20.0])
|
||||
|
||||
result = WireManager.list_texts(sch)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["text"] == "Power Section"
|
||||
assert result[0]["position"] == {"x": 10.0, "y": 20.0}
|
||||
|
||||
def test_lists_multiple_texts(self, tmp_path):
|
||||
from commands.wire_manager import WireManager
|
||||
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
WireManager.add_text(sch, "Alpha", [0.0, 0.0])
|
||||
WireManager.add_text(sch, "Beta", [10.0, 10.0])
|
||||
|
||||
result = WireManager.list_texts(sch)
|
||||
|
||||
texts = {r["text"] for r in result}
|
||||
assert texts == {"Alpha", "Beta"}
|
||||
|
||||
def test_angle_is_preserved(self, tmp_path):
|
||||
from commands.wire_manager import WireManager
|
||||
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
WireManager.add_text(sch, "Rotated", [5.0, 5.0], angle=90)
|
||||
|
||||
result = WireManager.list_texts(sch)
|
||||
|
||||
assert result[0]["angle"] == 90.0
|
||||
|
||||
def test_bold_italic_are_preserved(self, tmp_path):
|
||||
from commands.wire_manager import WireManager
|
||||
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
WireManager.add_text(sch, "Styled", [5.0, 5.0], bold=True, italic=True)
|
||||
|
||||
result = WireManager.list_texts(sch)
|
||||
|
||||
assert result[0]["bold"] is True
|
||||
assert result[0]["italic"] is True
|
||||
|
||||
def test_font_size_is_preserved(self, tmp_path):
|
||||
from commands.wire_manager import WireManager
|
||||
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
WireManager.add_text(sch, "Big", [5.0, 5.0], font_size=2.54)
|
||||
|
||||
result = WireManager.list_texts(sch)
|
||||
|
||||
assert result[0]["font_size"] == 2.54
|
||||
|
||||
def test_nonexistent_file_returns_none(self, tmp_path):
|
||||
from commands.wire_manager import WireManager
|
||||
|
||||
result = WireManager.list_texts(tmp_path / "nope.kicad_sch")
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _handle_list_schematic_texts handler tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestHandleListSchematicTexts:
|
||||
def test_missing_schematic_path(self, iface):
|
||||
result = iface._handle_list_schematic_texts({})
|
||||
assert result["success"] is False
|
||||
assert "schematicPath" in result["message"]
|
||||
|
||||
def test_nonexistent_file(self, iface, tmp_path):
|
||||
result = iface._handle_list_schematic_texts(
|
||||
{"schematicPath": str(tmp_path / "nope.kicad_sch")}
|
||||
)
|
||||
assert result["success"] is False
|
||||
assert "not found" in result["message"].lower()
|
||||
|
||||
def test_empty_schematic(self, iface, tmp_path):
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
result = iface._handle_list_schematic_texts({"schematicPath": str(sch)})
|
||||
assert result["success"] is True
|
||||
assert result["texts"] == []
|
||||
assert result["count"] == 0
|
||||
|
||||
def test_returns_all_texts(self, iface, tmp_path):
|
||||
from commands.wire_manager import WireManager
|
||||
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
WireManager.add_text(sch, "Note A", [0.0, 0.0])
|
||||
WireManager.add_text(sch, "Note B", [10.0, 10.0])
|
||||
|
||||
result = iface._handle_list_schematic_texts({"schematicPath": str(sch)})
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["count"] == 2
|
||||
|
||||
def test_text_filter_substring_match(self, iface, tmp_path):
|
||||
from commands.wire_manager import WireManager
|
||||
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
WireManager.add_text(sch, "Power Supply", [0.0, 0.0])
|
||||
WireManager.add_text(sch, "Ground Plane", [10.0, 10.0])
|
||||
|
||||
result = iface._handle_list_schematic_texts({"schematicPath": str(sch), "text": "power"})
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["count"] == 1
|
||||
assert result["texts"][0]["text"] == "Power Supply"
|
||||
|
||||
def test_text_filter_case_insensitive(self, iface, tmp_path):
|
||||
from commands.wire_manager import WireManager
|
||||
|
||||
sch = tmp_path / "test.kicad_sch"
|
||||
sch.write_text(_MINIMAL_SCH, encoding="utf-8")
|
||||
WireManager.add_text(sch, "SECTION HEADER", [0.0, 0.0])
|
||||
|
||||
result = iface._handle_list_schematic_texts({"schematicPath": str(sch), "text": "section"})
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["count"] == 1
|
||||
Reference in New Issue
Block a user