kicad-sch-api 0.3.0__py3-none-any.whl → 0.5.1__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.
- kicad_sch_api/__init__.py +68 -3
- kicad_sch_api/cli/__init__.py +45 -0
- kicad_sch_api/cli/base.py +302 -0
- kicad_sch_api/cli/bom.py +164 -0
- kicad_sch_api/cli/erc.py +229 -0
- kicad_sch_api/cli/export_docs.py +289 -0
- kicad_sch_api/cli/kicad_to_python.py +169 -0
- kicad_sch_api/cli/netlist.py +94 -0
- kicad_sch_api/cli/types.py +43 -0
- kicad_sch_api/collections/__init__.py +36 -0
- kicad_sch_api/collections/base.py +604 -0
- kicad_sch_api/collections/components.py +1623 -0
- kicad_sch_api/collections/junctions.py +206 -0
- kicad_sch_api/collections/labels.py +508 -0
- kicad_sch_api/collections/wires.py +292 -0
- kicad_sch_api/core/__init__.py +37 -2
- kicad_sch_api/core/collections/__init__.py +5 -0
- kicad_sch_api/core/collections/base.py +248 -0
- kicad_sch_api/core/component_bounds.py +34 -7
- kicad_sch_api/core/components.py +213 -52
- kicad_sch_api/core/config.py +110 -15
- kicad_sch_api/core/connectivity.py +692 -0
- kicad_sch_api/core/exceptions.py +175 -0
- kicad_sch_api/core/factories/__init__.py +5 -0
- kicad_sch_api/core/factories/element_factory.py +278 -0
- kicad_sch_api/core/formatter.py +60 -9
- kicad_sch_api/core/geometry.py +94 -5
- kicad_sch_api/core/junctions.py +26 -75
- kicad_sch_api/core/labels.py +324 -0
- kicad_sch_api/core/managers/__init__.py +30 -0
- kicad_sch_api/core/managers/base.py +76 -0
- kicad_sch_api/core/managers/file_io.py +246 -0
- kicad_sch_api/core/managers/format_sync.py +502 -0
- kicad_sch_api/core/managers/graphics.py +580 -0
- kicad_sch_api/core/managers/hierarchy.py +661 -0
- kicad_sch_api/core/managers/metadata.py +271 -0
- kicad_sch_api/core/managers/sheet.py +492 -0
- kicad_sch_api/core/managers/text_elements.py +537 -0
- kicad_sch_api/core/managers/validation.py +476 -0
- kicad_sch_api/core/managers/wire.py +410 -0
- kicad_sch_api/core/nets.py +305 -0
- kicad_sch_api/core/no_connects.py +252 -0
- kicad_sch_api/core/parser.py +194 -970
- kicad_sch_api/core/parsing_utils.py +63 -0
- kicad_sch_api/core/pin_utils.py +103 -9
- kicad_sch_api/core/schematic.py +1328 -1079
- kicad_sch_api/core/texts.py +316 -0
- kicad_sch_api/core/types.py +159 -23
- kicad_sch_api/core/wires.py +27 -75
- kicad_sch_api/exporters/__init__.py +10 -0
- kicad_sch_api/exporters/python_generator.py +610 -0
- kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
- kicad_sch_api/geometry/__init__.py +38 -0
- kicad_sch_api/geometry/font_metrics.py +22 -0
- kicad_sch_api/geometry/routing.py +211 -0
- kicad_sch_api/geometry/symbol_bbox.py +608 -0
- 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 +145 -0
- kicad_sch_api/parsers/elements/__init__.py +22 -0
- kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
- kicad_sch_api/parsers/elements/label_parser.py +216 -0
- kicad_sch_api/parsers/elements/library_parser.py +165 -0
- kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
- kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
- kicad_sch_api/parsers/elements/symbol_parser.py +485 -0
- kicad_sch_api/parsers/elements/text_parser.py +250 -0
- kicad_sch_api/parsers/elements/wire_parser.py +242 -0
- kicad_sch_api/parsers/registry.py +155 -0
- kicad_sch_api/parsers/utils.py +80 -0
- kicad_sch_api/symbols/__init__.py +18 -0
- kicad_sch_api/symbols/cache.py +467 -0
- kicad_sch_api/symbols/resolver.py +361 -0
- kicad_sch_api/symbols/validators.py +504 -0
- kicad_sch_api/utils/logging.py +555 -0
- kicad_sch_api/utils/logging_decorators.py +587 -0
- kicad_sch_api/utils/validation.py +16 -22
- kicad_sch_api/validation/__init__.py +25 -0
- kicad_sch_api/validation/erc.py +171 -0
- kicad_sch_api/validation/erc_models.py +203 -0
- kicad_sch_api/validation/pin_matrix.py +243 -0
- kicad_sch_api/validation/validators.py +391 -0
- kicad_sch_api/wrappers/__init__.py +14 -0
- kicad_sch_api/wrappers/base.py +89 -0
- kicad_sch_api/wrappers/wire.py +198 -0
- kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
- kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
- kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
- mcp_server/__init__.py +34 -0
- mcp_server/example_logging_integration.py +506 -0
- mcp_server/models.py +252 -0
- mcp_server/server.py +357 -0
- mcp_server/tools/__init__.py +32 -0
- mcp_server/tools/component_tools.py +516 -0
- mcp_server/tools/connectivity_tools.py +532 -0
- mcp_server/tools/consolidated_tools.py +1216 -0
- mcp_server/tools/pin_discovery.py +333 -0
- mcp_server/utils/__init__.py +38 -0
- mcp_server/utils/logging.py +127 -0
- mcp_server/utils.py +36 -0
- kicad_sch_api/core/manhattan_routing.py +0 -430
- kicad_sch_api/core/simple_manhattan.py +0 -228
- kicad_sch_api/core/wire_routing.py +0 -380
- kicad_sch_api-0.3.0.dist-info/METADATA +0 -483
- kicad_sch_api-0.3.0.dist-info/RECORD +0 -31
- kicad_sch_api-0.3.0.dist-info/entry_points.txt +0 -2
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
kicad_sch_api/core/geometry.py
CHANGED
|
@@ -7,7 +7,7 @@ migrated from circuit-synth for improved maintainability.
|
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
9
|
import math
|
|
10
|
-
from typing import Optional, Tuple
|
|
10
|
+
from typing import Optional, Tuple, Union
|
|
11
11
|
|
|
12
12
|
from .types import Point
|
|
13
13
|
|
|
@@ -71,20 +71,29 @@ def apply_transformation(
|
|
|
71
71
|
|
|
72
72
|
Migrated from circuit-synth for accurate pin position calculation.
|
|
73
73
|
|
|
74
|
+
CRITICAL: Symbol coordinates use normal Y-axis (+Y is up), but schematic
|
|
75
|
+
coordinates use inverted Y-axis (+Y is down). We must negate Y from symbol
|
|
76
|
+
space before applying transformations.
|
|
77
|
+
|
|
74
78
|
Args:
|
|
75
|
-
point: Point to transform (x, y) relative to origin
|
|
76
|
-
origin: Component origin point
|
|
79
|
+
point: Point to transform (x, y) relative to origin in SYMBOL space
|
|
80
|
+
origin: Component origin point in SCHEMATIC space
|
|
77
81
|
rotation: Rotation in degrees (0, 90, 180, 270)
|
|
78
82
|
mirror: Mirror axis ("x" or "y" or None)
|
|
79
83
|
|
|
80
84
|
Returns:
|
|
81
|
-
Transformed absolute position (x, y)
|
|
85
|
+
Transformed absolute position (x, y) in SCHEMATIC space
|
|
82
86
|
"""
|
|
83
87
|
x, y = point
|
|
84
88
|
|
|
85
89
|
logger.debug(f"Transforming point ({x}, {y}) with rotation={rotation}°, mirror={mirror}")
|
|
86
90
|
|
|
87
|
-
#
|
|
91
|
+
# CRITICAL: Negate Y to convert from symbol space (normal Y) to schematic space (inverted Y)
|
|
92
|
+
# This must happen BEFORE rotation/mirroring
|
|
93
|
+
y = -y
|
|
94
|
+
logger.debug(f"After Y-axis inversion (symbol→schematic): ({x}, {y})")
|
|
95
|
+
|
|
96
|
+
# Apply mirroring
|
|
88
97
|
if mirror == "x":
|
|
89
98
|
x = -x
|
|
90
99
|
logger.debug(f"After X mirror: ({x}, {y})")
|
|
@@ -109,3 +118,83 @@ def apply_transformation(
|
|
|
109
118
|
|
|
110
119
|
logger.debug(f"Final absolute position: ({final_x}, {final_y})")
|
|
111
120
|
return (final_x, final_y)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def calculate_position_for_pin(
|
|
124
|
+
pin_local_position: Union[Point, Tuple[float, float]],
|
|
125
|
+
desired_pin_position: Union[Point, Tuple[float, float]],
|
|
126
|
+
rotation: float = 0.0,
|
|
127
|
+
mirror: Optional[str] = None,
|
|
128
|
+
grid_size: float = 1.27,
|
|
129
|
+
) -> Point:
|
|
130
|
+
"""
|
|
131
|
+
Calculate component position needed to place a specific pin at a desired location.
|
|
132
|
+
|
|
133
|
+
This is the inverse of get_pin_position() - given where you want a pin to be,
|
|
134
|
+
it calculates where the component center needs to be placed.
|
|
135
|
+
|
|
136
|
+
Useful for aligning components by their pins rather than their centers, which
|
|
137
|
+
is essential for clean horizontal signal flows without unnecessary wire jogs.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
pin_local_position: Pin position in symbol space (from symbol definition)
|
|
141
|
+
desired_pin_position: Where you want the pin to be in schematic space
|
|
142
|
+
rotation: Component rotation in degrees (0, 90, 180, 270)
|
|
143
|
+
mirror: Mirror axis ("x" or "y" or None) - currently unused
|
|
144
|
+
grid_size: Grid size for snapping result (default 1.27mm = 50mil)
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Component position that will place the pin at desired_pin_position
|
|
148
|
+
|
|
149
|
+
Example:
|
|
150
|
+
>>> # Place resistor so pin 2 is at (150, 100)
|
|
151
|
+
>>> pin_pos = Point(0, -3.81) # Pin 2 local position from symbol
|
|
152
|
+
>>> comp_pos = calculate_position_for_pin(pin_pos, (150, 100))
|
|
153
|
+
>>> # Now add component at comp_pos, and pin 2 will be at (150, 100)
|
|
154
|
+
|
|
155
|
+
Note:
|
|
156
|
+
The result is automatically snapped to the KiCAD grid for proper connectivity.
|
|
157
|
+
This function matches the behavior of SchematicSymbol.get_pin_position().
|
|
158
|
+
"""
|
|
159
|
+
# Convert inputs to proper types
|
|
160
|
+
if isinstance(pin_local_position, Point):
|
|
161
|
+
pin_x, pin_y = pin_local_position.x, pin_local_position.y
|
|
162
|
+
else:
|
|
163
|
+
pin_x, pin_y = pin_local_position
|
|
164
|
+
|
|
165
|
+
if isinstance(desired_pin_position, Point):
|
|
166
|
+
target_x, target_y = desired_pin_position.x, desired_pin_position.y
|
|
167
|
+
else:
|
|
168
|
+
target_x, target_y = desired_pin_position
|
|
169
|
+
|
|
170
|
+
logger.debug(
|
|
171
|
+
f"Calculating component position for pin at local ({pin_x}, {pin_y}) "
|
|
172
|
+
f"to reach target ({target_x}, {target_y}) with rotation={rotation}°"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Apply the same transformation that get_pin_position() uses
|
|
176
|
+
# This is a standard 2D rotation matrix (NO Y-axis inversion)
|
|
177
|
+
angle_rad = math.radians(rotation)
|
|
178
|
+
cos_a = math.cos(angle_rad)
|
|
179
|
+
sin_a = math.sin(angle_rad)
|
|
180
|
+
|
|
181
|
+
# Calculate rotated offset (same as get_pin_position)
|
|
182
|
+
rotated_x = pin_x * cos_a - pin_y * sin_a
|
|
183
|
+
rotated_y = pin_x * sin_a + pin_y * cos_a
|
|
184
|
+
|
|
185
|
+
logger.debug(f"Pin offset after rotation: ({rotated_x:.3f}, {rotated_y:.3f})")
|
|
186
|
+
|
|
187
|
+
# Calculate component origin
|
|
188
|
+
# Since: target = component + rotated_offset
|
|
189
|
+
# Therefore: component = target - rotated_offset
|
|
190
|
+
component_x = target_x - rotated_x
|
|
191
|
+
component_y = target_y - rotated_y
|
|
192
|
+
|
|
193
|
+
logger.debug(f"Calculated component position (before grid snap): ({component_x:.3f}, {component_y:.3f})")
|
|
194
|
+
|
|
195
|
+
# Snap to grid for proper KiCAD connectivity
|
|
196
|
+
snapped_x, snapped_y = snap_to_grid((component_x, component_y), grid_size=grid_size)
|
|
197
|
+
|
|
198
|
+
logger.debug(f"Final component position (after grid snap): ({snapped_x:.3f}, {snapped_y:.3f})")
|
|
199
|
+
|
|
200
|
+
return Point(snapped_x, snapped_y)
|
kicad_sch_api/core/junctions.py
CHANGED
|
@@ -9,55 +9,34 @@ import logging
|
|
|
9
9
|
import uuid as uuid_module
|
|
10
10
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
11
11
|
|
|
12
|
+
from .collections import BaseCollection
|
|
12
13
|
from .types import Junction, Point
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
class JunctionCollection:
|
|
18
|
+
class JunctionCollection(BaseCollection[Junction]):
|
|
18
19
|
"""
|
|
19
20
|
Professional junction collection with enhanced management features.
|
|
20
21
|
|
|
22
|
+
Inherits from BaseCollection for standard operations and adds junction-specific
|
|
23
|
+
functionality.
|
|
24
|
+
|
|
21
25
|
Features:
|
|
22
|
-
- Fast UUID-based lookup and indexing
|
|
26
|
+
- Fast UUID-based lookup and indexing (inherited)
|
|
23
27
|
- Position-based junction queries
|
|
24
|
-
- Bulk operations for performance
|
|
28
|
+
- Bulk operations for performance (inherited)
|
|
25
29
|
- Validation and conflict detection
|
|
26
30
|
"""
|
|
27
31
|
|
|
28
|
-
def __init__(self, junctions: Optional[List[Junction]] = None):
|
|
32
|
+
def __init__(self, junctions: Optional[List[Junction]] = None) -> None:
|
|
29
33
|
"""
|
|
30
34
|
Initialize junction collection.
|
|
31
35
|
|
|
32
36
|
Args:
|
|
33
37
|
junctions: Initial list of junctions
|
|
34
38
|
"""
|
|
35
|
-
|
|
36
|
-
self._uuid_index: Dict[str, int] = {}
|
|
37
|
-
self._modified = False
|
|
38
|
-
|
|
39
|
-
# Build UUID index
|
|
40
|
-
self._rebuild_index()
|
|
41
|
-
|
|
42
|
-
logger.debug(f"JunctionCollection initialized with {len(self._junctions)} junctions")
|
|
43
|
-
|
|
44
|
-
def _rebuild_index(self):
|
|
45
|
-
"""Rebuild UUID index for fast lookups."""
|
|
46
|
-
self._uuid_index = {junction.uuid: i for i, junction in enumerate(self._junctions)}
|
|
47
|
-
|
|
48
|
-
def __len__(self) -> int:
|
|
49
|
-
"""Number of junctions in collection."""
|
|
50
|
-
return len(self._junctions)
|
|
51
|
-
|
|
52
|
-
def __iter__(self):
|
|
53
|
-
"""Iterate over junctions."""
|
|
54
|
-
return iter(self._junctions)
|
|
55
|
-
|
|
56
|
-
def __getitem__(self, uuid: str) -> Junction:
|
|
57
|
-
"""Get junction by UUID."""
|
|
58
|
-
if uuid not in self._uuid_index:
|
|
59
|
-
raise KeyError(f"Junction with UUID '{uuid}' not found")
|
|
60
|
-
return self._junctions[self._uuid_index[uuid]]
|
|
39
|
+
super().__init__(junctions, collection_name="junctions")
|
|
61
40
|
|
|
62
41
|
def add(
|
|
63
42
|
self,
|
|
@@ -94,35 +73,12 @@ class JunctionCollection:
|
|
|
94
73
|
# Create junction
|
|
95
74
|
junction = Junction(uuid=uuid, position=position, diameter=diameter, color=color)
|
|
96
75
|
|
|
97
|
-
# Add to collection
|
|
98
|
-
self.
|
|
99
|
-
self._uuid_index[uuid] = len(self._junctions) - 1
|
|
100
|
-
self._modified = True
|
|
76
|
+
# Add to collection using base class method
|
|
77
|
+
self._add_item(junction)
|
|
101
78
|
|
|
102
79
|
logger.debug(f"Added junction at {position}, UUID={uuid}")
|
|
103
80
|
return uuid
|
|
104
81
|
|
|
105
|
-
def remove(self, uuid: str) -> bool:
|
|
106
|
-
"""
|
|
107
|
-
Remove junction by UUID.
|
|
108
|
-
|
|
109
|
-
Args:
|
|
110
|
-
uuid: Junction UUID to remove
|
|
111
|
-
|
|
112
|
-
Returns:
|
|
113
|
-
True if junction was removed, False if not found
|
|
114
|
-
"""
|
|
115
|
-
if uuid not in self._uuid_index:
|
|
116
|
-
return False
|
|
117
|
-
|
|
118
|
-
index = self._uuid_index[uuid]
|
|
119
|
-
del self._junctions[index]
|
|
120
|
-
self._rebuild_index()
|
|
121
|
-
self._modified = True
|
|
122
|
-
|
|
123
|
-
logger.debug(f"Removed junction: {uuid}")
|
|
124
|
-
return True
|
|
125
|
-
|
|
126
82
|
def get_at_position(
|
|
127
83
|
self, position: Union[Point, Tuple[float, float]], tolerance: float = 0.01
|
|
128
84
|
) -> Optional[Junction]:
|
|
@@ -139,7 +95,7 @@ class JunctionCollection:
|
|
|
139
95
|
if isinstance(position, tuple):
|
|
140
96
|
position = Point(position[0], position[1])
|
|
141
97
|
|
|
142
|
-
for junction in self.
|
|
98
|
+
for junction in self._items:
|
|
143
99
|
if junction.position.distance_to(position) <= tolerance:
|
|
144
100
|
return junction
|
|
145
101
|
|
|
@@ -162,40 +118,35 @@ class JunctionCollection:
|
|
|
162
118
|
point = Point(point[0], point[1])
|
|
163
119
|
|
|
164
120
|
matching_junctions = []
|
|
165
|
-
for junction in self.
|
|
121
|
+
for junction in self._items:
|
|
166
122
|
if junction.position.distance_to(point) <= tolerance:
|
|
167
123
|
matching_junctions.append(junction)
|
|
168
124
|
|
|
169
125
|
return matching_junctions
|
|
170
126
|
|
|
171
127
|
def get_statistics(self) -> Dict[str, Any]:
|
|
172
|
-
"""Get junction collection statistics."""
|
|
173
|
-
|
|
174
|
-
|
|
128
|
+
"""Get junction collection statistics (extends base statistics)."""
|
|
129
|
+
base_stats = super().get_statistics()
|
|
130
|
+
if not self._items:
|
|
131
|
+
return {**base_stats, "total_junctions": 0, "avg_diameter": 0, "positions": []}
|
|
175
132
|
|
|
176
|
-
avg_diameter = sum(j.diameter for j in self.
|
|
177
|
-
positions = [(j.position.x, j.position.y) for j in self.
|
|
133
|
+
avg_diameter = sum(j.diameter for j in self._items) / len(self._items)
|
|
134
|
+
positions = [(j.position.x, j.position.y) for j in self._items]
|
|
178
135
|
|
|
179
136
|
return {
|
|
180
|
-
|
|
137
|
+
**base_stats,
|
|
138
|
+
"total_junctions": len(self._items),
|
|
181
139
|
"avg_diameter": avg_diameter,
|
|
182
140
|
"positions": positions,
|
|
183
|
-
"unique_diameters": len(set(j.diameter for j in self.
|
|
184
|
-
"unique_colors": len(set(j.color for j in self.
|
|
141
|
+
"unique_diameters": len(set(j.diameter for j in self._items)),
|
|
142
|
+
"unique_colors": len(set(j.color for j in self._items)),
|
|
185
143
|
}
|
|
186
144
|
|
|
187
|
-
def clear(self):
|
|
188
|
-
"""Remove all junctions from collection."""
|
|
189
|
-
self._junctions.clear()
|
|
190
|
-
self._uuid_index.clear()
|
|
191
|
-
self._modified = True
|
|
192
|
-
logger.debug("Cleared all junctions")
|
|
193
|
-
|
|
194
145
|
@property
|
|
195
146
|
def modified(self) -> bool:
|
|
196
147
|
"""Check if collection has been modified."""
|
|
197
|
-
return self.
|
|
148
|
+
return self.is_modified()
|
|
198
149
|
|
|
199
|
-
def mark_saved(self):
|
|
150
|
+
def mark_saved(self) -> None:
|
|
200
151
|
"""Mark collection as saved (reset modified flag)."""
|
|
201
|
-
self.
|
|
152
|
+
self.reset_modified_flag()
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Label element management for KiCAD schematics.
|
|
3
|
+
|
|
4
|
+
This module provides collection classes for managing label elements,
|
|
5
|
+
featuring fast lookup, bulk operations, and validation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import uuid
|
|
10
|
+
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
|
|
11
|
+
|
|
12
|
+
from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
|
|
13
|
+
from .collections import BaseCollection
|
|
14
|
+
from .types import Label, Point
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LabelElement:
|
|
20
|
+
"""
|
|
21
|
+
Enhanced wrapper for schematic label elements with modern API.
|
|
22
|
+
|
|
23
|
+
Provides intuitive access to label properties and operations
|
|
24
|
+
while maintaining exact format preservation.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, label_data: Label, parent_collection: "LabelCollection"):
|
|
28
|
+
"""
|
|
29
|
+
Initialize label element wrapper.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
label_data: Underlying label data
|
|
33
|
+
parent_collection: Parent collection for updates
|
|
34
|
+
"""
|
|
35
|
+
self._data = label_data
|
|
36
|
+
self._collection = parent_collection
|
|
37
|
+
self._validator = SchematicValidator()
|
|
38
|
+
|
|
39
|
+
# Core properties with validation
|
|
40
|
+
@property
|
|
41
|
+
def uuid(self) -> str:
|
|
42
|
+
"""Label element UUID."""
|
|
43
|
+
return self._data.uuid
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def text(self) -> str:
|
|
47
|
+
"""Label text (net name)."""
|
|
48
|
+
return self._data.text
|
|
49
|
+
|
|
50
|
+
@text.setter
|
|
51
|
+
def text(self, value: str):
|
|
52
|
+
"""Set label text with validation."""
|
|
53
|
+
if not isinstance(value, str) or not value.strip():
|
|
54
|
+
raise ValidationError("Label text cannot be empty")
|
|
55
|
+
old_text = self._data.text
|
|
56
|
+
self._data.text = value.strip()
|
|
57
|
+
self._collection._update_text_index(old_text, self)
|
|
58
|
+
self._collection._mark_modified()
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def position(self) -> Point:
|
|
62
|
+
"""Label position."""
|
|
63
|
+
return self._data.position
|
|
64
|
+
|
|
65
|
+
@position.setter
|
|
66
|
+
def position(self, value: Union[Point, Tuple[float, float]]):
|
|
67
|
+
"""Set label position."""
|
|
68
|
+
if isinstance(value, tuple):
|
|
69
|
+
value = Point(value[0], value[1])
|
|
70
|
+
elif not isinstance(value, Point):
|
|
71
|
+
raise ValidationError(f"Position must be Point or tuple, got {type(value)}")
|
|
72
|
+
self._data.position = value
|
|
73
|
+
self._collection._mark_modified()
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def rotation(self) -> float:
|
|
77
|
+
"""Label rotation in degrees."""
|
|
78
|
+
return self._data.rotation
|
|
79
|
+
|
|
80
|
+
@rotation.setter
|
|
81
|
+
def rotation(self, value: float):
|
|
82
|
+
"""Set label rotation."""
|
|
83
|
+
self._data.rotation = float(value)
|
|
84
|
+
self._collection._mark_modified()
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def size(self) -> float:
|
|
88
|
+
"""Label text size."""
|
|
89
|
+
return self._data.size
|
|
90
|
+
|
|
91
|
+
@size.setter
|
|
92
|
+
def size(self, value: float):
|
|
93
|
+
"""Set label size with validation."""
|
|
94
|
+
if value <= 0:
|
|
95
|
+
raise ValidationError(f"Label size must be positive, got {value}")
|
|
96
|
+
self._data.size = float(value)
|
|
97
|
+
self._collection._mark_modified()
|
|
98
|
+
|
|
99
|
+
def validate(self) -> List[ValidationIssue]:
|
|
100
|
+
"""Validate this label element."""
|
|
101
|
+
return self._validator.validate_label(self._data.__dict__)
|
|
102
|
+
|
|
103
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
104
|
+
"""Convert label element to dictionary representation."""
|
|
105
|
+
return {
|
|
106
|
+
"uuid": self.uuid,
|
|
107
|
+
"text": self.text,
|
|
108
|
+
"position": {"x": self.position.x, "y": self.position.y},
|
|
109
|
+
"rotation": self.rotation,
|
|
110
|
+
"size": self.size,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
def __str__(self) -> str:
|
|
114
|
+
"""String representation."""
|
|
115
|
+
return f"<Label '{self.text}' @ {self.position}>"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class LabelCollection(BaseCollection[LabelElement]):
|
|
119
|
+
"""
|
|
120
|
+
Collection class for efficient label element management.
|
|
121
|
+
|
|
122
|
+
Inherits from BaseCollection for standard operations and adds label-specific
|
|
123
|
+
functionality including text-based indexing.
|
|
124
|
+
|
|
125
|
+
Provides fast lookup, filtering, and bulk operations for schematic label elements.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(self, labels: List[Label] = None):
|
|
129
|
+
"""
|
|
130
|
+
Initialize label collection.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
labels: Initial list of label data
|
|
134
|
+
"""
|
|
135
|
+
# Initialize base collection
|
|
136
|
+
super().__init__([], collection_name="labels")
|
|
137
|
+
|
|
138
|
+
# Additional label-specific index
|
|
139
|
+
self._text_index: Dict[str, List[LabelElement]] = {}
|
|
140
|
+
|
|
141
|
+
# Add initial labels
|
|
142
|
+
if labels:
|
|
143
|
+
for label_data in labels:
|
|
144
|
+
self._add_to_indexes(LabelElement(label_data, self))
|
|
145
|
+
|
|
146
|
+
def add(
|
|
147
|
+
self,
|
|
148
|
+
text: str,
|
|
149
|
+
position: Union[Point, Tuple[float, float]],
|
|
150
|
+
rotation: float = 0.0,
|
|
151
|
+
size: float = 1.27,
|
|
152
|
+
label_uuid: Optional[str] = None,
|
|
153
|
+
) -> LabelElement:
|
|
154
|
+
"""
|
|
155
|
+
Add a new label element to the schematic.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
text: Label text (net name)
|
|
159
|
+
position: Label position
|
|
160
|
+
rotation: Label rotation in degrees
|
|
161
|
+
size: Label text size
|
|
162
|
+
label_uuid: Specific UUID for label (auto-generated if None)
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Newly created LabelElement
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
ValidationError: If label data is invalid
|
|
169
|
+
"""
|
|
170
|
+
# Validate inputs
|
|
171
|
+
if not isinstance(text, str) or not text.strip():
|
|
172
|
+
raise ValidationError("Label text cannot be empty")
|
|
173
|
+
|
|
174
|
+
if isinstance(position, tuple):
|
|
175
|
+
position = Point(position[0], position[1])
|
|
176
|
+
elif not isinstance(position, Point):
|
|
177
|
+
raise ValidationError(f"Position must be Point or tuple, got {type(position)}")
|
|
178
|
+
|
|
179
|
+
if size <= 0:
|
|
180
|
+
raise ValidationError(f"Label size must be positive, got {size}")
|
|
181
|
+
|
|
182
|
+
# Generate UUID if not provided
|
|
183
|
+
if not label_uuid:
|
|
184
|
+
label_uuid = str(uuid.uuid4())
|
|
185
|
+
|
|
186
|
+
# Check for duplicate UUID
|
|
187
|
+
if label_uuid in self._uuid_index:
|
|
188
|
+
raise ValidationError(f"Label UUID {label_uuid} already exists")
|
|
189
|
+
|
|
190
|
+
# Create label data
|
|
191
|
+
label_data = Label(
|
|
192
|
+
uuid=label_uuid,
|
|
193
|
+
position=position,
|
|
194
|
+
text=text.strip(),
|
|
195
|
+
rotation=rotation,
|
|
196
|
+
size=size,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Create wrapper and add to collection
|
|
200
|
+
label_element = LabelElement(label_data, self)
|
|
201
|
+
self._add_to_indexes(label_element)
|
|
202
|
+
self._mark_modified()
|
|
203
|
+
|
|
204
|
+
logger.debug(f"Added label: {label_element}")
|
|
205
|
+
return label_element
|
|
206
|
+
|
|
207
|
+
# get() method inherited from BaseCollection
|
|
208
|
+
|
|
209
|
+
def get_by_text(self, text: str) -> List[LabelElement]:
|
|
210
|
+
"""Get all labels with the given text."""
|
|
211
|
+
return self._text_index.get(text, []).copy()
|
|
212
|
+
|
|
213
|
+
def remove(self, label_uuid: str) -> bool:
|
|
214
|
+
"""
|
|
215
|
+
Remove label by UUID.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
label_uuid: UUID of label to remove
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
True if label was removed, False if not found
|
|
222
|
+
"""
|
|
223
|
+
label_element = self.get(label_uuid)
|
|
224
|
+
if not label_element:
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
# Remove from text index
|
|
228
|
+
text = label_element.text
|
|
229
|
+
if text in self._text_index:
|
|
230
|
+
self._text_index[text].remove(label_element)
|
|
231
|
+
if not self._text_index[text]:
|
|
232
|
+
del self._text_index[text]
|
|
233
|
+
|
|
234
|
+
# Remove using base class method
|
|
235
|
+
super().remove(label_uuid)
|
|
236
|
+
|
|
237
|
+
logger.debug(f"Removed label: {label_element}")
|
|
238
|
+
return True
|
|
239
|
+
|
|
240
|
+
def find_by_text(self, text: str, exact: bool = True) -> List[LabelElement]:
|
|
241
|
+
"""
|
|
242
|
+
Find labels by text.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
text: Text to search for
|
|
246
|
+
exact: If True, exact match; if False, substring match
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
List of matching label elements
|
|
250
|
+
"""
|
|
251
|
+
if exact:
|
|
252
|
+
return self._text_index.get(text, []).copy()
|
|
253
|
+
else:
|
|
254
|
+
matches = []
|
|
255
|
+
for label_element in self._items:
|
|
256
|
+
if text.lower() in label_element.text.lower():
|
|
257
|
+
matches.append(label_element)
|
|
258
|
+
return matches
|
|
259
|
+
|
|
260
|
+
def filter(self, predicate: Callable[[LabelElement], bool]) -> List[LabelElement]:
|
|
261
|
+
"""
|
|
262
|
+
Filter labels by predicate function.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
predicate: Function that returns True for labels to include
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
List of labels matching predicate
|
|
269
|
+
"""
|
|
270
|
+
return [label for label in self._items if predicate(label)]
|
|
271
|
+
|
|
272
|
+
def bulk_update(self, criteria: Callable[[LabelElement], bool], updates: Dict[str, Any]):
|
|
273
|
+
"""
|
|
274
|
+
Update multiple labels matching criteria.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
criteria: Function to select labels to update
|
|
278
|
+
updates: Dictionary of property updates
|
|
279
|
+
"""
|
|
280
|
+
updated_count = 0
|
|
281
|
+
for label_element in self._items:
|
|
282
|
+
if criteria(label_element):
|
|
283
|
+
for prop, value in updates.items():
|
|
284
|
+
if hasattr(label_element, prop):
|
|
285
|
+
setattr(label_element, prop, value)
|
|
286
|
+
updated_count += 1
|
|
287
|
+
|
|
288
|
+
if updated_count > 0:
|
|
289
|
+
self._mark_modified()
|
|
290
|
+
logger.debug(f"Bulk updated {updated_count} label properties")
|
|
291
|
+
|
|
292
|
+
def clear(self):
|
|
293
|
+
"""Remove all labels from collection."""
|
|
294
|
+
self._text_index.clear()
|
|
295
|
+
super().clear()
|
|
296
|
+
|
|
297
|
+
def _add_to_indexes(self, label_element: LabelElement):
|
|
298
|
+
"""Add label to internal indexes (base + text index)."""
|
|
299
|
+
self._add_item(label_element)
|
|
300
|
+
|
|
301
|
+
# Add to text index
|
|
302
|
+
text = label_element.text
|
|
303
|
+
if text not in self._text_index:
|
|
304
|
+
self._text_index[text] = []
|
|
305
|
+
self._text_index[text].append(label_element)
|
|
306
|
+
|
|
307
|
+
def _update_text_index(self, old_text: str, label_element: LabelElement):
|
|
308
|
+
"""Update text index when label text changes."""
|
|
309
|
+
# Remove from old text index
|
|
310
|
+
if old_text in self._text_index:
|
|
311
|
+
self._text_index[old_text].remove(label_element)
|
|
312
|
+
if not self._text_index[old_text]:
|
|
313
|
+
del self._text_index[old_text]
|
|
314
|
+
|
|
315
|
+
# Add to new text index
|
|
316
|
+
new_text = label_element.text
|
|
317
|
+
if new_text not in self._text_index:
|
|
318
|
+
self._text_index[new_text] = []
|
|
319
|
+
self._text_index[new_text].append(label_element)
|
|
320
|
+
|
|
321
|
+
# Collection interface methods - __len__, __iter__, __getitem__ inherited from BaseCollection
|
|
322
|
+
def __bool__(self) -> bool:
|
|
323
|
+
"""Return True if collection has labels."""
|
|
324
|
+
return len(self._items) > 0
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Schematic management modules for separating responsibilities.
|
|
3
|
+
|
|
4
|
+
This package contains specialized managers for different aspects of schematic
|
|
5
|
+
manipulation, enabling clean separation of concerns and better maintainability.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .base import BaseManager
|
|
9
|
+
from .file_io import FileIOManager
|
|
10
|
+
from .format_sync import FormatSyncManager
|
|
11
|
+
from .graphics import GraphicsManager
|
|
12
|
+
from .hierarchy import HierarchyManager
|
|
13
|
+
from .metadata import MetadataManager
|
|
14
|
+
from .sheet import SheetManager
|
|
15
|
+
from .text_elements import TextElementManager
|
|
16
|
+
from .validation import ValidationManager
|
|
17
|
+
from .wire import WireManager
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"BaseManager",
|
|
21
|
+
"FileIOManager",
|
|
22
|
+
"FormatSyncManager",
|
|
23
|
+
"GraphicsManager",
|
|
24
|
+
"HierarchyManager",
|
|
25
|
+
"MetadataManager",
|
|
26
|
+
"SheetManager",
|
|
27
|
+
"TextElementManager",
|
|
28
|
+
"ValidationManager",
|
|
29
|
+
"WireManager",
|
|
30
|
+
]
|