kicad-sch-api 0.3.4__py3-none-any.whl → 0.3.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kicad-sch-api might be problematic. Click here for more details.
- kicad_sch_api/collections/__init__.py +21 -0
- kicad_sch_api/collections/base.py +296 -0
- kicad_sch_api/collections/components.py +422 -0
- kicad_sch_api/collections/junctions.py +378 -0
- kicad_sch_api/collections/labels.py +412 -0
- kicad_sch_api/collections/wires.py +407 -0
- kicad_sch_api/core/labels.py +348 -0
- kicad_sch_api/core/nets.py +310 -0
- kicad_sch_api/core/no_connects.py +274 -0
- kicad_sch_api/core/schematic.py +136 -2
- kicad_sch_api/core/texts.py +343 -0
- kicad_sch_api/core/types.py +12 -0
- kicad_sch_api/geometry/symbol_bbox.py +26 -32
- kicad_sch_api/interfaces/__init__.py +17 -0
- kicad_sch_api/interfaces/parser.py +76 -0
- kicad_sch_api/interfaces/repository.py +70 -0
- kicad_sch_api/interfaces/resolver.py +117 -0
- kicad_sch_api/parsers/__init__.py +14 -0
- kicad_sch_api/parsers/base.py +148 -0
- kicad_sch_api/parsers/label_parser.py +254 -0
- kicad_sch_api/parsers/registry.py +153 -0
- kicad_sch_api/parsers/symbol_parser.py +227 -0
- kicad_sch_api/parsers/wire_parser.py +99 -0
- kicad_sch_api/symbols/__init__.py +18 -0
- kicad_sch_api/symbols/cache.py +470 -0
- kicad_sch_api/symbols/resolver.py +367 -0
- kicad_sch_api/symbols/validators.py +453 -0
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.3.5.dist-info}/METADATA +1 -1
- kicad_sch_api-0.3.5.dist-info/RECORD +58 -0
- kicad_sch_api-0.3.4.dist-info/RECORD +0 -34
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.3.5.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.3.5.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.3.5.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.3.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Repository interfaces for schematic data persistence.
|
|
3
|
+
|
|
4
|
+
These interfaces define the contract for loading and saving schematic data,
|
|
5
|
+
abstracting away the specific file format and storage mechanisms.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, Protocol, Union
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ISchematicRepository(Protocol):
|
|
14
|
+
"""Interface for schematic data persistence operations."""
|
|
15
|
+
|
|
16
|
+
def load(self, filepath: Union[str, Path]) -> Dict[str, Any]:
|
|
17
|
+
"""
|
|
18
|
+
Load schematic data from a file.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
filepath: Path to the schematic file
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Complete schematic data structure
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
FileNotFoundError: If file doesn't exist
|
|
28
|
+
RepositoryError: If file cannot be loaded
|
|
29
|
+
"""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
def save(self, data: Dict[str, Any], filepath: Union[str, Path]) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Save schematic data to a file.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
data: Complete schematic data structure
|
|
38
|
+
filepath: Path where to save the file
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
RepositoryError: If file cannot be saved
|
|
42
|
+
"""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
def exists(self, filepath: Union[str, Path]) -> bool:
|
|
46
|
+
"""
|
|
47
|
+
Check if a schematic file exists.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
filepath: Path to check
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
True if file exists and is accessible
|
|
54
|
+
"""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
def validate_format(self, filepath: Union[str, Path]) -> bool:
|
|
58
|
+
"""
|
|
59
|
+
Validate that a file is in the correct format.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
filepath: Path to the file to validate
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
True if file format is valid
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
FileNotFoundError: If file doesn't exist
|
|
69
|
+
"""
|
|
70
|
+
...
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Symbol resolver interfaces for symbol library operations.
|
|
3
|
+
|
|
4
|
+
These interfaces define the contract for resolving symbols from libraries,
|
|
5
|
+
handling inheritance chains, and managing symbol caching.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import Any, Dict, List, Optional, Protocol
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ISymbolResolver(Protocol):
|
|
13
|
+
"""Interface for symbol resolution and inheritance operations."""
|
|
14
|
+
|
|
15
|
+
def resolve_symbol(self, lib_id: str) -> Optional[Dict[str, Any]]:
|
|
16
|
+
"""
|
|
17
|
+
Resolve a symbol with full inheritance chain applied.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
lib_id: Library identifier (e.g., "Device:R")
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Fully resolved symbol data with inheritance applied,
|
|
24
|
+
or None if symbol not found
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
SymbolError: If symbol resolution fails
|
|
28
|
+
"""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
def get_symbol_raw(self, lib_id: str) -> Optional[Dict[str, Any]]:
|
|
32
|
+
"""
|
|
33
|
+
Get raw symbol data without inheritance resolution.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
lib_id: Library identifier
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Raw symbol data as stored in library,
|
|
40
|
+
or None if symbol not found
|
|
41
|
+
"""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
def resolve_inheritance_chain(self, lib_id: str) -> List[str]:
|
|
45
|
+
"""
|
|
46
|
+
Get the complete inheritance chain for a symbol.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
lib_id: Library identifier
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
List of lib_ids in inheritance order (base to derived)
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
SymbolError: If circular inheritance detected
|
|
56
|
+
"""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
def is_symbol_available(self, lib_id: str) -> bool:
|
|
60
|
+
"""
|
|
61
|
+
Check if a symbol is available in the libraries.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
lib_id: Library identifier
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
True if symbol exists and can be resolved
|
|
68
|
+
"""
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
def clear_cache(self) -> None:
|
|
72
|
+
"""Clear any cached symbol data."""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ISymbolCache(Protocol):
|
|
77
|
+
"""Interface for symbol library caching operations."""
|
|
78
|
+
|
|
79
|
+
def get_symbol(self, lib_id: str) -> Optional[Dict[str, Any]]:
|
|
80
|
+
"""
|
|
81
|
+
Get a symbol from cache or load from library.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
lib_id: Library identifier
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Symbol data or None if not found
|
|
88
|
+
"""
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
def cache_symbol(self, lib_id: str, symbol_data: Dict[str, Any]) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Cache symbol data.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
lib_id: Library identifier
|
|
97
|
+
symbol_data: Symbol data to cache
|
|
98
|
+
"""
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
def invalidate(self, lib_id: Optional[str] = None) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Invalidate cache entries.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
lib_id: Specific symbol to invalidate, or None for all
|
|
107
|
+
"""
|
|
108
|
+
...
|
|
109
|
+
|
|
110
|
+
def get_cache_stats(self) -> Dict[str, Any]:
|
|
111
|
+
"""
|
|
112
|
+
Get cache statistics.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Dictionary with cache statistics (hits, misses, size, etc.)
|
|
116
|
+
"""
|
|
117
|
+
...
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modular S-expression parsers for KiCAD elements.
|
|
3
|
+
|
|
4
|
+
This package provides specialized parsers for different types of KiCAD
|
|
5
|
+
S-expression elements, organized by responsibility and testable in isolation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .registry import ElementParserRegistry
|
|
9
|
+
from .base import BaseElementParser
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ElementParserRegistry",
|
|
13
|
+
"BaseElementParser",
|
|
14
|
+
]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base parser implementation for S-expression elements.
|
|
3
|
+
|
|
4
|
+
Provides common functionality and utilities for all element parsers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from ..interfaces.parser import IElementParser
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BaseElementParser(IElementParser):
|
|
16
|
+
"""Base implementation for S-expression element parsers."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, element_type: str):
|
|
19
|
+
"""
|
|
20
|
+
Initialize base parser.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
element_type: The S-expression element type this parser handles
|
|
24
|
+
"""
|
|
25
|
+
self.element_type = element_type
|
|
26
|
+
self._logger = logger.getChild(self.__class__.__name__)
|
|
27
|
+
|
|
28
|
+
def can_parse(self, element: List[Any]) -> bool:
|
|
29
|
+
"""Check if this parser can handle the given element type."""
|
|
30
|
+
if not element or not isinstance(element, list):
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
element_type = element[0] if element else None
|
|
34
|
+
# Convert sexpdata.Symbol to string for comparison
|
|
35
|
+
element_type_str = str(element_type) if element_type else None
|
|
36
|
+
return element_type_str == self.element_type
|
|
37
|
+
|
|
38
|
+
def parse(self, element: List[Any]) -> Optional[Dict[str, Any]]:
|
|
39
|
+
"""
|
|
40
|
+
Parse an S-expression element with error handling.
|
|
41
|
+
|
|
42
|
+
This method provides common error handling and validation,
|
|
43
|
+
then delegates to the specific parse_element implementation.
|
|
44
|
+
"""
|
|
45
|
+
if not self.can_parse(element):
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
result = self.parse_element(element)
|
|
50
|
+
if result is not None:
|
|
51
|
+
self._logger.debug(f"Successfully parsed {self.element_type} element")
|
|
52
|
+
return result
|
|
53
|
+
except Exception as e:
|
|
54
|
+
self._logger.error(f"Failed to parse {self.element_type} element: {e}")
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def parse_element(self, element: List[Any]) -> Optional[Dict[str, Any]]:
|
|
58
|
+
"""
|
|
59
|
+
Parse the specific element type.
|
|
60
|
+
|
|
61
|
+
This method should be implemented by subclasses to handle
|
|
62
|
+
the specific parsing logic for their element type.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
element: S-expression element to parse
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Parsed element data or None if parsing failed
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
NotImplementedError: If not implemented by subclass
|
|
72
|
+
"""
|
|
73
|
+
raise NotImplementedError(f"parse_element not implemented for {self.element_type}")
|
|
74
|
+
|
|
75
|
+
def _extract_position(self, element: List[Any]) -> Optional[Dict[str, float]]:
|
|
76
|
+
"""
|
|
77
|
+
Extract position information from common S-expression formats.
|
|
78
|
+
|
|
79
|
+
Many KiCAD elements have position info in formats like:
|
|
80
|
+
(at x y [angle])
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
element: S-expression sub-element containing position
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Dictionary with x, y, and optionally angle, or None if not found
|
|
87
|
+
"""
|
|
88
|
+
if not isinstance(element, list) or len(element) < 3:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
if element[0] != "at":
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
result = {
|
|
96
|
+
"x": float(element[1]),
|
|
97
|
+
"y": float(element[2])
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Optional angle parameter
|
|
101
|
+
if len(element) > 3:
|
|
102
|
+
result["angle"] = float(element[3])
|
|
103
|
+
|
|
104
|
+
return result
|
|
105
|
+
except (ValueError, IndexError):
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
def _extract_property_list(self, elements: List[Any], property_name: str) -> List[Dict[str, Any]]:
|
|
109
|
+
"""
|
|
110
|
+
Extract all instances of a property from a list of elements.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
elements: List of S-expression elements
|
|
114
|
+
property_name: Name of property to extract
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List of parsed property dictionaries
|
|
118
|
+
"""
|
|
119
|
+
properties = []
|
|
120
|
+
for element in elements:
|
|
121
|
+
if (isinstance(element, list) and
|
|
122
|
+
len(element) > 0 and
|
|
123
|
+
element[0] == property_name):
|
|
124
|
+
prop = self._parse_property_element(element)
|
|
125
|
+
if prop:
|
|
126
|
+
properties.append(prop)
|
|
127
|
+
return properties
|
|
128
|
+
|
|
129
|
+
def _parse_property_element(self, element: List[Any]) -> Optional[Dict[str, Any]]:
|
|
130
|
+
"""
|
|
131
|
+
Parse a generic property element.
|
|
132
|
+
|
|
133
|
+
Override this method in subclasses for property-specific parsing.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
element: S-expression property element
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Parsed property data or None
|
|
140
|
+
"""
|
|
141
|
+
if len(element) < 2:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
"type": element[0],
|
|
146
|
+
"value": element[1] if len(element) > 1 else None,
|
|
147
|
+
"raw": element
|
|
148
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Label element parser for S-expression label definitions.
|
|
3
|
+
|
|
4
|
+
Handles parsing of text labels, hierarchical labels, and other text elements.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
import sexpdata
|
|
11
|
+
|
|
12
|
+
from .base import BaseElementParser
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LabelParser(BaseElementParser):
|
|
18
|
+
"""Parser for label S-expression elements."""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
"""Initialize label parser."""
|
|
22
|
+
super().__init__("label")
|
|
23
|
+
|
|
24
|
+
def parse_element(self, element: List[Any]) -> Optional[Dict[str, Any]]:
|
|
25
|
+
"""
|
|
26
|
+
Parse a label S-expression element.
|
|
27
|
+
|
|
28
|
+
Expected format:
|
|
29
|
+
(label "text" (at x y angle) (effects (font (size s s))))
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
element: Label S-expression element
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Parsed label data with text, position, and formatting
|
|
36
|
+
"""
|
|
37
|
+
if len(element) < 2:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
label_data = {
|
|
41
|
+
"text": str(element[1]),
|
|
42
|
+
"position": {"x": 0, "y": 0, "angle": 0},
|
|
43
|
+
"effects": {
|
|
44
|
+
"font_size": 1.27,
|
|
45
|
+
"font_thickness": 0.15,
|
|
46
|
+
"bold": False,
|
|
47
|
+
"italic": False,
|
|
48
|
+
"hide": False,
|
|
49
|
+
"justify": []
|
|
50
|
+
},
|
|
51
|
+
"uuid": None
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for elem in element[2:]:
|
|
55
|
+
if not isinstance(elem, list):
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
59
|
+
|
|
60
|
+
if elem_type == "at":
|
|
61
|
+
self._parse_position(elem, label_data)
|
|
62
|
+
elif elem_type == "effects":
|
|
63
|
+
label_data["effects"] = self._parse_effects(elem)
|
|
64
|
+
elif elem_type == "uuid":
|
|
65
|
+
label_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
66
|
+
|
|
67
|
+
return label_data
|
|
68
|
+
|
|
69
|
+
def _parse_position(self, at_element: List[Any], label_data: Dict[str, Any]) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Parse position from at element.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
at_element: (at x y [angle])
|
|
75
|
+
label_data: Label data dictionary to update
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
if len(at_element) >= 3:
|
|
79
|
+
label_data["position"]["x"] = float(at_element[1])
|
|
80
|
+
label_data["position"]["y"] = float(at_element[2])
|
|
81
|
+
if len(at_element) >= 4:
|
|
82
|
+
label_data["position"]["angle"] = float(at_element[3])
|
|
83
|
+
except (ValueError, IndexError) as e:
|
|
84
|
+
self._logger.warning(f"Invalid position coordinates: {at_element}, error: {e}")
|
|
85
|
+
|
|
86
|
+
def _parse_effects(self, effects_element: List[Any]) -> Dict[str, Any]:
|
|
87
|
+
"""
|
|
88
|
+
Parse effects element for text formatting.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
effects_element: (effects (font ...) (justify ...) ...)
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Parsed effects data
|
|
95
|
+
"""
|
|
96
|
+
effects = {
|
|
97
|
+
"font_size": 1.27,
|
|
98
|
+
"font_thickness": 0.15,
|
|
99
|
+
"bold": False,
|
|
100
|
+
"italic": False,
|
|
101
|
+
"hide": False,
|
|
102
|
+
"justify": []
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for elem in effects_element[1:]:
|
|
106
|
+
if isinstance(elem, list) and len(elem) > 0:
|
|
107
|
+
elem_type = str(elem[0])
|
|
108
|
+
if elem_type == "font":
|
|
109
|
+
self._parse_font(elem, effects)
|
|
110
|
+
elif elem_type == "justify":
|
|
111
|
+
effects["justify"] = [str(j) for j in elem[1:]]
|
|
112
|
+
elif elem_type == "hide":
|
|
113
|
+
effects["hide"] = True
|
|
114
|
+
|
|
115
|
+
return effects
|
|
116
|
+
|
|
117
|
+
def _parse_font(self, font_element: List[Any], effects: Dict[str, Any]) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Parse font element within effects.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
font_element: (font (size w h) (thickness t) ...)
|
|
123
|
+
effects: Effects dictionary to update
|
|
124
|
+
"""
|
|
125
|
+
for elem in font_element[1:]:
|
|
126
|
+
if isinstance(elem, list) and len(elem) > 0:
|
|
127
|
+
elem_type = str(elem[0])
|
|
128
|
+
if elem_type == "size":
|
|
129
|
+
try:
|
|
130
|
+
if len(elem) >= 3:
|
|
131
|
+
# Usually (size width height), use width for font_size
|
|
132
|
+
effects["font_size"] = float(elem[1])
|
|
133
|
+
except (ValueError, IndexError):
|
|
134
|
+
pass
|
|
135
|
+
elif elem_type == "thickness":
|
|
136
|
+
try:
|
|
137
|
+
effects["font_thickness"] = float(elem[1])
|
|
138
|
+
except (ValueError, IndexError):
|
|
139
|
+
pass
|
|
140
|
+
elif elem_type == "bold":
|
|
141
|
+
effects["bold"] = True
|
|
142
|
+
elif elem_type == "italic":
|
|
143
|
+
effects["italic"] = True
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class HierarchicalLabelParser(BaseElementParser):
|
|
147
|
+
"""Parser for hierarchical label S-expression elements."""
|
|
148
|
+
|
|
149
|
+
def __init__(self):
|
|
150
|
+
"""Initialize hierarchical label parser."""
|
|
151
|
+
super().__init__("hierarchical_label")
|
|
152
|
+
|
|
153
|
+
def parse_element(self, element: List[Any]) -> Optional[Dict[str, Any]]:
|
|
154
|
+
"""
|
|
155
|
+
Parse a hierarchical label S-expression element.
|
|
156
|
+
|
|
157
|
+
Expected format:
|
|
158
|
+
(hierarchical_label "text" (shape input) (at x y angle) ...)
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
element: Hierarchical label S-expression element
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Parsed hierarchical label data
|
|
165
|
+
"""
|
|
166
|
+
if len(element) < 2:
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
label_data = {
|
|
170
|
+
"text": str(element[1]),
|
|
171
|
+
"shape": "input", # Default shape
|
|
172
|
+
"position": {"x": 0, "y": 0, "angle": 0},
|
|
173
|
+
"effects": {
|
|
174
|
+
"font_size": 1.27,
|
|
175
|
+
"font_thickness": 0.15,
|
|
176
|
+
"bold": False,
|
|
177
|
+
"italic": False,
|
|
178
|
+
"hide": False,
|
|
179
|
+
"justify": []
|
|
180
|
+
},
|
|
181
|
+
"uuid": None
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for elem in element[2:]:
|
|
185
|
+
if not isinstance(elem, list):
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
189
|
+
|
|
190
|
+
if elem_type == "shape":
|
|
191
|
+
label_data["shape"] = str(elem[1]) if len(elem) > 1 else "input"
|
|
192
|
+
elif elem_type == "at":
|
|
193
|
+
self._parse_position(elem, label_data)
|
|
194
|
+
elif elem_type == "effects":
|
|
195
|
+
label_data["effects"] = self._parse_effects(elem)
|
|
196
|
+
elif elem_type == "uuid":
|
|
197
|
+
label_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
198
|
+
|
|
199
|
+
return label_data
|
|
200
|
+
|
|
201
|
+
def _parse_position(self, at_element: List[Any], label_data: Dict[str, Any]) -> None:
|
|
202
|
+
"""Parse position from at element."""
|
|
203
|
+
try:
|
|
204
|
+
if len(at_element) >= 3:
|
|
205
|
+
label_data["position"]["x"] = float(at_element[1])
|
|
206
|
+
label_data["position"]["y"] = float(at_element[2])
|
|
207
|
+
if len(at_element) >= 4:
|
|
208
|
+
label_data["position"]["angle"] = float(at_element[3])
|
|
209
|
+
except (ValueError, IndexError) as e:
|
|
210
|
+
self._logger.warning(f"Invalid position coordinates: {at_element}, error: {e}")
|
|
211
|
+
|
|
212
|
+
def _parse_effects(self, effects_element: List[Any]) -> Dict[str, Any]:
|
|
213
|
+
"""Parse effects element for text formatting."""
|
|
214
|
+
effects = {
|
|
215
|
+
"font_size": 1.27,
|
|
216
|
+
"font_thickness": 0.15,
|
|
217
|
+
"bold": False,
|
|
218
|
+
"italic": False,
|
|
219
|
+
"hide": False,
|
|
220
|
+
"justify": []
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for elem in effects_element[1:]:
|
|
224
|
+
if isinstance(elem, list) and len(elem) > 0:
|
|
225
|
+
elem_type = str(elem[0])
|
|
226
|
+
if elem_type == "font":
|
|
227
|
+
self._parse_font(elem, effects)
|
|
228
|
+
elif elem_type == "justify":
|
|
229
|
+
effects["justify"] = [str(j) for j in elem[1:]]
|
|
230
|
+
elif elem_type == "hide":
|
|
231
|
+
effects["hide"] = True
|
|
232
|
+
|
|
233
|
+
return effects
|
|
234
|
+
|
|
235
|
+
def _parse_font(self, font_element: List[Any], effects: Dict[str, Any]) -> None:
|
|
236
|
+
"""Parse font element within effects."""
|
|
237
|
+
for elem in font_element[1:]:
|
|
238
|
+
if isinstance(elem, list) and len(elem) > 0:
|
|
239
|
+
elem_type = str(elem[0])
|
|
240
|
+
if elem_type == "size":
|
|
241
|
+
try:
|
|
242
|
+
if len(elem) >= 3:
|
|
243
|
+
effects["font_size"] = float(elem[1])
|
|
244
|
+
except (ValueError, IndexError):
|
|
245
|
+
pass
|
|
246
|
+
elif elem_type == "thickness":
|
|
247
|
+
try:
|
|
248
|
+
effects["font_thickness"] = float(elem[1])
|
|
249
|
+
except (ValueError, IndexError):
|
|
250
|
+
pass
|
|
251
|
+
elif elem_type == "bold":
|
|
252
|
+
effects["bold"] = True
|
|
253
|
+
elif elem_type == "italic":
|
|
254
|
+
effects["italic"] = True
|